docs(15-03): complete signer-aware sign route plan — atomic completion, accumulate PDF
This commit is contained in:
@@ -2,15 +2,15 @@
|
|||||||
gsd_state_version: 1.0
|
gsd_state_version: 1.0
|
||||||
milestone: v1.1
|
milestone: v1.1
|
||||||
milestone_name: Smart Document Preparation
|
milestone_name: Smart Document Preparation
|
||||||
status: executing
|
status: verifying
|
||||||
stopped_at: Completed 15-02-PLAN.md — Multi-signer send route with token loop and legacy fallback
|
stopped_at: Completed 15-03-PLAN.md — signer-aware GET/POST sign route, atomic completion, accumulate PDF
|
||||||
last_updated: "2026-04-03T21:47:51.565Z"
|
last_updated: "2026-04-03T21:48:31.695Z"
|
||||||
last_activity: 2026-04-03
|
last_activity: 2026-04-03
|
||||||
progress:
|
progress:
|
||||||
total_phases: 17
|
total_phases: 17
|
||||||
completed_phases: 15
|
completed_phases: 16
|
||||||
total_plans: 52
|
total_plans: 52
|
||||||
completed_plans: 50
|
completed_plans: 51
|
||||||
percent: 100
|
percent: 100
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ See: .planning/PROJECT.md (updated 2026-04-03)
|
|||||||
|
|
||||||
Phase: 15 (multi-signer-backend) — EXECUTING
|
Phase: 15 (multi-signer-backend) — EXECUTING
|
||||||
Plan: 3 of 3
|
Plan: 3 of 3
|
||||||
Status: Ready to execute
|
Status: Phase complete — ready for verification
|
||||||
Last activity: 2026-04-03
|
Last activity: 2026-04-03
|
||||||
|
|
||||||
## Note on v1.1
|
## Note on v1.1
|
||||||
@@ -88,6 +88,7 @@ Progress: [█████████████] 100% (13/13 phases complete
|
|||||||
| Phase 14-multi-signer-schema P01 | 5 | 2 tasks | 3 files |
|
| Phase 14-multi-signer-schema P01 | 5 | 2 tasks | 3 files |
|
||||||
| Phase 15 P01 | 2 | 3 tasks | 3 files |
|
| Phase 15 P01 | 2 | 3 tasks | 3 files |
|
||||||
| Phase 15-multi-signer-backend P02 | 5 | 1 tasks | 1 files |
|
| Phase 15-multi-signer-backend P02 | 5 | 1 tasks | 1 files |
|
||||||
|
| Phase 15-multi-signer-backend P03 | 2 | 2 tasks | 1 files |
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
|
|
||||||
@@ -162,6 +163,9 @@ Recent decisions affecting v1.1 work:
|
|||||||
- [Phase 15]: signerEmail stored in DB only (not JWT payload) — keeps token minimal, consistent with D-03
|
- [Phase 15]: signerEmail stored in DB only (not JWT payload) — keeps token minimal, consistent with D-03
|
||||||
- [Phase 15-multi-signer-backend]: Promise.all fail-fast for multi-signer send: one email failure rolls back entire send, agent retries — consistent with legacy single-signer behavior
|
- [Phase 15-multi-signer-backend]: Promise.all fail-fast for multi-signer send: one email failure rolls back entire send, agent retries — consistent with legacy single-signer behavior
|
||||||
- [Phase 15-multi-signer-backend]: APP_BASE_URL replaces NEXT_PUBLIC_BASE_URL for signing URLs — server-side env var correct for API routes
|
- [Phase 15-multi-signer-backend]: APP_BASE_URL replaces NEXT_PUBLIC_BASE_URL for signing URLs — server-side env var correct for API routes
|
||||||
|
- [Phase 15-03]: GET filter uses tokenRow.signerEmail — null = legacy returns all isClientVisibleField fields (D-04, D-05)
|
||||||
|
- [Phase 15-03]: POST accumulate: signedFilePath as working PDF — each signer reads latest, writes JTI-keyed partial (D-10)
|
||||||
|
- [Phase 15-03]: completionTriggeredAt atomic guard ensures only one concurrent handler sets status=Signed and sends completion emails (D-07, D-08)
|
||||||
|
|
||||||
### v1.2 Pre-decisions (from research)
|
### v1.2 Pre-decisions (from research)
|
||||||
|
|
||||||
@@ -185,6 +189,6 @@ None yet.
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-04-03T21:47:51.561Z
|
Last session: 2026-04-03T21:48:31.691Z
|
||||||
Stopped at: Completed 15-02-PLAN.md — Multi-signer send route with token loop and legacy fallback
|
Stopped at: Completed 15-03-PLAN.md — signer-aware GET/POST sign route, atomic completion, accumulate PDF
|
||||||
Resume file: None
|
Resume file: None
|
||||||
|
|||||||
86
.planning/phases/15-multi-signer-backend/15-03-SUMMARY.md
Normal file
86
.planning/phases/15-multi-signer-backend/15-03-SUMMARY.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
phase: 15-multi-signer-backend
|
||||||
|
plan: "03"
|
||||||
|
subsystem: signing-api
|
||||||
|
tags: [multi-signer, atomic-completion, accumulate-pdf, signer-aware]
|
||||||
|
dependency_graph:
|
||||||
|
requires: [15-01, 14-01]
|
||||||
|
provides: [signer-aware-sign-route, atomic-completion-guard, accumulate-pdf]
|
||||||
|
affects: [sign-token-route, signing-flow]
|
||||||
|
tech_stack:
|
||||||
|
added: []
|
||||||
|
patterns: [accumulate-pdf, update-where-is-null-returning, fire-and-forget-emails]
|
||||||
|
key_files:
|
||||||
|
created: []
|
||||||
|
modified:
|
||||||
|
- teressa-copeland-homes/src/app/api/sign/[token]/route.ts
|
||||||
|
decisions:
|
||||||
|
- "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"
|
||||||
|
metrics:
|
||||||
|
duration_minutes: 2
|
||||||
|
completed_date: "2026-04-03"
|
||||||
|
tasks_completed: 2
|
||||||
|
files_modified: 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)
|
||||||
Reference in New Issue
Block a user