import { serverAuth } from "@/lib/pb"; import { revalidateInvoicePages } from "@/lib/invoice-revalidate"; import { canReplaceRetrySource, intakeRetryHeaders } from "@/lib/intake-retry"; export const dynamic = "force-dynamic"; // POST /api/invoices/intake/:id/retry // Re-attempts the import for a problem invoice. // // Retry is only meaningful once the REASON is addressed, so this never silently // re-parses a broken PDF and calls it fixed: it re-reads the stored document and, if // the parse still fails, updates the recorded reason instead of pretending it worked. // Once it succeeds the inbox row is marked imported and linked to the new invoice. export async function POST( _req: Request, { params }: { params: Promise<{ id: string }> }, ) { const { id } = await params; const pb = await serverAuth(); try { const record = await pb.collection("invoice_intake").getOne(id); if (!record.source_pdf) { return Response.json({ ok: false, error: "The original document is no longer stored" }, { status: 400 }); } // Pull the stored bytes back down. A bare /api/files/ 404s for a // rule-locked collection even with an admin token — protected files resolve by // the full /collectionId/recordId/filename path, which the SDK builds for us. const token = record.source_pdf as string; const url = pb.files.getURL(record, token); const res = await fetch(url, { headers: pb.authStore.token ? { Authorization: pb.authStore.token } : undefined, }); if (!res.ok) { const reason = `Could not read the stored document back from storage (HTTP ${res.status})`; await pb.collection("invoice_intake").update(id, { status: "failed", error: reason }); return Response.json({ ok: false, error: reason }, { status: 502 }); } const file = new File([await res.arrayBuffer()], record.file_name || token, { type: "application/pdf", }); const fd = new FormData(); fd.append("file", file); // Re-run the full intake against the same bytes. const intake = await fetch(new URL("/api/invoices/intake", _req.url), { method: "POST", headers: intakeRetryHeaders(_req), body: fd, redirect: "error", }); const result = await intake.json().catch(() => ({ ok: false, error: "Unexpected response" })); // Only discard this document once an import or a replacement inbox record // exists. Auth/outage/duplicate failures must keep the original available. if (!canReplaceRetrySource(intake.status, result)) { return Response.json({ ok: false, error: result.error || "Could not retry this invoice. The original document has been kept." }, { status: intake.ok ? 502 : intake.status }); } if (result.ok) { await pb.collection("invoice_intake").delete(id); revalidateInvoicePages("/income"); return Response.json({ ok: true, retried: true, ...result }); } // Still failing — keep it in the inbox with the newest reason, and drop the // duplicate record the retry just created so the inbox stays one row per file. await pb.collection("invoice_intake").delete(id).catch(() => {}); revalidateInvoicePages(); return Response.json( { ok: false, retried: true, stillFailing: true, error: result.error ?? "Import failed again" }, { status: 422 }, ); } catch (e: any) { return Response.json({ ok: false, error: e?.message || "Retry failed" }, { status: 500 }); } }