Files

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
multi-signer
atomic-completion
accumulate-pdf
signer-aware
requires provides affects
15-01
14-01
signer-aware-sign-route
atomic-completion-guard
accumulate-pdf
sign-token-route
signing-flow
added patterns
accumulate-pdf
update-where-is-null-returning
fire-and-forget-emails
created modified
teressa-copeland-homes/src/app/api/sign/[token]/route.ts
GET filter uses tokenRow.signerEmail — null = legacy token returns all isClientVisibleField fields (D-04, D-05)
POST accumulate: signedFilePath as working PDF — each signer reads latest, writes JTI-keyed partial (D-10)
Completion guard: UPDATE documents SET completionTriggeredAt=now WHERE completionTriggeredAt IS NULL RETURNING (D-07)
status='Signed' written only by completionTriggeredAt race winner — fixes first-signer-wins bug (D-08)
Signer completion emails sent only at completion from signers JSONB array (D-12, MSIGN-11)
Legacy null-signerEmail path unchanged: stamps all date fields, embeds all client-visible signable fields
duration_minutes completed_date tasks_completed files_modified
2 2026-04-03 2 1

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 where field.signerEmail === tokenRow.signerEmail are returned
  • If tokenRow.signerEmail === null (legacy): all isClientVisibleField fields 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)