From e91f29e555d2aecf66eb8243757c7e2af05cbd2b Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Sat, 21 Mar 2026 17:04:00 -0600 Subject: [PATCH] feat(13-02): implement POST /api/documents/[id]/ai-prepare route - Auth guard (401), OPENAI_API_KEY guard (503), not-found (404) - Draft-only guard (403), path traversal guard (403) - Loads document with client relation via Drizzle with: { client: true } - Calls extractPdfText then classifyFieldsWithAI in try/catch (500 on error) - Writes SignatureFieldData[] to DB signatureFields; status stays Draft - Returns { fields, textFillData } keyed by field UUID --- .../api/documents/[id]/ai-prepare/route.ts | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 teressa-copeland-homes/src/app/api/documents/[id]/ai-prepare/route.ts diff --git a/teressa-copeland-homes/src/app/api/documents/[id]/ai-prepare/route.ts b/teressa-copeland-homes/src/app/api/documents/[id]/ai-prepare/route.ts new file mode 100644 index 0000000..5825273 --- /dev/null +++ b/teressa-copeland-homes/src/app/api/documents/[id]/ai-prepare/route.ts @@ -0,0 +1,76 @@ +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 and classify fields — wrapped in try/catch for AI/PDF errors + try { + // 6a. Extract text from PDF (server-side, pdfjs-dist legacy build) + const pageTexts = await extractPdfText(filePath); + + // 6b. 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, + ); + + // 7. 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(); + + // 8. Return fields and textFillData for client state update + return Response.json({ + fields: updated.signatureFields ?? [], + textFillData, + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return Response.json({ error: message }, { status: 500 }); + } +}