---
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"
---

<objective>
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`
</objective>

<execution_context>
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.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.
</context>

<interfaces>
<!-- Key types and patterns from the codebase. Extracted for executor. -->

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<SignatureFieldData[]>(),
  textFillData: jsonb("text_fill_data").$type<Record<string, string>>(),
  // ...
});
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<PageText[]>
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<string, string> }>
```
</interfaces>

<tasks>

<task type="auto">
  <name>Task 1: Create POST /api/documents/[id]/ai-prepare route</name>
  <files>teressa-copeland-homes/src/app/api/documents/[id]/ai-prepare/route.ts</files>
  <action>
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.).
  </action>
  <verify>
    <automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | grep -E "ai-prepare|error" | head -20</automated>
  </verify>
  <done>
    - `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<string, string> }`
  </done>
</task>

</tasks>

<verification>
- `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)
</verification>

<success_criteria>
- `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
</success_criteria>

<output>
After completion, create `.planning/phases/13-ai-field-placement-and-pre-fill/13-02-SUMMARY.md`
</output>
