- 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.
100 lines
3.7 KiB
TypeScript
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);
|
|
});
|