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
This commit is contained in:
Chandler Copeland
2026-03-21 17:04:00 -06:00
parent 24e1f5aa00
commit e91f29e555

View File

@@ -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 });
}
}