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:
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user