docs(11-agent-saved-signature-and-signing-workflow): create phase plan
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
---
|
||||
phase: 11-agent-saved-signature-and-signing-workflow
|
||||
plan: "02"
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- "11-01"
|
||||
files_modified:
|
||||
- teressa-copeland-homes/src/lib/pdf/prepare-document.ts
|
||||
- teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts
|
||||
autonomous: true
|
||||
requirements:
|
||||
- AGENT-04
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "When agent prepares a document with agent-signature fields and a saved signature, the prepared PDF contains the signature image at each agent-sig field coordinate"
|
||||
- "The agent signature is invisible to the client — it is never returned by GET /api/sign/[token]"
|
||||
- "When agent prepares a document with agent-signature fields but no saved signature, the prepare route returns 422 with { error: 'agent-signature-missing' } — no silent failure"
|
||||
- "When agent prepares a document with no agent-signature fields, the prepare route succeeds normally regardless of whether a signature is saved"
|
||||
artifacts:
|
||||
- path: "teressa-copeland-homes/src/lib/pdf/prepare-document.ts"
|
||||
provides: "preparePdf() with agentSignatureData param; embedPng + drawImage at agent-sig field coordinates"
|
||||
contains: "agentSignatureData"
|
||||
- path: "teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts"
|
||||
provides: "Fetches agentSignatureData from DB; 422 guard when agent-sig fields present but no sig saved; passes to preparePdf()"
|
||||
contains: "agent-signature-missing"
|
||||
key_links:
|
||||
- from: "teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts"
|
||||
to: "teressa-copeland-homes/src/lib/pdf/prepare-document.ts"
|
||||
via: "preparePdf(srcPath, destPath, textFields, sigFields, agentSignatureData)"
|
||||
pattern: "preparePdf.*agentSignatureData"
|
||||
- from: "teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts"
|
||||
to: "teressa-copeland-homes/src/lib/db/schema.ts"
|
||||
via: "db.query.users.findFirst({ columns: { agentSignatureData: true } })"
|
||||
pattern: "agentSignatureData.*findFirst"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Wire the agent signature into the prepare pipeline: `preparePdf()` gains an `agentSignatureData` parameter and embeds the PNG at each agent-signature field coordinate using the same `pdfDoc.embedPng()` + `page.drawImage()` pattern already used for client signatures. The prepare route fetches the agent's saved signature from the DB and passes it to `preparePdf()`, with a 422 guard if agent-signature fields are present but no signature has been saved.
|
||||
|
||||
Purpose: Fulfills AGENT-04 — agent's saved signature is baked into the prepared PDF before it reaches the client.
|
||||
Output: Prepared PDFs with embedded agent signatures; 422 error when signature is missing; client signing session unaffected (isClientVisibleField filter already excludes agent-signature).
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/11-agent-saved-signature-and-signing-workflow/11-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and confirmed patterns from codebase (2026-03-21 inspection). -->
|
||||
|
||||
From src/lib/pdf/prepare-document.ts (current function signature — BEFORE Phase 11):
|
||||
```typescript
|
||||
export async function preparePdf(
|
||||
srcPath: string,
|
||||
destPath: string,
|
||||
textFields: Record<string, string>,
|
||||
sigFields: SignatureFieldData[],
|
||||
// Phase 11 adds: agentSignatureData: string | null = null
|
||||
): Promise<void>
|
||||
```
|
||||
|
||||
Current agent-signature stub at line ~138 (to be replaced):
|
||||
```typescript
|
||||
} else if (fieldType === 'agent-signature') {
|
||||
// Skip — agent signature handled by Phase 11; no placeholder drawn here
|
||||
}
|
||||
```
|
||||
|
||||
From src/lib/signing/embed-signature.ts (confirmed working embedPng pattern):
|
||||
```typescript
|
||||
// embedPng accepts base64 DataURL directly (no Buffer conversion needed)
|
||||
const pngImage = await pdfDoc.embedPng(sig.dataURL); // 'data:image/png;base64,...'
|
||||
page.drawImage(pngImage, {
|
||||
x: sig.x,
|
||||
y: sig.y,
|
||||
width: sig.width,
|
||||
height: sig.height,
|
||||
});
|
||||
```
|
||||
|
||||
From src/app/api/documents/[id]/prepare/route.ts (current POST handler structure):
|
||||
```typescript
|
||||
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return new Response('Unauthorized', { status: 401 });
|
||||
// ... body parsing, doc lookup, path resolution ...
|
||||
await preparePdf(srcPath, destPath, textFields, sigFields); // 4 args currently
|
||||
// ... DB update, audit log, return ...
|
||||
}
|
||||
```
|
||||
|
||||
From src/lib/db/schema.ts (getFieldType — confirmed working pattern):
|
||||
```typescript
|
||||
export function getFieldType(field: SignatureFieldData): SignatureFieldType {
|
||||
return field.type ?? 'client-signature';
|
||||
}
|
||||
```
|
||||
|
||||
CRITICAL: embedPng is called ONCE per document (not once per field). The returned PDFImage
|
||||
object is reused across multiple drawImage calls. Call pdfDoc.embedPng(agentSignatureData)
|
||||
before the field loop, store as agentSigImage, then reference agentSigImage inside the loop.
|
||||
|
||||
COORDINATE SYSTEM: Use field.x, field.y, field.width, field.height directly. These are already
|
||||
stored in PDF user-space (bottom-left origin) by FieldPlacer's screenToPdfCoords() conversion.
|
||||
Do NOT invert or adjust Y — this is identical to how embedSignatureInPdf() works for client sigs.
|
||||
|
||||
IMPORT NOTE: PDFImage type from @cantoo/pdf-lib is needed for the agentSigImage variable type.
|
||||
Check prepare-document.ts existing imports — PDFDocument, rgb, StandardFonts, etc. are already
|
||||
imported from @cantoo/pdf-lib. Add PDFImage to that import if not already present.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: preparePdf() — add agentSignatureData param and embed at agent-sig field coordinates</name>
|
||||
<files>
|
||||
teressa-copeland-homes/src/lib/pdf/prepare-document.ts
|
||||
</files>
|
||||
<action>
|
||||
Two targeted changes to `preparePdf()`:
|
||||
|
||||
**1. Add `agentSignatureData` parameter** with a default of `null` so existing call sites compile without change until the prepare route is updated in Task 2:
|
||||
```typescript
|
||||
export async function preparePdf(
|
||||
srcPath: string,
|
||||
destPath: string,
|
||||
textFields: Record<string, string>,
|
||||
sigFields: SignatureFieldData[],
|
||||
agentSignatureData: string | null = null, // ADD THIS
|
||||
): Promise<void>
|
||||
```
|
||||
|
||||
**2. Embed the agent signature PNG once before the field loop:**
|
||||
After `const pages = pdfDoc.getPages();` and before the field loop, add:
|
||||
```typescript
|
||||
// Embed agent signature image once — reused across all agent-sig fields
|
||||
let agentSigImage: import('@cantoo/pdf-lib').PDFImage | null = null;
|
||||
if (agentSignatureData) {
|
||||
agentSigImage = await pdfDoc.embedPng(agentSignatureData);
|
||||
}
|
||||
```
|
||||
|
||||
If PDFImage is already imported from @cantoo/pdf-lib at the top of the file (check the existing imports), use the named import directly instead of the inline import type. Only add `PDFImage` to the destructured import if it is not already there — do not change any other imports.
|
||||
|
||||
**3. Replace the agent-signature stub** in the field loop with real embedding:
|
||||
Find the existing `else if (fieldType === 'agent-signature') { // Skip — ... }` block and replace it with:
|
||||
```typescript
|
||||
} else if (fieldType === 'agent-signature') {
|
||||
if (agentSigImage) {
|
||||
page.drawImage(agentSigImage, {
|
||||
x: field.x,
|
||||
y: field.y,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
});
|
||||
}
|
||||
// If no signature saved: the prepare route guards against this with 422 before calling preparePdf
|
||||
}
|
||||
```
|
||||
|
||||
No other changes to prepare-document.ts. The function body, other field type branches, and the atomic write logic remain untouched.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
<done>TypeScript compiles clean; preparePdf() function signature now has 5 parameters (last is optional); agent-signature stub replaced with drawImage call; no regressions in other field type branches.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: prepare route — fetch agentSignatureData, 422 guard, pass to preparePdf</name>
|
||||
<files>
|
||||
teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts
|
||||
</files>
|
||||
<action>
|
||||
Three additions to the POST handler in `src/app/api/documents/[id]/prepare/route.ts`:
|
||||
|
||||
**1. Import `users` from schema** (if not already imported — check existing imports at top of file):
|
||||
```typescript
|
||||
import { users } from '@/lib/db/schema';
|
||||
```
|
||||
Also ensure `getFieldType` is imported from `@/lib/db/schema` (needed for the guard check). If already present, do not duplicate.
|
||||
|
||||
**2. Fetch agentSignatureData from DB** — add this block AFTER the existing doc lookup and sigFields parsing, BEFORE the `preparePdf()` call:
|
||||
```typescript
|
||||
// Fetch agent's saved signature for embedding at agent-signature field coordinates
|
||||
const agentUser = await db.query.users.findFirst({
|
||||
where: eq(users.id, session.user.id),
|
||||
columns: { agentSignatureData: true },
|
||||
});
|
||||
const agentSignatureData = agentUser?.agentSignatureData ?? null;
|
||||
|
||||
// Guard: if document has agent-signature fields but no signature saved, block prepare
|
||||
const hasAgentSigFields = sigFields.some(f => getFieldType(f) === 'agent-signature');
|
||||
if (hasAgentSigFields && !agentSignatureData) {
|
||||
return Response.json(
|
||||
{ error: 'agent-signature-missing', message: 'No agent signature saved. Go to Profile to save your signature first.' },
|
||||
{ status: 422 }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**3. Pass `agentSignatureData` to `preparePdf()`** — update the existing call:
|
||||
```typescript
|
||||
// BEFORE (4 args):
|
||||
await preparePdf(srcPath, destPath, textFields, sigFields);
|
||||
|
||||
// AFTER (5 args):
|
||||
await preparePdf(srcPath, destPath, textFields, sigFields, agentSignatureData);
|
||||
```
|
||||
|
||||
Do not change any other logic in the route handler — the DB update, audit logging, and response all remain unchanged.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
<done>TypeScript compiles clean; prepare route fetches agentSignatureData; guard returns 422 with { error: 'agent-signature-missing' } when agent-sig fields exist but no signature saved; preparePdf called with 5 args including agentSignatureData.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
After both tasks complete, verify the full Plan 02 state:
|
||||
|
||||
1. `npx tsc --noEmit` passes with zero errors
|
||||
2. `npm run build` succeeds (or `npm run dev` starts clean)
|
||||
3. Functional test — draw save place prepare round-trip:
|
||||
a. Visit /portal/profile, draw and save a signature
|
||||
b. Open a document in the portal, place an "Agent Signature" field token on any page
|
||||
c. Fill text fields and click Prepare
|
||||
d. Prepare succeeds (200 response)
|
||||
e. Download the prepared PDF and verify the agent signature PNG is embedded at the correct position
|
||||
4. Guard test — prepare with no saved signature:
|
||||
a. Use a fresh test account (or temporarily clear agentSignatureData in DB)
|
||||
b. Place an agent-signature field, attempt to prepare
|
||||
c. Prepare route returns 422 with `{ error: 'agent-signature-missing' }`
|
||||
5. Client signing page test — open the signing link for the prepared document:
|
||||
a. Signing page does NOT show an agent-signature field overlay (isClientVisibleField already filters it)
|
||||
b. Client can sign normally — only client-signature and initials overlays appear
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- AGENT-04: Agent's saved PNG is embedded at each agent-signature field coordinate in the prepared PDF
|
||||
- No agent-signature content is exposed to the client via GET /api/sign/[token]
|
||||
- Prepare fails with actionable 422 when agent-sig fields exist but no signature saved
|
||||
- TypeScript build clean; zero new npm packages; no regressions on other field types
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/11-agent-saved-signature-and-signing-workflow/11-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user