WFDASH-WEBAPP/scripts/nss-add-invoice-columns.ts

138 lines
4.4 KiB
TypeScript
Raw Normal View History

fix(invoicing): bill only what's collected, support partial pickups + kit splits The invoice qty rule used to read the customer's order qty regardless of how much the driver actually collected. QuickBooks ended up billing 8-of-10 on one line and 7-of-10 on the other for Kusasalethu PO 390588 (INV10924) — a 5-unit overbilling that the mine's receiving team flags as suspicious. The rule now caps the invoice qty at min(qty_collected, quantity): - Short pickup → bill what's collected, the row stays at item_status="ready_invoice" with qty_invoiced < quantity, and a follow-up invoice fires automatically when the rest arrives. - MOQ overflow → still capped at the customer's order qty; the surplus-book flow continues to route the overflow to stock. The Invoicing page now shows "N short" / "N units short" badges on short-pickup rows, a "N hidden" badge when supplier-side rows are suppressed, and a "Show supplier-side lines" toggle (default OFF) so the invoicing operator doesn't see kit-component remainders unless she needs to. A split-remainder dialog on the order process page lets the operator explicitly flag a remainder as exclude_from_invoice=true when the original line was a kit (e.g. "M20 bolt comes with nut and washer" split across 3 suppliers). PB schema additions (one-time, run scripts/nss-add-invoice-columns.ts): - order_items.qty_invoiced (number) - order_items.customer_invoice_number (text) - order_items.invoiced_date (date) These three columns were referenced throughout the codebase but never existed on the live schema, so writes were being silently dropped. The schema fix unblocks the reconcile flow that stamps them after every QB webhook returns. Backfill for the live Kusasalethu order: scripts/nss-fix-kusasalethu-inv10924.ts updates order_items.quantity 10→8 / 10→7 to match what was collected, stamps the new columns, and flips order.invoiced=true. New server helper reconcileOrderItemInvoicedStatus() runs after each QB webhook and per-row decides: - item_status = "invoiced" if qty_invoiced >= quantity (fully billed) - item_status = "ready_invoice" otherwise (still partial — stays in the to-invoice tab for a follow-up invoice once the rest arrives) Wired into both the bulk invoicing page and the delivery-note-invoice route. markOrderInvoiced now compares against qty_invoiced vs quantity rather than the flat item_status check, and its error message names the outstanding shortfall per row.
2026-09-17 11:53:36 +00:00
/* eslint-disable no-console */
/**
* scripts/nss-add-invoice-columns.ts
*
* One-shot migration to add the missing `qty_invoiced`,
* `customer_invoice_number`, and `invoiced_date` columns to the
* `order_items` PB collection. The codebase has been writing to these
* fields for months and PB silently dropped every write because the
* columns didn't exist — that bug caused INV10924 on Kusasalethu PO
* 390588 to leave 5 units unaccounted-for. This script adds the
* columns so the reconcile / data-fix scripts can persist the data.
*
* Idempotent: re-running with the columns already present is a no-op
* (the script detects the field by name and skips if it already
* exists).
*
* Usage:
* npx tsx scripts/nss-add-invoice-columns.ts
*/
import PocketBase from "pocketbase";
import { readFileSync, existsSync } from "node:fs";
import { resolve } from "node:path";
for (const file of [".env.local", ".env"]) {
const path = resolve(process.cwd(), file);
if (existsSync(path)) {
const text = readFileSync(path, "utf8");
for (const rawLine of text.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) continue;
const eq = line.indexOf("=");
if (eq <= 0) continue;
const key = line.slice(0, eq).trim();
let value = line.slice(eq + 1).trim();
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
if (!process.env[key]) process.env[key] = value;
}
break;
}
}
const url = process.env.NEXT_PUBLIC_POCKETBASE_URL ?? "http://127.0.0.1:8090";
const email = process.env.PB_ADMIN_EMAIL;
const password = process.env.PB_ADMIN_PASSWORD;
if (!email || !password) {
console.error("PB_ADMIN_EMAIL / PB_ADMIN_PASSWORD are required (set in .env.local).");
process.exit(1);
}
const COLLECTION = "order_items";
const NEW_FIELDS = [
{
name: "qty_invoiced",
type: "number" as const,
required: false,
presentable: false,
options: { min: 0, max: null, noDecimal: true },
},
{
name: "customer_invoice_number",
type: "text" as const,
required: false,
presentable: false,
options: { min: 0, max: 100, pattern: "" },
},
{
name: "invoiced_date",
type: "date" as const,
required: false,
presentable: false,
options: { min: "", max: "" },
},
];
async function main() {
const pb = new PocketBase(url);
pb.autoCancellation(false);
await pb.collection("_superusers").authWithPassword(email, password);
console.log(`Connected to ${url} as ${email}.`);
const collection = await pb.collections.getOne(COLLECTION);
const existingNames = new Set((collection.fields ?? []).map((f) => f.name));
const toAdd = NEW_FIELDS.filter((f) => !existingNames.has(f.name));
if (toAdd.length === 0) {
console.log(
`All 3 columns already present on ${COLLECTION}. No-op.`,
);
return;
}
console.log(
`Adding ${toAdd.length} column(s) to ${COLLECTION}: ${toAdd
.map((f) => f.name)
.join(", ")}`,
);
const nextFields = [
...(collection.fields ?? []),
...toAdd.map((f) => ({
...f,
system: false,
hidden: false,
})),
];
// PB's collection API expects the fields array (NOT `schema` — that's
// the Go API shape). The SDK serialises `fields` straight through.
await pb.collections.update(collection.id, {
fields: nextFields.map((f) => ({
name: f.name,
type: f.type,
required: f.required,
presentable: f.presentable,
hidden: f.hidden,
system: f.system,
options: f.options,
})),
});
// Verify the columns landed.
const refreshed = await pb.collections.getOne(COLLECTION);
const verifiedNames = new Set((refreshed.fields ?? []).map((f) => f.name));
for (const f of toAdd) {
if (!verifiedNames.has(f.name)) {
throw new Error(`Column "${f.name}" did not appear after update — aborting.`);
}
}
console.log(`Verified: ${toAdd.map((f) => f.name).join(", ")} present on ${COLLECTION}.`);
console.log("\nNext steps:");
console.log(` 1. Re-run scripts/nss-fix-kusasalethu-inv10924.ts to back-fill INV10924 on the Kusasalethu PO 390588 lines.`);
console.log(` 2. The new columns are now writable from /api/v1/orders/reconcile-invoice and the DN-invoice route.`);
}
main().catch((err) => {
console.error("FAILED:", err);
process.exit(1);
});