From e5a9221c3e1505226ea946ee7ecafb35aeb2ad32 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Wed, 29 Jul 2026 14:59:50 +0200 Subject: [PATCH] feat(api): add /quote_items/[id]/item-history endpoint Co-authored-by: Orca --- .../v1/quote_items/[id]/item-history/route.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/app/api/v1/quote_items/[id]/item-history/route.ts diff --git a/src/app/api/v1/quote_items/[id]/item-history/route.ts b/src/app/api/v1/quote_items/[id]/item-history/route.ts new file mode 100644 index 0000000..fa78031 --- /dev/null +++ b/src/app/api/v1/quote_items/[id]/item-history/route.ts @@ -0,0 +1,85 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getAdminPocketBase } from "@/lib/server/pocketbase-admin"; +import { getItemHistory } from "@/lib/server/item-history-service"; +import type { ItemHistoryResponse } from "@/lib/types"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +/** + * GET /api/v1/quote_items/[id]/item-history + * + * Composes deterministic PB joins (via `getItemHistory`) with the + * existing n8n-written `ai_item_insights` blob into one response. + * + * The `ITEM_HISTORY_COMPARE_MODE` env var controls which side runs: + * - `both` (default) — both sides, side-by-side render. + * - `deterministic` — PB joins only; skip the AI read. + * - `ai` — AI blob only; skip the service call. + * - `none` — 404; trigger hides itself. + * + * Auth: same as the rest of `/api/v1` (no Bearer required, uses admin PB). + */ + +type Mode = "both" | "deterministic" | "ai" | "none"; + +function readMode(): Mode { + const raw = process.env.ITEM_HISTORY_COMPARE_MODE?.toLowerCase(); + if (raw === "deterministic" || raw === "ai" || raw === "none") return raw; + return "both"; +} + +export async function GET( + _req: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params; + if (!id || typeof id !== "string") { + return NextResponse.json({ error: "Missing id" }, { status: 400 }); + } + + const mode = readMode(); + if (mode === "none") { + return NextResponse.json({ error: "Item history disabled" }, { status: 404 }); + } + + const generated_at = new Date().toISOString(); + const response: ItemHistoryResponse = { + deterministic: null, + ai: null, + ai_updated_at: null, + generated_at, + }; + + // Run the deterministic service unless mode === "ai". + if (mode === "both" || mode === "deterministic") { + try { + response.deterministic = await getItemHistory(id); + } catch (err) { + response.error = err instanceof Error ? err.message : "Deterministic lookup failed"; + } + } + + // Read the AI blob unless mode === "deterministic". + if (mode === "both" || mode === "ai") { + try { + const pb = await getAdminPocketBase(); + const row = await pb.collection("quote_items").getOne<{ + ai_item_insights?: string | null; + updated?: string; + }>(id, { fields: "ai_item_insights,updated" }); + if (row.ai_item_insights) { + try { + response.ai = JSON.parse(row.ai_item_insights); + response.ai_updated_at = row.updated || null; + } catch { + // Malformed JSON — leave `ai` null and continue. + } + } + } catch { + // Silent: deterministic side still works. + } + } + + return NextResponse.json(response); +}