diff --git a/src/app/api/v1/items/[collection]/[id]/intel/refresh/route.ts b/src/app/api/v1/items/[collection]/[id]/intel/refresh/route.ts new file mode 100644 index 0000000..661bfe4 --- /dev/null +++ b/src/app/api/v1/items/[collection]/[id]/intel/refresh/route.ts @@ -0,0 +1,88 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getAdminPocketBase } from "@/lib/server/pocketbase-admin"; +import { populateItemIntel } from "@/lib/server/item-intel-service"; +import type { ItemIntel } from "@/lib/types"; + +/** + * POST /api/v1/items/[collection]/[id]/intel/refresh + * + * collection: "quote_items" | "order_items" + * id: the PB record id + * body: { dry_run?: boolean } + * + * Runs `populateItemIntel` for one item and writes the result back + * to `ai_item_insights`. Used by: + * + * - The n8n webhook that fires on quote_item / order_item create, + * so new items get intel before the operator opens them. + * - The n8n cron that batches historical items via + * /api/v1/items/backfill-intel (see backfill-intel/route.ts). + * + * The route is gated to trusted callers (PB admin via + * `getAdminPocketBase`). For live-traffic exposure the agent key + * would be required, but n8n runs server-side and has the admin + * token already. + * + * Runtime: nodejs. + */ +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +const VALID_COLLECTIONS = ["quote_items", "order_items"] as const; + +export async function POST( + req: NextRequest, + { params }: { params: Promise<{ collection: string; id: string }> }, +) { + try { + const { collection, id } = await params; + if (!VALID_COLLECTIONS.includes(collection as "quote_items" | "order_items")) { + return NextResponse.json( + { error: `Invalid collection "${collection}". Use quote_items or order_items.` }, + { status: 400 }, + ); + } + if (!id) { + return NextResponse.json({ error: "Missing id" }, { status: 400 }); + } + + const body = (await req.json().catch(() => ({}))) as { dry_run?: boolean }; + const dryRun = body.dry_run === true; + + // Verify the record exists (cheap check) so the caller gets a clean + // 404 instead of a confusing 500 from the backfill service. + const pb = await getAdminPocketBase(); + try { + await pb.collection(collection).getOne(id); + } catch { + return NextResponse.json( + { error: `${collection}/${id} not found` }, + { status: 404 }, + ); + } + + if (dryRun) { + return NextResponse.json({ + ok: true, + dry_run: true, + collection, + id, + // Surface a tiny preview of what would change by computing + // the intel without writing. + intel: await populateItemIntel(id, collection as "quote_items" | "order_items", { + write: false, + }), + }); + } + + const intel: ItemIntel = await populateItemIntel( + id, + collection as "quote_items" | "order_items", + { write: true }, + ); + return NextResponse.json({ ok: true, collection, id, intel }); + } catch (error) { + const message = error instanceof Error ? error.message : "Refresh failed"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} \ No newline at end of file