---
phase: 15-multi-signer-backend
plan: 02
type: execute
wave: 2
depends_on: [15-01]
files_modified:
  - teressa-copeland-homes/src/app/api/documents/[id]/send/route.ts
autonomous: true
requirements: [MSIGN-05, MSIGN-06]
must_haves:
  truths:
    - "When documents.signers is populated, one signing token is created per signer with that signer's email"
    - "All signing request emails are dispatched in parallel via Promise.all"
    - "When documents.signers is null/empty, the existing single-signer behavior is preserved exactly"
    - "The signing URL uses APP_BASE_URL (not NEXT_PUBLIC_BASE_URL)"
    - "Document status is set to Sent after all emails dispatch successfully"
    - "Each signer gets an audit event email_sent with metadata.signerEmail"
  artifacts:
    - path: "teressa-copeland-homes/src/app/api/documents/[id]/send/route.ts"
      provides: "Multi-signer send route with legacy fallback"
      exports: ["POST"]
  key_links:
    - from: "src/app/api/documents/[id]/send/route.ts"
      to: "src/lib/signing/token.ts"
      via: "createSigningToken(doc.id, signer.email)"
      pattern: "createSigningToken.*signerEmail"
    - from: "src/app/api/documents/[id]/send/route.ts"
      to: "src/lib/signing/signing-mailer.tsx"
      via: "sendSigningRequestEmail in Promise.all loop"
      pattern: "Promise\\.all.*sendSigningRequestEmail"
---

<objective>
Rewrite the send route to loop over document signers, create one token per signer, and dispatch all signing emails in parallel. Preserve exact legacy single-signer behavior when signers is null/empty. Fix NEXT_PUBLIC_BASE_URL to APP_BASE_URL.

Purpose: This is the entry point for multi-signer flow — without this, documents can only be sent to one recipient.
Output: Rewritten send/route.ts with multi-signer token loop + legacy fallback.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/15-multi-signer-backend/15-CONTEXT.md
@.planning/phases/15-multi-signer-backend/15-RESEARCH.md
@.planning/phases/15-multi-signer-backend/15-01-SUMMARY.md

@teressa-copeland-homes/src/app/api/documents/[id]/send/route.ts
@teressa-copeland-homes/src/lib/signing/token.ts
@teressa-copeland-homes/src/lib/db/schema.ts

<interfaces>
<!-- From Plan 01 — token.ts now has: -->
```typescript
export async function createSigningToken(documentId: string, signerEmail?: string): Promise<{ token: string; jti: string; expiresAt: Date }>;
```

<!-- From schema.ts — documents.signers type: -->
```typescript
export interface DocumentSigner { email: string; color: string; }
// documents.signers is jsonb().$type<DocumentSigner[]>() — nullable
```

<!-- Current send route signature (being rewritten): -->
```typescript
export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> })
```

