259 lines
12 KiB
Markdown
259 lines
12 KiB
Markdown
---
|
|
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>
|