--- phase: 13-ai-field-placement-and-pre-fill plan: 02 type: execute wave: 2 depends_on: - 13-01 files_modified: - teressa-copeland-homes/src/app/api/documents/[id]/ai-prepare/route.ts autonomous: true requirements: - AI-01 - AI-02 must_haves: truths: - "POST /api/documents/[id]/ai-prepare extracts PDF text, calls GPT-4o-mini, converts coordinates, and writes SignatureFieldData[] to the DB" - "Route returns { fields, textFillData } on success — client uses this to update FieldPlacer and pre-fill state" - "Route returns 503 if OPENAI_API_KEY is not configured, 401 if unauthenticated, 404 if document not found, 403 if document is not Draft" - "textFillData in response is keyed by field UUID (not by label string)" artifacts: - path: "teressa-copeland-homes/src/app/api/documents/[id]/ai-prepare/route.ts" provides: "POST handler orchestrating extract → AI → convert → DB write → return" exports: ["POST"] key_links: - from: "teressa-copeland-homes/src/app/api/documents/[id]/ai-prepare/route.ts" to: "teressa-copeland-homes/src/lib/ai/extract-text.ts" via: "import { extractPdfText } from '@/lib/ai/extract-text'" pattern: "extractPdfText" - from: "teressa-copeland-homes/src/app/api/documents/[id]/ai-prepare/route.ts" to: "teressa-copeland-homes/src/lib/ai/field-placement.ts" via: "import { classifyFieldsWithAI } from '@/lib/ai/field-placement'" pattern: "classifyFieldsWithAI" - from: "teressa-copeland-homes/src/app/api/documents/[id]/ai-prepare/route.ts" to: "drizzle documents table" via: "db.update(documents).set({ signatureFields: fields })" pattern: "signatureFields" --- Create the `POST /api/documents/[id]/ai-prepare` route that orchestrates the full AI placement pipeline: extract PDF text, call GPT-4o-mini, convert AI percentage coordinates to PDF user-space points, and write the resulting fields to the database. Purpose: This is the server-side orchestration layer. The AI utility modules (Plan 01) provide the logic; this route wires them to the document DB record and returns the result to the client. Output: `src/app/api/documents/[id]/ai-prepare/route.ts` @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/13-ai-field-placement-and-pre-fill/13-RESEARCH.md @.planning/phases/13-ai-field-placement-and-pre-fill/13-01-SUMMARY.md Read next.config.ts and check for Next.js guide in node_modules/next/dist/docs/ before writing route code. From teressa-copeland-homes/src/lib/db/schema.ts: ```typescript export interface SignatureFieldData { id: string; page: number; x: number; y: number; width: number; height: number; type?: SignatureFieldType; } export const documents = pgTable("documents", { id: text("id").primaryKey(), clientId: text("client_id").notNull(), status: documentStatusEnum("status").notNull().default("Draft"), filePath: text("file_path"), signatureFields: jsonb("signature_fields").$type(), textFillData: jsonb("text_fill_data").$type>(), // ... }); export const clients = pgTable("clients", { id: text("id").primaryKey(), name: text("name").notNull(), propertyAddress: text("property_address"), // ... }); export const documentsRelations = relations(documents, ({ one }) => ({ client: one(clients, { fields: [documents.clientId], references: [clients.id] }), })); ``` From existing prepare route pattern (src/app/api/documents/[id]/prepare/route.ts): ```typescript import { auth } from '@/lib/auth'; import { db } from '@/lib/db'; import { documents, users } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; import path from 'node:path'; const UPLOADS_DIR = path.join(process.cwd(), 'uploads'); 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 }); const { id } = await params; const doc = await db.query.documents.findFirst({ where: eq(documents.id, id), with: { client: true }, // use this to get client.name and client.propertyAddress }); if (!doc) return Response.json({ error: 'Not found' }, { status: 404 }); // ... } ``` From Plan 01 (13-01-SUMMARY.md): ```typescript // src/lib/ai/extract-text.ts export async function extractPdfText(filePath: string): Promise export interface PageText { page: number; text: string; width: number; height: number; } // src/lib/ai/field-placement.ts export async function classifyFieldsWithAI( pageTexts: PageText[], client: { name: string | null; propertyAddress: string | null } | null ): Promise<{ fields: SignatureFieldData[], textFillData: Record }> ``` Task 1: Create POST /api/documents/[id]/ai-prepare route teressa-copeland-homes/src/app/api/documents/[id]/ai-prepare/route.ts Create the directory `src/app/api/documents/[id]/ai-prepare/` and the `route.ts` file. Implement: ```typescript import { auth } from '@/lib/auth'; import { db } from '@/lib/db'; import { documents } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; import path from 'node:path'; import { extractPdfText } from '@/lib/ai/extract-text'; import { classifyFieldsWithAI } from '@/lib/ai/field-placement'; const UPLOADS_DIR = path.join(process.cwd(), 'uploads'); export async function POST( req: Request, { params }: { params: Promise<{ id: string }> } ) { // 1. Auth guard — same pattern as prepare/route.ts const session = await auth(); if (!session?.user?.id) return new Response('Unauthorized', { status: 401 }); // 2. OPENAI_API_KEY guard — fail fast with 503 if not configured if (!process.env.OPENAI_API_KEY) { return Response.json( { error: 'OPENAI_API_KEY not configured. Add it to .env.local.' }, { status: 503 } ); } // 3. Load document with client relation const { id } = await params; const doc = await db.query.documents.findFirst({ where: eq(documents.id, id), with: { client: true }, }); if (!doc) return Response.json({ error: 'Not found' }, { status: 404 }); if (!doc.filePath) return Response.json({ error: 'Document has no PDF file' }, { status: 422 }); // 4. Only Draft documents can be AI-prepared (same principle as prepare route) if (doc.status !== 'Draft') { return Response.json({ error: 'Document is locked — only Draft documents can use AI auto-place' }, { status: 403 }); } // 5. Path traversal guard const filePath = path.join(UPLOADS_DIR, doc.filePath); if (!filePath.startsWith(UPLOADS_DIR)) { return new Response('Forbidden', { status: 403 }); } // 6. Extract text from PDF (server-side, pdfjs-dist legacy build) const pageTexts = await extractPdfText(filePath); // 7. Classify fields with GPT-4o-mini + coordinate conversion const client = (doc as typeof doc & { client: { name: string; propertyAddress: string | null } | null }).client; const { fields, textFillData } = await classifyFieldsWithAI( pageTexts, client ? { name: client.name, propertyAddress: client.propertyAddress ?? null } : null, ); // 8. Write fields to DB (replaces existing signatureFields — agent reviews after) // Do NOT change document status — stays Draft so agent can review and adjust const [updated] = await db .update(documents) .set({ signatureFields: fields }) .where(eq(documents.id, id)) .returning(); // 9. Return fields and textFillData for client state update return Response.json({ fields: updated.signatureFields ?? [], textFillData, }); } ``` Key decisions to honor (from STATE.md and RESEARCH.md): - Do NOT use zodResponseFormat (broken with Zod v4) — this route delegates to classifyFieldsWithAI which uses manual json_schema - Do NOT change document status to anything other than Draft — only the prepare route moves status - textFillData is keyed by field UUID (classifyFieldsWithAI handles this — see Plan 01) - `doc.client` is available via Drizzle relation — use `with: { client: true }` in the query Error handling: wrap the extractPdfText + classifyFieldsWithAI calls in try/catch, return 500 with the error message if they throw (AI errors, PDF read errors, etc.). cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | grep -E "ai-prepare|error" | head -20 - `src/app/api/documents/[id]/ai-prepare/route.ts` exists and exports `POST` - TypeScript compiles without errors for this file - Route structure matches Next.js App Router pattern (params as Promise) - All guards present: auth, OPENAI_API_KEY, not-found, no-filePath, non-Draft - Fields written to DB, document status stays Draft - Returns `{ fields: SignatureFieldData[], textFillData: Record }` - `npx tsc --noEmit` passes with no errors related to ai-prepare route - Route file exists at correct Next.js App Router path - Drizzle query uses `with: { client: true }` to load client profile data - Route does NOT call `extractPdfText` or `classifyFieldsWithAI` directly — only after all guards pass - Document status remains Draft after successful AI placement - `textFillData` in response is keyed by SignatureFieldData UUIDs (not labels) - `POST /api/documents/[id]/ai-prepare` returns 401 for unauthenticated requests - Returns 503 when OPENAI_API_KEY is absent from environment - Returns 403 for non-Draft documents - Returns `{ fields, textFillData }` on success, with fields written to DB - TypeScript compiles clean After completion, create `.planning/phases/13-ai-field-placement-and-pre-fill/13-02-SUMMARY.md`