--- phase: 06-signing-flow plan: "02" type: execute wave: 2 depends_on: - "06-01" files_modified: - teressa-copeland-homes/src/emails/SigningRequestEmail.tsx - teressa-copeland-homes/src/lib/signing/signing-mailer.tsx - teressa-copeland-homes/src/app/api/documents/[id]/send/route.ts - teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts autonomous: true requirements: - SIGN-01 - LEGAL-01 must_haves: truths: - "POST /api/documents/[id]/send creates a signing token, stores jti in signingTokens, sends a branded HTML email with the signing URL, logs email_sent audit event, and updates document status to Sent" - "The email sender is 'Teressa Copeland '" - "The email body includes document name, expiry date, and a prominent CTA button linking to /sign/[token]" - "Agent receives notification email when client completes signing (logged on POST /api/sign/[token] completion — wired in plan 04)" - "POST /api/documents/[id]/prepare now logs a document_prepared audit event" - "npm run build passes cleanly" artifacts: - path: "teressa-copeland-homes/src/emails/SigningRequestEmail.tsx" provides: "Branded React Email component for signing request" exports: ["SigningRequestEmail"] - path: "teressa-copeland-homes/src/lib/signing/signing-mailer.tsx" provides: "sendSigningRequestEmail(), sendAgentNotificationEmail()" exports: ["sendSigningRequestEmail", "sendAgentNotificationEmail"] - path: "teressa-copeland-homes/src/app/api/documents/[id]/send/route.ts" provides: "POST handler: create token, send email, log audit event, update status" key_links: - from: "send/route.ts" to: "createSigningToken() in token.ts" via: "import and call to create JWT + DB row" - from: "send/route.ts" to: "logAuditEvent() in audit.ts" via: "logs email_sent after sendMail resolves" - from: "signing-mailer.tsx" to: "SigningRequestEmail.tsx" via: "@react-email/render renders component to HTML string" - from: "prepare/route.ts" to: "logAuditEvent()" via: "logs document_prepared event after successful PDF preparation" --- Implement the email delivery layer: branded signing request email, agent notification, and the POST /api/documents/[id]/send endpoint that kicks off the signing ceremony. Purpose: SIGN-01 requires clients receive a unique link via email with no account required. LEGAL-01 requires logging document_prepared and email_sent audit events. Output: Branded HTML email component (React Email), signing-mailer utilities, send API route, and document_prepared audit logging added to existing prepare route. @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/06-signing-flow/06-CONTEXT.md @.planning/phases/06-signing-flow/06-RESEARCH.md @.planning/phases/06-signing-flow/06-01-SUMMARY.md From teressa-copeland-homes/src/lib/signing/token.ts: ```typescript export async function createSigningToken(documentId: string): Promise<{ token: string; jti: string; expiresAt: Date }> ``` From teressa-copeland-homes/src/lib/signing/audit.ts: ```typescript type AuditEventType = 'document_prepared' | 'email_sent' | 'link_opened' | 'document_viewed' | 'signature_submitted' | 'pdf_hash_computed'; export async function logAuditEvent(opts: { documentId: string; eventType: AuditEventType; ipAddress?: string; userAgent?: string; metadata?: Record; }): Promise ``` From existing prepare route (teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts): - POST handler that calls preparePdf(), updates preparedFilePath/textFillData/assignedClientId/status to Sent - Currently does NOT log any audit events — Task 2 adds document_prepared logging here From existing contact-mailer.ts (SMTP pattern reference): ```typescript import nodemailer from 'nodemailer'; const transporter = nodemailer.createTransport({ host: process.env.CONTACT_SMTP_HOST, port: Number(process.env.CONTACT_SMTP_PORT ?? 587), secure: false, auth: { user: process.env.CONTACT_EMAIL_USER, pass: process.env.CONTACT_EMAIL_PASS }, }); ``` Reuse the same CONTACT_SMTP_* env vars for the signing email — same SMTP provider. Task 1: Branded signing request email + mailer teressa-copeland-homes/src/emails/SigningRequestEmail.tsx teressa-copeland-homes/src/lib/signing/signing-mailer.tsx Create directory: teressa-copeland-homes/src/emails/ **src/emails/SigningRequestEmail.tsx** — React Email component for signing request: The email must match locked decisions from CONTEXT.md: - Sender name: "Teressa Copeland" - Body: document name, expiry deadline, instruction "No account needed — just click the button below" - Prominent "Review & Sign" CTA button Use @react-email/components primitives (Html, Head, Body, Container, Heading, Text, Button, Hr, Preview). Brand colors: navy #1B2B4B, gold #C9A84C, cream/white background. ```typescript // src/emails/SigningRequestEmail.tsx import { Html, Head, Body, Container, Heading, Text, Button, Hr, Preview } from '@react-email/components'; interface SigningRequestEmailProps { documentName: string; signingUrl: string; expiryDate: string; // e.g. "March 25, 2026" clientName?: string; } export function SigningRequestEmail({ documentName, signingUrl, expiryDate, clientName }: SigningRequestEmailProps) { return ( Please review and sign: {documentName} Teressa Copeland Homes
{clientName && ( Hello {clientName}, )} You have a document ready for your review and signature: {documentName} No account needed — just click the button below. This link expires on {expiryDate}. If you did not expect this document, you can safely ignore this email.
Teressa Copeland Homes · Utah Licensed Real Estate Agent
); } ``` Also create a minimal agent notification component inline in signing-mailer.tsx (no separate file needed — it is simpler). **src/lib/signing/signing-mailer.tsx** — mailer functions. IMPORTANT: Must be .tsx extension (not .ts) because it calls render() with JSX: ```typescript import { render } from '@react-email/render'; import nodemailer from 'nodemailer'; import { SigningRequestEmail } from '@/emails/SigningRequestEmail'; import React from 'react'; function createTransporter() { return nodemailer.createTransport({ host: process.env.CONTACT_SMTP_HOST!, port: Number(process.env.CONTACT_SMTP_PORT ?? 587), secure: false, auth: { user: process.env.CONTACT_EMAIL_USER!, pass: process.env.CONTACT_EMAIL_PASS!, }, }); } export async function sendSigningRequestEmail(opts: { to: string; clientName?: string; documentName: string; signingUrl: string; expiresAt: Date; }): Promise { const expiryDate = opts.expiresAt.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric', }); const html = await render( React.createElement(SigningRequestEmail, { documentName: opts.documentName, signingUrl: opts.signingUrl, expiryDate, clientName: opts.clientName, }) ); const transporter = createTransporter(); await transporter.sendMail({ from: '"Teressa Copeland" ', to: opts.to, subject: `Please sign: ${opts.documentName}`, html, }); } export async function sendAgentNotificationEmail(opts: { clientName: string; documentName: string; signedAt: Date; }): Promise { const formattedTime = opts.signedAt.toLocaleString('en-US', { month: 'long', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', timeZoneName: 'short', }); const transporter = createTransporter(); await transporter.sendMail({ from: '"Teressa Copeland Homes" ', to: 'teressa@teressacopelandhomes.com', subject: `Signed: ${opts.documentName}`, text: `${opts.clientName} has signed "${opts.documentName}" on ${formattedTime}.`, }); } ```
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | tail -5 SigningRequestEmail.tsx and signing-mailer.tsx exist and compile; npm run build passes
Task 2: Send API route + document_prepared audit logging teressa-copeland-homes/src/app/api/documents/[id]/send/route.ts teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts **Create src/app/api/documents/[id]/send/route.ts** — POST handler (agent-authenticated): Logic: 1. Auth guard (same pattern as all portal API routes: `const session = await auth(); if (!session) return new Response('Unauthorized', { status: 401 })`) 2. Resolve params: `const { id } = await params` (Next.js 16 — params is a Promise) 3. Fetch document from DB — if not found return 404; if no preparedFilePath return 422 ("Document not yet prepared"); if status is 'Signed' return 409 ("Already signed") 4. Fetch client email from assignedClientId (fall back to doc.clientId for the email lookup) 5. Call `createSigningToken(doc.id)` — returns { token, jti, expiresAt } 6. Build signing URL: `${process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000'}/sign/${token}` 7. Call `sendSigningRequestEmail({ to: clientEmail, clientName, documentName: doc.name, signingUrl, expiresAt })` 8. Log audit event: `await logAuditEvent({ documentId: doc.id, eventType: 'email_sent' })` 9. Update document status to 'Sent', set sentAt = now() (only if not already Sent — use db.update with WHERE status != 'Signed') 10. Return 200 JSON: `{ ok: true, expiresAt: expiresAt.toISOString() }` Error handling: wrap entire body in try/catch; if sendMail throws, return 502 with error message (do NOT update DB status if email failed). ```typescript // src/app/api/documents/[id]/send/route.ts import { NextResponse } from 'next/server'; import { auth } from '@/lib/auth'; import { db } from '@/lib/db'; import { documents, clients } 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 }); // Resolve recipient: prefer assignedClientId, fall back to clientId 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 baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000'; 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, expiresAt: expiresAt.toISOString() }); } catch (err) { console.error('[send] error:', err); return NextResponse.json({ error: 'Failed to send signing email' }, { status: 502 }); } } ``` **Modify src/app/api/documents/[id]/prepare/route.ts** — add document_prepared audit log after the existing preparePdf() + db.update() success path. Find the line where status is updated to 'Sent' and add immediately after: ```typescript await logAuditEvent({ documentId: id, eventType: 'document_prepared' }); ``` Import logAuditEvent at the top of prepare/route.ts: ```typescript import { logAuditEvent } from '@/lib/signing/audit'; ``` IMPORTANT: The prepare route already marks status 'Sent'. In the new signing flow, PreparePanel will call prepare (fills text + places rects) and then a separate "Send" button calls POST /api/documents/[id]/send. The document_prepared event must be logged in prepare/route.ts where the PDF is actually prepared — not in send/route.ts. cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | grep -E "error|Error|✓" | tail -10 send/route.ts exists; prepare/route.ts imports and calls logAuditEvent; npm run build passes with no TypeScript errors; curl POST /api/documents/{id}/send on an unauthenticated request returns 401
- Build passes: `npm run build` exits 0 - send route file exists: `ls teressa-copeland-homes/src/app/api/documents/*/send/route.ts` - Email template: `ls teressa-copeland-homes/src/emails/SigningRequestEmail.tsx` - prepare/route.ts references logAuditEvent: `grep logAuditEvent teressa-copeland-homes/src/app/api/documents/*/prepare/route.ts` Email delivery layer complete when: SigningRequestEmail.tsx renders without error, signing-mailer.tsx exports both functions, send/route.ts exists and returns 401 unauthenticated, prepare/route.ts logs document_prepared, npm run build passes. After completion, create `.planning/phases/06-signing-flow/06-02-SUMMARY.md`