<!-- Existing imports used by send route: -->
```typescript
import { createSigningToken } from '@/lib/signing/token';
import { sendSigningRequestEmail } from '@/lib/signing/signing-mailer';
import { logAuditEvent } from '@/lib/signing/audit';
import { documents, clients } from '@/lib/db/schema';
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Rewrite send route with multi-signer token loop and legacy fallback</name>
  <files>teressa-copeland-homes/src/app/api/documents/[id]/send/route.ts</files>
  <read_first>
    - teressa-copeland-homes/src/app/api/documents/[id]/send/route.ts
    - teressa-copeland-homes/src/lib/signing/token.ts (confirm signerEmail param exists from Plan 01)
    - teressa-copeland-homes/src/lib/signing/signing-mailer.tsx (confirm sendSigningRequestEmail signature)
    - teressa-copeland-homes/src/lib/db/schema.ts (confirm DocumentSigner type, documents.signers)
    - teressa-copeland-homes/node_modules/next/dist/docs/ (scan for route handler API)
  </read_first>
  <action>
    Rewrite the POST handler in `src/app/api/documents/[id]/send/route.ts`. The structure:

    **Keep unchanged:** auth guard, doc fetch, preparedFilePath guard, status === 'Signed' guard, error boundary try/catch.

    **Change 1 — APP_BASE_URL (Pitfall 5, per quality gate):** Replace `process.env.NEXT_PUBLIC_BASE_URL` with `process.env.APP_BASE_URL` on the baseUrl line. Keep `?? 'http://localhost:3000'` fallback.

    **Change 2 — Branching logic (D-01, D-02):** After the doc fetch and guards, branch on `doc.signers && doc.signers.length > 0`:

    **Multi-signer path (D-02):**
    ```typescript
    const signers = doc.signers as DocumentSigner[];
    const baseUrl = process.env.APP_BASE_URL ?? 'http://localhost:3000';

    await Promise.all(
      signers.map(async (signer) => {
        const { token, expiresAt } = await createSigningToken(doc.id, signer.email);
        const signingUrl = `${baseUrl}/sign/${token}`;
        await sendSigningRequestEmail({
          to: signer.email,
          documentName: doc.name,
          signingUrl,
          expiresAt,
        });
        await logAuditEvent({
          documentId: doc.id,
          eventType: 'email_sent',
          metadata: { signerEmail: signer.email },
        });
      })
    );
    ```

    Note: `sendSigningRequestEmail` has an optional `clientName` param. In multi-signer mode, we do NOT have a per-signer name (DocumentSigner is `{ email, color }` only). Omitting `clientName` is fine — the email template handles it gracefully.

    **Legacy single-signer path (D-01):** Keep the existing logic verbatim — resolve clientId, fetch client, create single token (no signerEmail), send email with client.name, log audit event. Only change is `APP_BASE_URL` instead of `NEXT_PUBLIC_BASE_URL`.

    **After branch:** Both paths converge — update status to 'Sent' if currently 'Draft' (existing logic, unchanged). Return `{ ok: true, expiresAt }` — for multi-signer, use the last signer's expiresAt or just return `ok: true` without expiresAt (all tokens have the same 72h TTL anyway).

    **Import changes:** Add `DocumentSigner` import from schema if not already imported.

    **Error handling (Claude's Discretion):** Individual signer email failure should not block the entire send. Wrap each signer's map callback in try/catch — if one email fails, the others still send. Collect errors and return 207 Multi-Status if partial failure, or 502 if all fail. Alternatively, keep it simple: let Promise.all fail if any email fails (current behavior for single signer). Decision: keep Promise.all as-is for now (simple, consistent with existing pattern). If one email fails, the entire send fails and the agent retries.

    Complete rewrite:

    ```typescript
    import { NextResponse } from 'next/server';
    import { auth } from '@/lib/auth';
    import { db } from '@/lib/db';
    import { documents, clients, DocumentSigner } from '@/lib/db/schema';
    import { eq } from 'drizzle-orm';
    import { createSigningToken } from '@/lib/signing/token';
    import { sendSigningRequestEmail } from '@/lib/signing/signing-mailer';
    import { logAuditEvent } from '@/lib/signing/audit';

    export async function POST(
      _req: Request,
      { params }: { params: Promise<{ id: string }> }
    ) {
      const session = await auth();
      if (!session) return new Response('Unauthorized', { status: 401 });

      const { id } = await params;

      try {
        const doc = await db.query.documents.findFirst({
          where: eq(documents.id, id),
        });
        if (!doc) return NextResponse.json({ error: 'Document not found' }, { status: 404 });
        if (!doc.preparedFilePath)
          return NextResponse.json({ error: 'Document not yet prepared' }, { status: 422 });
        if (doc.status === 'Signed')
          return NextResponse.json({ error: 'Already signed' }, { status: 409 });

        const baseUrl = process.env.APP_BASE_URL ?? 'http://localhost:3000';

        if (doc.signers && (doc.signers as DocumentSigner[]).length > 0) {
          // ── Multi-signer path (D-02) ──
          const signers = doc.signers as DocumentSigner[];
          await Promise.all(
            signers.map(async (signer) => {
              const { token, expiresAt } = await createSigningToken(doc.id, signer.email);
              const signingUrl = `${baseUrl}/sign/${token}`;
              await sendSigningRequestEmail({
                to: signer.email,
                documentName: doc.name,
                signingUrl,
                expiresAt,
              });
              await logAuditEvent({
                documentId: doc.id,
                eventType: 'email_sent',
                metadata: { signerEmail: signer.email },
              });
            })
          );
        } else {
          // ── Legacy single-signer path (D-01) ──
          const clientId = doc.assignedClientId ?? doc.clientId;
          const client = await db.query.clients.findFirst({ where: eq(clients.id, clientId) });
          if (!client) return NextResponse.json({ error: 'Client not found' }, { status: 422 });

          const { token, expiresAt } = await createSigningToken(doc.id);
          const signingUrl = `${baseUrl}/sign/${token}`;
          await sendSigningRequestEmail({
            to: client.email,
            clientName: client.name,
            documentName: doc.name,
            signingUrl,
            expiresAt,
          });
          await logAuditEvent({ documentId: doc.id, eventType: 'email_sent' });
        }

        // Update status to Sent (skip if already Sent or Signed to avoid downgrade)
        if (doc.status === 'Draft') {
          await db
            .update(documents)
            .set({ status: 'Sent', sentAt: new Date() })
            .where(eq(documents.id, id));
        }

        return NextResponse.json({ ok: true });
      } catch (err) {
        console.error('[send] error:', err);
        return NextResponse.json({ error: 'Failed to send signing email' }, { status: 502 });
      }
    }
    ```
  </action>
  <verify>
    <automated>cd teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30</automated>
  </verify>
  <acceptance_criteria>
    - grep confirms APP_BASE_URL: `grep "APP_BASE_URL" src/app/api/documents/\[id\]/send/route.ts`
    - grep confirms NO NEXT_PUBLIC_BASE_URL: `grep -c "NEXT_PUBLIC_BASE_URL" src/app/api/documents/\[id\]/send/route.ts` returns 0
    - grep confirms multi-signer branch: `grep "doc.signers" src/app/api/documents/\[id\]/send/route.ts`
    - grep confirms Promise.all: `grep "Promise.all" src/app/api/documents/\[id\]/send/route.ts`
    - grep confirms createSigningToken with signerEmail: `grep "createSigningToken(doc.id, signer.email)" src/app/api/documents/\[id\]/send/route.ts`
    - grep confirms legacy path still uses createSigningToken without signerEmail: `grep "createSigningToken(doc.id)" src/app/api/documents/\[id\]/send/route.ts`
    - grep confirms metadata with signerEmail in audit: `grep "signerEmail: signer.email" src/app/api/documents/\[id\]/send/route.ts`
    - grep confirms DocumentSigner import: `grep "DocumentSigner" src/app/api/documents/\[id\]/send/route.ts`
    - `npx tsc --noEmit` passes with zero errors
  </acceptance_criteria>
  <done>Send route creates one token per signer with signerEmail written to DB, dispatches all emails in parallel. Legacy single-signer path preserved unchanged. APP_BASE_URL replaces NEXT_PUBLIC_BASE_URL.</done>
</task>

</tasks>

<verification>
`cd teressa-copeland-homes && npx tsc --noEmit` passes
Send route has both multi-signer and legacy branches
APP_BASE_URL used instead of NEXT_PUBLIC_BASE_URL
</verification>

<success_criteria>
1. Multi-signer documents: `doc.signers = [{email: 'a@x.com', color: '#f00'}, {email: 'b@x.com', color: '#0f0'}]` results in 2 tokens created, 2 emails sent, 2 audit events
2. Legacy documents: `doc.signers = null` results in 1 token (no signerEmail), 1 email to assigned client
3. `APP_BASE_URL` is the env var used for signing URLs (not `NEXT_PUBLIC_BASE_URL`)
4. `npx tsc --noEmit` passes
</success_criteria>

<output>
After completion, create `.planning/phases/15-multi-signer-backend/15-02-SUMMARY.md`
</output>
