301 lines
14 KiB
Markdown
301 lines
14 KiB
Markdown
|
|
---
|
||
|
|
phase: 11.1-agent-and-client-initials
|
||
|
|
plan: "02"
|
||
|
|
type: execute
|
||
|
|
wave: 2
|
||
|
|
depends_on:
|
||
|
|
- "11.1-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:
|
||
|
|
- INIT-03
|
||
|
|
- INIT-04
|
||
|
|
|
||
|
|
must_haves:
|
||
|
|
truths:
|
||
|
|
- "When agent prepares a document with agent-initials fields and saved initials, the prepared PDF contains the initials image at each agent-initials field coordinate"
|
||
|
|
- "The agent initials are invisible to the client — GET /api/sign/[token] never returns agent-initials field coordinates (isClientVisibleField already guards this from Plan 01)"
|
||
|
|
- "When agent prepares a document with agent-initials fields but no saved initials, the prepare route returns 422 with { error: 'agent-initials-missing' } — no silent failure"
|
||
|
|
- "When agent prepares a document with no agent-initials fields, the prepare route succeeds normally regardless of whether initials are saved"
|
||
|
|
- "The existing 'initials' (client-initials) branch in preparePdf() is completely untouched — purple placeholder still drawn at prepare time, client still prompted during signing"
|
||
|
|
- "Agent-signature embedding (from Phase 11) continues to work unchanged alongside agent-initials"
|
||
|
|
artifacts:
|
||
|
|
- path: "teressa-copeland-homes/src/lib/pdf/prepare-document.ts"
|
||
|
|
provides: "preparePdf() with agentInitialsData param (6th, optional); embedPng + drawImage at agent-initials field coordinates; existing agent-signature and initials branches untouched"
|
||
|
|
contains: "agentInitialsData"
|
||
|
|
- path: "teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts"
|
||
|
|
provides: "Fetches both agentSignatureData and agentInitialsData in a single DB query; 422 guard for missing initials when agent-initials fields present; passes agentInitialsData as 6th arg to preparePdf()"
|
||
|
|
contains: "agent-initials-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, agentInitialsData)"
|
||
|
|
pattern: "preparePdf.*agentInitialsData"
|
||
|
|
- 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, agentInitialsData: true } })"
|
||
|
|
pattern: "agentInitialsData.*findFirst"
|
||
|
|
---
|
||
|
|
|
||
|
|
<objective>
|
||
|
|
Wire the agent initials into the prepare pipeline: `preparePdf()` gains an optional `agentInitialsData` parameter (6th, default null) and embeds the PNG at each agent-initials field coordinate using the same embed-once-draw-many pattern already used for agent-signature. The prepare route updates its single DB query to fetch `agentInitialsData` alongside `agentSignatureData`, adds a 422 guard for missing initials, and passes `agentInitialsData` as the 6th argument to `preparePdf()`.
|
||
|
|
|
||
|
|
Purpose: Fulfills INIT-03 — agent's saved initials are baked into the prepared PDF before it reaches the client. Confirms INIT-04 — the existing `'initials'` (client-initials) branch is deliberately left untouched; client-initials already work end-to-end.
|
||
|
|
Output: Prepared PDFs with embedded agent initials; 422 error when initials are missing; client signing session unaffected; zero regressions.
|
||
|
|
</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.1-agent-and-client-initials/11.1-01-SUMMARY.md
|
||
|
|
|
||
|
|
<interfaces>
|
||
|
|
<!-- Key types and confirmed patterns from codebase (2026-03-21 inspection + Phase 11 Plan 02 established pattern). -->
|
||
|
|
|
||
|
|
From src/lib/pdf/prepare-document.ts (current function signature — Phase 11 complete, agentInitialsData NOT YET present):
|
||
|
|
```typescript
|
||
|
|
export async function preparePdf(
|
||
|
|
srcPath: string,
|
||
|
|
destPath: string,
|
||
|
|
textFields: Record<string, string>,
|
||
|
|
sigFields: SignatureFieldData[],
|
||
|
|
agentSignatureData: string | null = null, // Added Phase 11
|
||
|
|
// Phase 11.1 adds: agentInitialsData: string | null = null,
|
||
|
|
): Promise<void>
|
||
|
|
```
|
||
|
|
|
||
|
|
Current 'initials' branch in preparePdf() — DO NOT TOUCH (this is client-initials):
|
||
|
|
```typescript
|
||
|
|
} else if (fieldType === 'initials') {
|
||
|
|
// Purple "Initials" placeholder — transparent background, border + label only
|
||
|
|
page.drawRectangle({
|
||
|
|
x: field.x, y: field.y, width: field.width, height: field.height,
|
||
|
|
borderColor: rgb(0.49, 0.23, 0.93), borderWidth: 1.5,
|
||
|
|
});
|
||
|
|
page.drawText('Initials', {
|
||
|
|
x: field.x + 4, y: field.y + 4, size: 8, font: helvetica,
|
||
|
|
color: rgb(0.49, 0.23, 0.93),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Current agent-signature branch in preparePdf() (Phase 11 — working — do not change):
|
||
|
|
```typescript
|
||
|
|
} else if (fieldType === 'agent-signature') {
|
||
|
|
if (agentSigImage) {
|
||
|
|
page.drawImage(agentSigImage, {
|
||
|
|
x: field.x,
|
||
|
|
y: field.y,
|
||
|
|
width: field.width,
|
||
|
|
height: field.height,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Pattern for embed-once-draw-many (Phase 11 confirmed working):
|
||
|
|
```typescript
|
||
|
|
// BEFORE the field loop — embed once:
|
||
|
|
let agentSigImage: PDFImage | null = null;
|
||
|
|
if (agentSignatureData) {
|
||
|
|
agentSigImage = await pdfDoc.embedPng(agentSignatureData);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Phase 11.1 parallel pattern — add after agentSigImage block:
|
||
|
|
let agentInitialsImage: PDFImage | null = null;
|
||
|
|
if (agentInitialsData) {
|
||
|
|
agentInitialsImage = await pdfDoc.embedPng(agentInitialsData);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
CRITICAL: Do NOT call embedPng() inside the field loop. The Phase 11 pattern calls it once
|
||
|
|
before the loop and reuses the PDFImage reference. Phase 11.1 follows the same pattern.
|
||
|
|
|
||
|
|
From src/app/api/documents/[id]/prepare/route.ts (Phase 11 state — current):
|
||
|
|
```typescript
|
||
|
|
// Current: fetches only agentSignatureData
|
||
|
|
const agentUser = await db.query.users.findFirst({
|
||
|
|
where: eq(users.id, session.user.id),
|
||
|
|
columns: { agentSignatureData: true }, // Phase 11.1: ADD agentInitialsData: true
|
||
|
|
});
|
||
|
|
const agentSignatureData = agentUser?.agentSignatureData ?? null;
|
||
|
|
|
||
|
|
// Phase 11 guard (existing — leave unchanged):
|
||
|
|
const hasAgentSigFields = sigFields.some(f => getFieldType(f) === 'agent-signature');
|
||
|
|
if (hasAgentSigFields && !agentSignatureData) {
|
||
|
|
return Response.json(
|
||
|
|
{ error: 'agent-signature-missing', message: '...' },
|
||
|
|
{ status: 422 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Current call (5 args):
|
||
|
|
await preparePdf(srcPath, destPath, textFields, sigFields, agentSignatureData);
|
||
|
|
```
|
||
|
|
|
||
|
|
PDFImage type note: PDFImage is already imported (or available as a type import) from
|
||
|
|
@cantoo/pdf-lib in prepare-document.ts since Phase 11. Use the same import pattern.
|
||
|
|
Do not add a new import if PDFImage is already destructured from the existing import.
|
||
|
|
</interfaces>
|
||
|
|
</context>
|
||
|
|
|
||
|
|
<tasks>
|
||
|
|
|
||
|
|
<task type="auto">
|
||
|
|
<name>Task 1: preparePdf() — add agentInitialsData param and embed at agent-initials field coordinates</name>
|
||
|
|
<files>
|
||
|
|
teressa-copeland-homes/src/lib/pdf/prepare-document.ts
|
||
|
|
</files>
|
||
|
|
<action>
|
||
|
|
Three targeted changes to `preparePdf()` — no other logic changes:
|
||
|
|
|
||
|
|
**1. Add `agentInitialsData` parameter** as the 6th parameter with a default of `null` (so the existing 5-arg call site in the prepare route still compiles until Task 2 updates it):
|
||
|
|
```typescript
|
||
|
|
export async function preparePdf(
|
||
|
|
srcPath: string,
|
||
|
|
destPath: string,
|
||
|
|
textFields: Record<string, string>,
|
||
|
|
sigFields: SignatureFieldData[],
|
||
|
|
agentSignatureData: string | null = null,
|
||
|
|
agentInitialsData: string | null = null, // ADD THIS
|
||
|
|
): Promise<void>
|
||
|
|
```
|
||
|
|
|
||
|
|
**2. Embed agent initials image once** — add this block immediately AFTER the existing `agentSigImage` embed block (before the field loop):
|
||
|
|
```typescript
|
||
|
|
// Embed agent initials image once — reused across all agent-initials fields
|
||
|
|
let agentInitialsImage: PDFImage | null = null;
|
||
|
|
if (agentInitialsData) {
|
||
|
|
agentInitialsImage = await pdfDoc.embedPng(agentInitialsData);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
If `PDFImage` is already in the existing `@cantoo/pdf-lib` import destructure, use it directly. If not, add `PDFImage` to the existing import — do not create a new import statement.
|
||
|
|
|
||
|
|
**3. Add `'agent-initials'` branch** in the field loop — add it as a new `else if` block after the existing `'agent-signature'` branch:
|
||
|
|
```typescript
|
||
|
|
} else if (fieldType === 'agent-initials') {
|
||
|
|
if (agentInitialsImage) {
|
||
|
|
page.drawImage(agentInitialsImage, {
|
||
|
|
x: field.x,
|
||
|
|
y: field.y,
|
||
|
|
width: field.width,
|
||
|
|
height: field.height,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
// If no initials saved: the prepare route guards against this with 422 before calling preparePdf
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Do NOT modify:
|
||
|
|
- The `'initials'` branch (client-initials purple placeholder) — leave completely untouched
|
||
|
|
- The `'agent-signature'` branch — leave completely untouched
|
||
|
|
- Any other field type branches or function logic
|
||
|
|
</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 6 parameters (last two optional); agent-initials branch draws the image; existing 'initials' (client-initials) and 'agent-signature' branches are untouched; no regressions on other field type branches.</done>
|
||
|
|
</task>
|
||
|
|
|
||
|
|
<task type="auto">
|
||
|
|
<name>Task 2: prepare route — fetch agentInitialsData in same query, 422 guard, pass to preparePdf</name>
|
||
|
|
<files>
|
||
|
|
teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts
|
||
|
|
</files>
|
||
|
|
<action>
|
||
|
|
Three targeted additions to the POST handler — do not change any other logic:
|
||
|
|
|
||
|
|
**1. Update the existing DB query** to fetch `agentInitialsData` alongside `agentSignatureData` in the same `findFirst()` call (one DB round-trip, not two):
|
||
|
|
```typescript
|
||
|
|
// BEFORE:
|
||
|
|
const agentUser = await db.query.users.findFirst({
|
||
|
|
where: eq(users.id, session.user.id),
|
||
|
|
columns: { agentSignatureData: true },
|
||
|
|
});
|
||
|
|
const agentSignatureData = agentUser?.agentSignatureData ?? null;
|
||
|
|
|
||
|
|
// AFTER:
|
||
|
|
const agentUser = await db.query.users.findFirst({
|
||
|
|
where: eq(users.id, session.user.id),
|
||
|
|
columns: { agentSignatureData: true, agentInitialsData: true }, // ADD agentInitialsData
|
||
|
|
});
|
||
|
|
const agentSignatureData = agentUser?.agentSignatureData ?? null;
|
||
|
|
const agentInitialsData = agentUser?.agentInitialsData ?? null; // ADD
|
||
|
|
```
|
||
|
|
|
||
|
|
**2. Add the agent-initials 422 guard** — add this block immediately AFTER the existing `hasAgentSigFields` guard (NOT before it):
|
||
|
|
```typescript
|
||
|
|
// Guard: agent-initials fields present but no initials saved
|
||
|
|
const hasAgentInitialsFields = sigFields.some(f => getFieldType(f) === 'agent-initials');
|
||
|
|
if (hasAgentInitialsFields && !agentInitialsData) {
|
||
|
|
return Response.json(
|
||
|
|
{ error: 'agent-initials-missing', message: 'No agent initials saved. Go to Profile to save your initials first.' },
|
||
|
|
{ status: 422 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**3. Update the preparePdf() call** to pass `agentInitialsData` as the 6th argument:
|
||
|
|
```typescript
|
||
|
|
// BEFORE (5 args):
|
||
|
|
await preparePdf(srcPath, destPath, textFields, sigFields, agentSignatureData);
|
||
|
|
|
||
|
|
// AFTER (6 args):
|
||
|
|
await preparePdf(srcPath, destPath, textFields, sigFields, agentSignatureData, agentInitialsData);
|
||
|
|
```
|
||
|
|
|
||
|
|
Do not change any other logic in the route handler — 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 both agentSignatureData and agentInitialsData in a single query; guard returns 422 with { error: 'agent-initials-missing' } when agent-initials fields exist but no initials saved; preparePdf called with 6 args including agentInitialsData.</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 dev` starts without errors (or `npm run build` succeeds)
|
||
|
|
3. Agent-initials round-trip test:
|
||
|
|
a. Visit /portal/profile, draw and save initials
|
||
|
|
b. Open a document in the portal, place an "Agent Initials" field (orange 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 initials PNG is embedded at the correct position
|
||
|
|
4. Agent-initials guard test — prepare with no saved initials:
|
||
|
|
a. Place an agent-initials field on a document for an account with no initials saved
|
||
|
|
b. Prepare route returns 422 with `{ error: 'agent-initials-missing' }`
|
||
|
|
5. Agent-signature regression test — confirm Phase 11 behavior unchanged:
|
||
|
|
a. Place an agent-signature field, ensure a signature is saved, prepare the document
|
||
|
|
b. Signature still embeds correctly; no regression
|
||
|
|
6. Client-initials regression test — confirm existing 'initials' behavior unchanged:
|
||
|
|
a. Place a purple "Initials" field, prepare the document
|
||
|
|
b. Prepared PDF shows purple "Initials" placeholder at the field location
|
||
|
|
c. Open the signing link — client still sees the initials overlay and can initial the field
|
||
|
|
7. Client signing page isolation test:
|
||
|
|
a. Open the signing link for a document with agent-initials fields
|
||
|
|
b. Signing page does NOT show an overlay at agent-initials coordinates (isClientVisibleField already returns false from Plan 01)
|
||
|
|
</verification>
|
||
|
|
|
||
|
|
<success_criteria>
|
||
|
|
- INIT-03: Agent's saved initials PNG is embedded at each agent-initials field coordinate in the prepared PDF; field is never exposed to the client
|
||
|
|
- INIT-04: Existing 'initials' type (client-initials) behavior confirmed unchanged — client is still prompted to initial during signing session
|
||
|
|
- Prepare fails with actionable 422 when agent-initials fields exist but no initials saved
|
||
|
|
- Agent-signature embedding (Phase 11) still works — no regression
|
||
|
|
- TypeScript build clean; zero new npm packages; no regressions on any other field type
|
||
|
|
</success_criteria>
|
||
|
|
|
||
|
|
<output>
|
||
|
|
After completion, create `.planning/phases/11.1-agent-and-client-initials/11.1-02-SUMMARY.md`
|
||
|
|
</output>
|