feat(15-02): rewrite send route with multi-signer token loop and legacy fallback

- Loop over doc.signers when populated: one createSigningToken per signer with signerEmail
- Dispatch all signing emails in parallel via Promise.all
- Preserve legacy single-signer path unchanged when signers is null/empty
- Replace NEXT_PUBLIC_BASE_URL with APP_BASE_URL for signing URLs
- Add audit event with metadata.signerEmail for each signer in multi-signer path
- Import DocumentSigner from schema for type casting
This commit is contained in:
Chandler Copeland
2026-04-03 15:46:44 -06:00
parent 0f97c4233f
commit 7a04a4f617

View File

@@ -1,7 +1,7 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { db } from '@/lib/db'; import { db } from '@/lib/db';
import { documents, clients } from '@/lib/db/schema'; import { documents, clients, DocumentSigner } from '@/lib/db/schema';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { createSigningToken } from '@/lib/signing/token'; import { createSigningToken } from '@/lib/signing/token';
import { sendSigningRequestEmail } from '@/lib/signing/signing-mailer'; import { sendSigningRequestEmail } from '@/lib/signing/signing-mailer';
@@ -26,24 +26,45 @@ export async function POST(
if (doc.status === 'Signed') if (doc.status === 'Signed')
return NextResponse.json({ error: 'Already signed' }, { status: 409 }); return NextResponse.json({ error: 'Already signed' }, { status: 409 });
// Resolve recipient: prefer assignedClientId, fall back to clientId const baseUrl = process.env.APP_BASE_URL ?? 'http://localhost:3000';
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); if (doc.signers && (doc.signers as DocumentSigner[]).length > 0) {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000'; // ── Multi-signer path (D-02) ──
const signingUrl = `${baseUrl}/sign/${token}`; 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 });
await sendSigningRequestEmail({ const { token, expiresAt } = await createSigningToken(doc.id);
to: client.email, const signingUrl = `${baseUrl}/sign/${token}`;
clientName: client.name, await sendSigningRequestEmail({
documentName: doc.name, to: client.email,
signingUrl, clientName: client.name,
expiresAt, documentName: doc.name,
}); signingUrl,
expiresAt,
await logAuditEvent({ documentId: doc.id, eventType: 'email_sent' }); });
await logAuditEvent({ documentId: doc.id, eventType: 'email_sent' });
}
// Update status to Sent (skip if already Sent or Signed to avoid downgrade) // Update status to Sent (skip if already Sent or Signed to avoid downgrade)
if (doc.status === 'Draft') { if (doc.status === 'Draft') {
@@ -53,7 +74,7 @@ export async function POST(
.where(eq(documents.id, id)); .where(eq(documents.id, id));
} }
return NextResponse.json({ ok: true, expiresAt: expiresAt.toISOString() }); return NextResponse.json({ ok: true });
} catch (err) { } catch (err) {
console.error('[send] error:', err); console.error('[send] error:', err);
return NextResponse.json({ error: 'Failed to send signing email' }, { status: 502 }); return NextResponse.json({ error: 'Failed to send signing email' }, { status: 502 });