WFDASH-WEBAPP/scripts/migrations/2026-09-25-backfill-invoice-reconcile.ts
Dave 7555196166 feat(audit): implement audit trail for delivery notes and orders
- Added an audit trail feature that logs every create, update, and delete action on delivery notes and orders, capturing detailed changes per field.
- Introduced the `AuditTimeline` component to display the history of changes for delivery notes and orders, enhancing traceability.
- Updated the `page.tsx` files for delivery notes and orders to fetch and display audit logs, ensuring users can view the history of modifications.
- Enhanced the `package.json` with a new migration script for setting up the audit log functionality.

This feature improves accountability and transparency in the management of delivery notes and orders.
2026-09-30 01:48:44 +02:00

100 lines
3.7 KiB
TypeScript

/**
* Backfill `customer_invoice_number`, `invoiced_date` and (where it can
* be known) `qty_invoiced` on order_items that were invoiced through the
* Invoicing page while its reconcile call was 401-ing.
*
* DRY RUN by default — reads only and prints the plan:
*
* npx tsx scripts/migrations/2026-09-25-backfill-invoice-reconcile.ts
*
* Write the plan (after reviewing it):
*
* npx tsx scripts/migrations/2026-09-25-backfill-invoice-reconcile.ts --apply
*
* Never changes `item_status`. Rows that need a human decision (short
* pickups, unknown billed qty, `ready_invoice` rows that already carry a
* QuickBooks invoice) are listed, not touched beyond the stamps. Logic
* and rules: src/lib/server/invoice-backfill.ts.
*
* Writes carry X-Audit-* headers so the audit_log hook records them as
* "Invoice backfill".
*/
import "./_env-loader";
import PocketBase from "pocketbase";
import { planInvoiceBackfill, type BackfillRow } from "../../src/lib/server/invoice-backfill";
const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090";
const APPLY = process.argv.includes("--apply");
async function main() {
const email = process.env.PB_ADMIN_EMAIL;
const password = process.env.PB_ADMIN_PASSWORD;
if (!email || !password) throw new Error("Set PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD.");
const pb = new PocketBase(PB_URL);
pb.autoCancellation(false);
await pb.collection("_superusers").authWithPassword(email, password);
pb.beforeSend = (url, options) => {
options.headers = {
...(options.headers as Record<string, string> | undefined),
"X-Audit-Source": "system",
"X-Audit-Actor-Name": encodeURIComponent("Invoice backfill 2026-09-25"),
};
return { url, options };
};
console.log(`[invoice-backfill] ${APPLY ? "APPLY" : "DRY RUN"} against ${PB_URL}`);
const rows = await pb.collection("order_items").getFullList<BackfillRow>({
// Everything that could carry an invoice; the plan filters to
// QuickBooks invoices precisely.
filter: 'order_item_details != null && order_item_details != ""',
fields:
"id,order,description,item_status,quantity,qty_collected,qty_invoiced," +
"customer_invoice_number,invoiced_date,exclude_from_invoice,order_item_details",
batch: 1000,
});
const plan = planInvoiceBackfill(rows);
console.log(
`[invoice-backfill] scanned ${plan.scanned} | no QB invoice ${plan.skippedNoQbInvoice} | ` +
`already reconciled ${plan.skippedAlreadyReconciled} | to stamp ${plan.patches.length} | ` +
`need a decision ${plan.needsDecision.length}`,
);
const withQty = plan.patches.filter((p) => p.patch.qty_invoiced !== undefined).length;
console.log(`[invoice-backfill] of the stamps, ${withQty} also set qty_invoiced`);
if (plan.needsDecision.length > 0) {
console.log("\n[invoice-backfill] NEEDS A DECISION (item_status left unchanged):");
console.table(
plan.needsDecision.map((d) => ({
id: d.id,
order: d.order,
invoice: d.invoiceNumber,
status: d.item_status,
qty: d.quantity,
collected: d.qty_collected,
billed_est: d.estimatedBilled,
reason: d.reason,
description: (d.description ?? "").slice(0, 40),
})),
);
}
if (!APPLY) {
console.log("\n[invoice-backfill] Dry run only — nothing written. Re-run with --apply to write the stamps.");
return;
}
let written = 0;
for (const { id, patch } of plan.patches) {
await pb.collection("order_items").update(id, patch);
written += 1;
}
console.log(`[invoice-backfill] Wrote ${written} row(s).`);
}
main().catch((err) => {
console.error("[invoice-backfill] Failed:", err);
process.exit(1);
});