fix(05): revise plan 05-01 based on checker feedback
This commit is contained in:
@@ -7,6 +7,7 @@ depends_on: []
|
|||||||
files_modified:
|
files_modified:
|
||||||
- teressa-copeland-homes/src/lib/db/schema.ts
|
- teressa-copeland-homes/src/lib/db/schema.ts
|
||||||
- teressa-copeland-homes/src/lib/pdf/prepare-document.ts
|
- teressa-copeland-homes/src/lib/pdf/prepare-document.ts
|
||||||
|
- teressa-copeland-homes/src/lib/pdf/__tests__/prepare-document.test.ts
|
||||||
- teressa-copeland-homes/src/app/api/documents/[id]/fields/route.ts
|
- teressa-copeland-homes/src/app/api/documents/[id]/fields/route.ts
|
||||||
- teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts
|
- teressa-copeland-homes/src/app/api/documents/[id]/prepare/route.ts
|
||||||
autonomous: true
|
autonomous: true
|
||||||
@@ -14,11 +15,11 @@ requirements: [DOC-04, DOC-05, DOC-06]
|
|||||||
|
|
||||||
must_haves:
|
must_haves:
|
||||||
truths:
|
truths:
|
||||||
- "Schema has signatureFields (JSONB), textFillData (JSONB), assignedClientId (text), and preparedFilePath (text) columns on documents table"
|
- "GET /api/documents/[id]/fields returns [] for a document with no placed fields"
|
||||||
- "Migration 0003 is applied — columns exist in the local PostgreSQL database"
|
- "PUT /api/documents/[id]/fields with a SignatureFieldData[] body returns the stored array on the next GET"
|
||||||
- "GET /api/documents/[id]/fields returns stored signature fields as JSON array"
|
- "POST /api/documents/[id]/prepare transitions the document status to Sent and creates a file at uploads/clients/{clientId}/{docId}_prepared.pdf"
|
||||||
- "PUT /api/documents/[id]/fields stores a SignatureFieldData[] array to the signatureFields column"
|
- "POST /api/documents/[id]/prepare returns 422 if the document has no PDF file attached"
|
||||||
- "POST /api/documents/[id]/prepare fills AcroForm fields (or draws text), burns signature rectangles, writes prepared PDF to uploads/clients/{clientId}/{docId}_prepared.pdf, and transitions document status to Sent"
|
- "A field placed at screen top (screenY=0) on a 792pt-tall page produces pdfY≈792 — Y-axis flip is correct"
|
||||||
artifacts:
|
artifacts:
|
||||||
- path: "teressa-copeland-homes/src/lib/db/schema.ts"
|
- path: "teressa-copeland-homes/src/lib/db/schema.ts"
|
||||||
provides: "Extended documents table with 4 new nullable columns"
|
provides: "Extended documents table with 4 new nullable columns"
|
||||||
@@ -26,6 +27,9 @@ must_haves:
|
|||||||
- path: "teressa-copeland-homes/src/lib/pdf/prepare-document.ts"
|
- path: "teressa-copeland-homes/src/lib/pdf/prepare-document.ts"
|
||||||
provides: "Server-side PDF mutation utility using @cantoo/pdf-lib"
|
provides: "Server-side PDF mutation utility using @cantoo/pdf-lib"
|
||||||
exports: ["preparePdf"]
|
exports: ["preparePdf"]
|
||||||
|
- path: "teressa-copeland-homes/src/lib/pdf/__tests__/prepare-document.test.ts"
|
||||||
|
provides: "Unit tests verifying Y-flip coordinate conversion formula against US Letter dimensions"
|
||||||
|
exports: []
|
||||||
- path: "teressa-copeland-homes/src/app/api/documents/[id]/fields/route.ts"
|
- path: "teressa-copeland-homes/src/app/api/documents/[id]/fields/route.ts"
|
||||||
provides: "GET/PUT API for signature field coordinates"
|
provides: "GET/PUT API for signature field coordinates"
|
||||||
exports: ["GET", "PUT"]
|
exports: ["GET", "PUT"]
|
||||||
@@ -48,11 +52,11 @@ must_haves:
|
|||||||
---
|
---
|
||||||
|
|
||||||
<objective>
|
<objective>
|
||||||
Extend the database schema with four new columns on the documents table, generate and apply the Drizzle migration, create the server-side PDF preparation utility using @cantoo/pdf-lib, and add two new API route files for signature field storage and document preparation.
|
Extend the database schema with four new columns on the documents table, generate and apply the Drizzle migration, create the server-side PDF preparation utility using @cantoo/pdf-lib, add two new API route files for signature field storage and document preparation, and add a unit test verifying the Y-flip coordinate conversion formula.
|
||||||
|
|
||||||
Purpose: Establish the data contracts and server logic that the field-placer UI (Plan 02) and text-fill UI (Plan 03) will consume. Everything in Phase 5 builds on these server endpoints.
|
Purpose: Establish the data contracts and server logic that the field-placer UI (Plan 02) and text-fill UI (Plan 03) will consume. Everything in Phase 5 builds on these server endpoints.
|
||||||
|
|
||||||
Output: Migration 0003 applied, two new API routes live, @cantoo/pdf-lib utility with atomic write behavior.
|
Output: Migration 0003 applied, two new API routes live, @cantoo/pdf-lib utility with atomic write behavior, passing unit test for Y-flip formula.
|
||||||
</objective>
|
</objective>
|
||||||
|
|
||||||
<execution_context>
|
<execution_context>
|
||||||
@@ -154,7 +158,7 @@ npm run db:migrate
|
|||||||
The migration should add 4 columns (ALTER TABLE documents ADD COLUMN ...) — confirm the generated SQL looks correct before applying. The migration command will prompt if needed; migration applies via drizzle-kit migrate (not push — schema is controlled via migrations in this project).
|
The migration should add 4 columns (ALTER TABLE documents ADD COLUMN ...) — confirm the generated SQL looks correct before applying. The migration command will prompt if needed; migration applies via drizzle-kit migrate (not push — schema is controlled via migrations in this project).
|
||||||
</action>
|
</action>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && node -e "const { db } = require('./src/lib/db'); db.execute('SELECT signature_fields, text_fill_data, assigned_client_id, prepared_file_path FROM documents LIMIT 0').then(() => { console.log('PASS: columns exist'); process.exit(0); }).catch(e => { console.error('FAIL:', e.message); process.exit(1); });" 2>/dev/null || echo "Verify via: ls drizzle/0003_*.sql should show generated file"</automated>
|
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run db:migrate 2>&1 | tail -5 && ls drizzle/0003_*.sql</automated>
|
||||||
</verify>
|
</verify>
|
||||||
<done>schema.ts has all 4 new nullable columns typed correctly; drizzle/0003_*.sql exists; npm run db:migrate completes without error</done>
|
<done>schema.ts has all 4 new nullable columns typed correctly; drizzle/0003_*.sql exists; npm run db:migrate completes without error</done>
|
||||||
</task>
|
</task>
|
||||||
@@ -380,15 +384,139 @@ cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | tail
|
|||||||
</done>
|
</done>
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Write unit tests for Y-flip coordinate conversion formula</name>
|
||||||
|
<files>
|
||||||
|
teressa-copeland-homes/src/lib/pdf/__tests__/prepare-document.test.ts
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create the directory if needed and write the test file. This test extracts the coordinate conversion logic (which will also exist inlined in FieldPlacer.tsx) as pure functions so they can be verified in isolation — no PDF loading, no DB, no network.
|
||||||
|
|
||||||
|
The coordinate conversion formulas are:
|
||||||
|
```
|
||||||
|
pdfY = ((renderedH - screenY) / renderedH) * originalHeight
|
||||||
|
pdfX = (screenX / renderedW) * originalWidth
|
||||||
|
```
|
||||||
|
|
||||||
|
For a US Letter PDF (612 × 792 pts) rendered at 1:1 scale (renderedW=612, renderedH=792):
|
||||||
|
- screenY=0 (visual top) → pdfY = ((792-0)/792) * 792 = 792 (high PDF Y = top of PDF space)
|
||||||
|
- screenY=792 (visual bottom) → pdfY = ((792-792)/792) * 792 = 0 (low PDF Y = bottom of PDF space)
|
||||||
|
- screenX=0 → pdfX = 0
|
||||||
|
- screenX=612 → pdfX = 612
|
||||||
|
|
||||||
|
Also test at a 50% zoom (renderedW=306, renderedH=396) — scaling must not affect the PDF coordinate output because the formula compensates for scale:
|
||||||
|
- screenY=0, renderedH=396, originalHeight=792 → pdfY = (396/396) * 792 = 792
|
||||||
|
|
||||||
|
Create teressa-copeland-homes/src/lib/pdf/__tests__/prepare-document.test.ts:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* Unit tests for the Y-flip coordinate conversion formula used in FieldPlacer.tsx.
|
||||||
|
*
|
||||||
|
* PDF user space: origin at bottom-left, Y increases upward.
|
||||||
|
* DOM/screen space: origin at top-left, Y increases downward.
|
||||||
|
* Formula must flip the Y axis.
|
||||||
|
*
|
||||||
|
* US Letter: 612 × 792 pts at 72 DPI.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Pure conversion functions extracted from the FieldPlacer formula.
|
||||||
|
// These MUST match what is implemented in FieldPlacer.tsx exactly.
|
||||||
|
function screenToPdfY(screenY: number, renderedH: number, originalHeight: number): number {
|
||||||
|
return ((renderedH - screenY) / renderedH) * originalHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function screenToPdfX(screenX: number, renderedW: number, originalWidth: number): number {
|
||||||
|
return (screenX / renderedW) * originalWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
const US_LETTER_W = 612; // pts
|
||||||
|
const US_LETTER_H = 792; // pts
|
||||||
|
|
||||||
|
describe('Y-flip coordinate conversion (US Letter 612×792)', () => {
|
||||||
|
describe('at 1:1 scale (rendered = original dimensions)', () => {
|
||||||
|
const rW = US_LETTER_W;
|
||||||
|
const rH = US_LETTER_H;
|
||||||
|
|
||||||
|
test('screenY=0 (visual top) produces pdfY≈792 (PDF top)', () => {
|
||||||
|
expect(screenToPdfY(0, rH, US_LETTER_H)).toBeCloseTo(792, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('screenY=792 (visual bottom) produces pdfY≈0 (PDF bottom)', () => {
|
||||||
|
expect(screenToPdfY(792, rH, US_LETTER_H)).toBeCloseTo(0, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('screenY=396 (visual center) produces pdfY≈396 (PDF center)', () => {
|
||||||
|
expect(screenToPdfY(396, rH, US_LETTER_H)).toBeCloseTo(396, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('screenX=0 produces pdfX=0', () => {
|
||||||
|
expect(screenToPdfX(0, rW, US_LETTER_W)).toBeCloseTo(0, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('screenX=612 (full width) produces pdfX≈612', () => {
|
||||||
|
expect(screenToPdfX(612, rW, US_LETTER_W)).toBeCloseTo(612, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('screenX=306 (horizontal center) produces pdfX≈306', () => {
|
||||||
|
expect(screenToPdfX(306, rW, US_LETTER_W)).toBeCloseTo(306, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('at 50% zoom (rendered = half original dimensions)', () => {
|
||||||
|
const rW = US_LETTER_W / 2; // 306
|
||||||
|
const rH = US_LETTER_H / 2; // 396
|
||||||
|
|
||||||
|
test('screenY=0 at 50% zoom still produces pdfY≈792 (scale-invariant)', () => {
|
||||||
|
expect(screenToPdfY(0, rH, US_LETTER_H)).toBeCloseTo(792, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('screenY=396 (visual bottom at 50% zoom) produces pdfY≈0', () => {
|
||||||
|
expect(screenToPdfY(396, rH, US_LETTER_H)).toBeCloseTo(0, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('screenY=198 (visual center at 50% zoom) produces pdfY≈396', () => {
|
||||||
|
expect(screenToPdfY(198, rH, US_LETTER_H)).toBeCloseTo(396, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('screenX=153 (quarter width at 50% zoom) produces pdfX≈306 (half PDF width)', () => {
|
||||||
|
expect(screenToPdfX(153, rW, US_LETTER_W)).toBeCloseTo(306, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
No special Jest config should be needed — the project uses ts-jest or similar already (check package.json). If there is no test runner configured, add jest config as follows in package.json:
|
||||||
|
```json
|
||||||
|
"jest": {
|
||||||
|
"preset": "ts-jest",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
And install if missing: `npm install --save-dev jest ts-jest @types/jest`
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx jest src/lib/pdf/__tests__/prepare-document.test.ts --no-coverage 2>&1 | tail -20</automated>
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- src/lib/pdf/__tests__/prepare-document.test.ts exists with 10 test cases
|
||||||
|
- All tests pass: npx jest src/lib/pdf/__tests__/prepare-document.test.ts --no-coverage exits 0
|
||||||
|
- screenY=0 on a 792pt page produces pdfY≈792 (visual top = high PDF Y confirmed)
|
||||||
|
- Tests pass at both 1:1 and 50% zoom to confirm scale-invariance of formula
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
</tasks>
|
</tasks>
|
||||||
|
|
||||||
<verification>
|
<verification>
|
||||||
After both tasks complete:
|
After all tasks complete:
|
||||||
1. `ls /Users/ccopeland/temp/red/teressa-copeland-homes/drizzle/0003_*.sql` — migration file exists
|
1. `ls /Users/ccopeland/temp/red/teressa-copeland-homes/drizzle/0003_*.sql` — migration file exists
|
||||||
2. `ls /Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/pdf/prepare-document.ts` — utility exists
|
2. `ls /Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/pdf/prepare-document.ts` — utility exists
|
||||||
3. `ls /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/documents/[docId]/fields/route.ts` — route exists (note: directory name may use [id] to match existing pattern)
|
3. `ls /Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/pdf/__tests__/prepare-document.test.ts` — test file exists
|
||||||
4. `npm run build` — clean compile
|
4. `ls /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/documents/[id]/fields/route.ts` — route exists
|
||||||
5. `curl -s http://localhost:3000/api/documents/test-id/fields` — returns "Unauthorized" (server must be running)
|
5. `npm run build` — clean compile
|
||||||
|
6. `npx jest src/lib/pdf/__tests__/prepare-document.test.ts --no-coverage` — all 10 tests pass
|
||||||
|
7. `curl -s http://localhost:3000/api/documents/test-id/fields` — returns "Unauthorized" (server must be running)
|
||||||
</verification>
|
</verification>
|
||||||
|
|
||||||
<success_criteria>
|
<success_criteria>
|
||||||
@@ -397,8 +525,11 @@ After both tasks complete:
|
|||||||
- @cantoo/pdf-lib installed (NOT the original pdf-lib — they conflict)
|
- @cantoo/pdf-lib installed (NOT the original pdf-lib — they conflict)
|
||||||
- preparePdf utility uses atomic tmp→rename write pattern
|
- preparePdf utility uses atomic tmp→rename write pattern
|
||||||
- form.flatten() called BEFORE drawing signature rectangles
|
- form.flatten() called BEFORE drawing signature rectangles
|
||||||
- GET/PUT /api/documents/[id]/fields returns 401 without auth, correct JSON with auth
|
- GET /api/documents/[id]/fields returns 401 without auth, [] with auth for a document with no fields
|
||||||
- POST /api/documents/[id]/prepare returns 401 without auth; with auth loads doc, calls preparePdf, transitions status to Sent
|
- PUT /api/documents/[id]/fields stores the array and returns it
|
||||||
|
- POST /api/documents/[id]/prepare returns 401 without auth; with auth loads doc, calls preparePdf, transitions status to Sent, creates prepared PDF on disk
|
||||||
|
- POST /api/documents/[id]/prepare returns 422 if document has no filePath
|
||||||
|
- Unit tests pass: screenY=0 on 792pt page → pdfY≈792; formula is scale-invariant
|
||||||
- npm run build compiles without errors
|
- npm run build compiles without errors
|
||||||
</success_criteria>
|
</success_criteria>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user