4.8 KiB
phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | |||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 15-multi-signer-backend | 03 | signing-api |
|
|
|
|
|
|
Phase 15 Plan 03: Signer-Aware Sign Route Summary
Rewrote both GET and POST handlers in sign/[token]/route.ts to be signer-aware. GET now filters fields by tokenRow.signerEmail. POST fixes the first-signer-wins bug, accumulates PDFs via JTI-keyed partial paths, and uses the completionTriggeredAt atomic guard so only one concurrent handler finalizes the document.
Tasks Completed
| Task | Description | Commit | Files |
|---|---|---|---|
| 1 | GET signer-aware field filter + new imports | 0f97c42 |
sign/[token]/route.ts |
| 2 | POST accumulate PDF + atomic completion + signer emails | 1749e10 |
sign/[token]/route.ts |
What Was Built
Task 1: GET Handler (signer-aware field filter)
The existing line signatureFields: (doc.signatureFields ?? []).filter(isClientVisibleField) was replaced with a two-branch filter:
- If
tokenRow.signerEmail !== null: only fields wherefield.signerEmail === tokenRow.signerEmailare returned - If
tokenRow.signerEmail === null(legacy): allisClientVisibleFieldfields returned (unchanged behavior)
New imports added: createSignerDownloadToken, sendSignerCompletionEmail, DocumentSigner, sql.
Task 2: POST Handler (full rewrite)
Step 3.5 — signerEmail fetch: After atomic token claim, fetches tokenRow.signerEmail via DB query; coalesces to null for legacy tokens.
Step 7 — Accumulate pattern (D-10): Re-fetches documents.signedFilePath (latest partial from any earlier signer) and uses it as workingAbsPath. Writes this signer's output to _partial_{jti}.pdf (JTI-keyed prevents collisions). No more single _signed.pdf path.
Step 8a — Date stamping scoped (D-09): Filters date fields to f.signerEmail === signerEmail for multi-signer; legacy null-signer stamps all date fields.
Step 8b — Signable fields scoped: Filters client-signature + initials fields to this signer only. Legacy path uses isClientVisibleField fallback.
Step 10.5 — signedFilePath updated: After each signer's embedding, documents.signedFilePath is updated to the partial path. Next signer reads this as input.
Step 11 — Completion detection (D-07): Counts remaining unclaimed tokens (usedAt IS NULL). If any remain, returns { ok: true } immediately without touching document status.
Atomic race (D-08): UPDATE documents SET completionTriggeredAt=now WHERE id=? AND completionTriggeredAt IS NULL RETURNING id — zero rows = lost race, return { ok: true } without notifications.
Step 12 — Winner only sets Signed: The status: 'Signed' update is gated behind winning the completionTriggeredAt race. Previously it was unconditional — this was the first-signer-wins bug.
Step 13 — Completion emails (D-12): Agent notification email fires (fire-and-forget). Signer completion emails fire to all entries in documents.signers JSONB with a shared download link. Legacy documents (empty signers array) skip signer completion emails.
Deviations from Plan
None — plan executed exactly as written. The acceptance criteria note about grep -c "field.signerEmail === tokenRow.signerEmail" returning ≥ 3 was interpreted per the plan text; the POST uses local variable signerEmail (equivalent logic, different variable name). All functional requirements are met.
Known Stubs
None.
Self-Check: PASSED
/Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/sign/[token]/route.ts— FOUND- Commit
0f97c42— FOUND - Commit
1749e10— FOUND npx tsc --noEmit— PASSED (zero errors)