docs(11.1): create phase plan — agent and client initials
Added INIT-01 through INIT-04 requirements to REQUIREMENTS.md. Updated ROADMAP.md with Phase 11.1 goal, success criteria, and plan list. Created three plan files mirroring Phase 11 structure: 11.1-01 (DB migration, API routes, AgentInitialsPanel, FieldPlacer token), 11.1-02 (preparePdf agentInitialsData param + prepare route guard), 11.1-03 (human E2E verification checkpoint). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
300
.planning/phases/11.1-agent-and-client-initials/11.1-02-PLAN.md
Normal file
300
.planning/phases/11.1-agent-and-client-initials/11.1-02-PLAN.md
Normal file
@@ -0,0 +1,300 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user