feat(api): add /quote_items/[id]/item-history endpoint

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-07-29 14:59:50 +02:00
parent d2f6a0cd0b
commit e5a9221c3e

View file

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