import type { StoredFile } from "@/lib/intake-files"; /** * finance โ†’ pantry hand-off for a grocery upload (plan ยง11 C3). * * The ORIGINAL files go across, not the extracted items: pantry owns grocery * extraction (canonical names, categories, staples) and its capture board is * the review surface. What finance adds is the routing decision โ€” which * transaction the confirm should enrich, or that it should create one โ€” carried * in the `intake` field so pantry's confirm seam (C4) can suppress the ING-10 * create that would otherwise double-book an order the email already booked. */ export type PantryForwardArgs = { files: StoredFile[]; batchId: string; platform: string | null; sharedBy: string | null; financeTargetTransactionId: number | null; financeAction: "enrich" | "create"; }; export type PantryForwardResult = { captureId: number; itemCount: number | null }; export class PantryDuplicateError extends Error { constructor( public readonly captureId: number | null, public readonly reason: string ) { super(reason); } } export class PantryUnavailableError extends Error {} export type PantryForward = (args: PantryForwardArgs) => Promise; export function pantryForwarder(opts: { baseUrl?: string; token?: string; fetchImpl?: typeof fetch } = {}): PantryForward { return async (args) => { const baseUrl = (opts.baseUrl ?? process.env.PANTRY_BASE_URL ?? "").replace(/\/$/, ""); const token = opts.token ?? process.env.RECEIPT_INGEST_TOKEN; if (!baseUrl || !token) throw new PantryUnavailableError("PANTRY_BASE_URL / RECEIPT_INGEST_TOKEN are not configured"); const doFetch = opts.fetchImpl ?? fetch; const form = new FormData(); for (const f of args.files) { form.append("image", new Blob([new Uint8Array(f.bytes)], { type: f.type }), f.name); } form.append("source", "receipt"); form.append( "intake", JSON.stringify({ batch_id: args.batchId, platform: args.platform, origin: "intake", shared_by: args.sharedBy, finance_target_transaction_id: args.financeTargetTransactionId, finance_action: args.financeAction, }) ); let response: Response; try { response = await doFetch(`${baseUrl}/api/capture`, { method: "POST", headers: { "x-ingest-token": token }, body: form }); } catch (e) { throw new PantryUnavailableError(`pantry unreachable: ${e instanceof Error ? e.message : String(e)}`); } const body = (await response.json().catch(() => ({}))) as { captureId?: number; error?: string; items?: unknown[]; reason?: string }; if (response.status === 409) { throw new PantryDuplicateError(typeof body.captureId === "number" ? body.captureId : null, body.reason ?? body.error ?? "already captured"); } if (!response.ok) throw new PantryUnavailableError(`pantry returned ${response.status}: ${body.error ?? ""}`.trim()); if (typeof body.captureId !== "number") throw new PantryUnavailableError("pantry returned no captureId"); return { captureId: body.captureId, itemCount: Array.isArray(body.items) ? body.items.length : null }; }; }