mega-plant/app/api/purchases/[id]/invoices/[invoiceId]/route.ts
David 642d23dd24 Suppliers: replace the menu entry with the new page, flag over-invoiced POs, audited invoice amount corrections
- Menu: old Suppliers leaf hidden (route kept), Suppliers 2 renamed Suppliers
- PO invoice status: new 'over' state (never closed), live over-PO warning when typing
- Admin correction of invoice amount/date/number with mandatory reason, logged to purchase_invoice_edits
- Payable now carries unpaid balance forward month to month

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 20:49:05 +02:00

92 lines
4.9 KiB
TypeScript

import { revalidatePath } from "next/cache";
import { serverAuth } from "@/lib/pb";
import { getSession } from "@/lib/auth";
import { refuseUnlessFull } from "@/lib/require-full";
export const dynamic = "force-dynamic";
// DELETE /api/purchases/:id/invoices/:invoiceId — remove a supplier invoice from a PO.
export async function DELETE(
_req: Request,
{ params }: { params: Promise<{ id: string; invoiceId: string }> },
) {
const { id, invoiceId } = await params;
try {
const pb = await serverAuth();
const inv = await pb.collection("purchase_invoices").getOne(invoiceId).catch(() => null);
if (!inv || inv.purchase !== id) {
return Response.json({ ok: false, error: "Invoice not found on that purchase order" }, { status: 404 });
}
await pb.collection("purchase_invoices").delete(invoiceId);
revalidatePath("/running/purchases");
revalidatePath("/expenses/suppliers-2");
return Response.json({ ok: true });
} catch (e: any) {
return Response.json({ ok: false, error: e?.message || "Could not delete the invoice" }, { status: 500 });
}
}
// PATCH /api/purchases/:id/invoices/:invoiceId { amount_incl?, invoice_date?, invoice_no?, reason }
// Admin correction of a typed-in invoice. A REASON is mandatory, and every changed field is
// written to `purchase_invoice_edits` (old value, new value, reason, who) — append-only.
export async function PATCH(
req: Request,
{ params }: { params: Promise<{ id: string; invoiceId: string }> },
) {
const refused = await refuseUnlessFull();
if (refused) return refused;
const { id, invoiceId } = await params;
let body: { amount_incl?: unknown; invoice_date?: unknown; invoice_no?: unknown; reason?: unknown } = {};
try { body = await req.json(); } catch { return Response.json({ ok: false, error: "Invalid JSON body" }, { status: 400 }); }
const reason = String(body.reason ?? "").trim();
if (reason.length < 3) return Response.json({ ok: false, error: "Give a reason for the change." }, { status: 400 });
try {
const pb = await serverAuth();
const inv = await pb.collection("purchase_invoices").getOne(invoiceId).catch(() => null);
if (!inv || inv.purchase !== id) return Response.json({ ok: false, error: "Invoice not found on that purchase order" }, { status: 404 });
const po = await pb.collection("purchases").getOne(id);
const patch: Record<string, unknown> = {};
const changes: { field: string; old: string; next: string }[] = [];
if ("amount_incl" in body) {
const raw = String(body.amount_incl ?? "").trim().replace(/\s/g, "").replace(",", ".");
const n = Number(raw);
if (!raw || !Number.isFinite(n) || n < 0) return Response.json({ ok: false, error: "The amount must be a positive number." }, { status: 400 });
if (n !== (inv.amount_incl ?? 0)) { patch.amount_incl = n; changes.push({ field: "amount_incl", old: String(inv.amount_incl ?? ""), next: String(n) }); }
}
if ("invoice_date" in body) {
const d = String(body.invoice_date ?? "").trim().slice(0, 10);
if (d && !/^\d{4}-\d{2}-\d{2}$/.test(d)) return Response.json({ ok: false, error: "Enter a valid date." }, { status: 400 });
if (d !== (inv.invoice_date || "")) { patch.invoice_date = d; changes.push({ field: "invoice_date", old: inv.invoice_date || "", next: d }); }
}
if ("invoice_no" in body) {
const no = String(body.invoice_no ?? "").trim();
if (!no) return Response.json({ ok: false, error: "The invoice number cannot be empty." }, { status: 400 });
if (no !== inv.invoice_no) {
const clash = (await pb.collection("purchase_invoices").getFullList({ filter: `purchase = "${id}"` }))
.find((i) => i.id !== invoiceId && (i.invoice_no || "").toUpperCase() === no.toUpperCase());
if (clash) return Response.json({ ok: false, error: `${no} is already on ${po.po_no}` }, { status: 409 });
patch.invoice_no = no; changes.push({ field: "invoice_no", old: inv.invoice_no || "", next: no });
}
}
if (!changes.length) return Response.json({ ok: false, error: "Nothing was changed." }, { status: 400 });
const session = await getSession();
const who = session ? `${session.user.name || session.user.email} <${session.user.email}>` : "unknown";
// Log FIRST: if the log cannot be written the edit must not happen unrecorded.
for (const c of changes) {
await pb.collection("purchase_invoice_edits").create({
invoice: invoiceId, po_no: po.po_no, invoice_no: inv.invoice_no, field: c.field,
old_value: c.old, new_value: c.next, reason: reason.slice(0, 500), edited_by: who,
});
}
await pb.collection("purchase_invoices").update(invoiceId, patch);
revalidatePath("/running/purchases");
revalidatePath("/expenses/suppliers-2");
return Response.json({ ok: true });
} catch (e: any) {
return Response.json({ ok: false, error: e?.message || "Could not save the change" }, { status: 500 });
}
}