import { NextRequest, NextResponse } from "next/server"; import { processReceiptIngestion, ReceiptValidationError, validateReceipt, type ParsedReceipt } from "@/lib/receipt-ingestion"; /** * Machine ingest endpoint for grocery receipts scanned in pantry-app. * * Sibling to /api/orders/ingest and deliberately shaped like it. Auth is a shared secret * rather than the Traefik `x-forwarded-user` header: this is called app-to-app, and there * is no browser session to forward. * * Its own token rather than ORDER_INGEST_TOKEN so pantry's credential can be rotated * without touching the n8n order flow, which runs on a schedule nobody is watching. */ function authorised(req: NextRequest): boolean { const expected = process.env.RECEIPT_INGEST_TOKEN; if (!expected) return false; // fail closed when unconfigured const got = req.headers.get("x-ingest-token"); return !!got && got === expected; } export async function POST(req: NextRequest) { if (!authorised(req)) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); let body: ParsedReceipt & { dryRun?: boolean }; try { body = await req.json(); } catch { return NextResponse.json({ error: "invalid JSON" }, { status: 400 }); } if (!body?.merchant_name || !body?.transaction_date || typeof body?.total !== "number" || !Number.isInteger(body?.capture_event_id)) { return NextResponse.json({ error: "merchant_name, transaction_date, total and capture_event_id are required" }, { status: 400 }); } try { // Dry run validates and reports what would be written without writing it — the same // affordance every maintenance script in pantry has, and for the same reason: the first // pass over a new receipt format is worth reading before it becomes money. if (body.dryRun) return NextResponse.json({ kind: "receipt", dryRun: true, flags: validateReceipt(body) }); const result = await processReceiptIngestion(body); return NextResponse.json({ kind: "receipt", ...result }); } catch (e) { // A receipt that will not validate is the failure that matters: it means the payment // side was read wrong, and booking it anyway would put a number in the ledger nobody // can stand behind. Loud, like OrderParseError. if (e instanceof ReceiptValidationError) { return NextResponse.json({ kind: "rejected", reason: e.message }, { status: 422 }); } const message = e instanceof Error ? e.message : String(e); return NextResponse.json({ error: message }, { status: 500 }); } }