/* eslint-disable no-console */ /** * scripts/nss-fix-kusasalethu-inv10924.ts * * One-shot data-fix for the Kusasalethu PO 390588 invoice INV10924 * incident. The driver collected 8 of one line and 7 of another (15 * total), but the QuickBooks invoice billed 10 of each (20 total) — 5 * units were over-billed. This script back-fills PB so the row state * agrees with reality: * * - `quantity` on each line is dropped to match what was collected * (10 → 8 on the 0161HDAKWF line, 10 → 7 on the 2161HDAKWF line). * Without this, `quantity - qty_collected` stays positive and the * new "outstanding qty" badge would mis-report the shortfall. * * - `qty_invoiced` is stamped on both rows (= qty_collected). * * - `customer_invoice_number` and `invoiced_date` are stamped so * the operator-facing pages cite INV10924 instead of showing a * blank. * * - `item_status` flips to "invoiced" (matching what the operator * sees in QuickBooks) so the rows drop off the to-invoice tab. * The order's `invoiced` flag is also flipped to true. * * QuickBooks itself is NOT touched — leaving INV10924 as-is and * raising a follow-up invoice once the supplier delivers the rest is * the agreed remediation. If you'd rather credit-note 5 units, do * that in QuickBooks manually and tell me the credit-memo number so * I can stamp `customer_credit_number` (would need a schema add). * * Usage: * npx tsx scripts/nss-fix-kusasalethu-inv10924.ts * * Requires PB_ADMIN_EMAIL / PB_ADMIN_PASSWORD in .env.local (already * present in this repo). */ import PocketBase from "pocketbase"; import { readFileSync, existsSync } from "node:fs"; import { resolve } from "node:path"; // Load .env.local (or .env) so PB_ADMIN_EMAIL / PB_ADMIN_PASSWORD are // populated the same way the rest of the app does. 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 ORDER_ID = "hqj5nhtkezs53ff"; const ITEM_FIXES: Array<{ id: string; description: string; newQuantity: number; qtyJustInvoiced: number; }> = [ { id: "9rqpomdhdhoxr45", description: "TOPTUL 0161HDAKWF 1/2\" X 10 mm Hex Bit Impact", newQuantity: 8, qtyJustInvoiced: 8, }, { id: "ynnvo3csqfq7dyg", description: "TOPTUL 2161HDAKWF 1/2\" x 12 mm Hex Bit Impact", newQuantity: 7, qtyJustInvoiced: 7, }, ]; const INVOICE_NUMBER = "INV10924"; const INVOICE_DATE = "2026-09-16"; const pb = new PocketBase(url); pb.autoCancellation(false); async function main() { await pb.collection("_superusers").authWithPassword(email, password); console.log(`Connected to ${url} as ${email}.`); // 1. Sanity-check the order exists and matches what we expect. const order = await pb.collection("orders").getOne(ORDER_ID); console.log(`\n[order] ${order.id} ${order.order_number} (${order.contact_name})`); console.log(` invoiced=${order.invoiced} status=${order.status || "(empty)"}`); // 2. Update each order_item. for (const fix of ITEM_FIXES) { const before = await pb.collection("order_items").getOne(fix.id); console.log( `\n[item ] ${before.id} ${fix.description}`, `\n quantity ${before.quantity} → ${fix.newQuantity}`, `\n qty_collected ${before.qty_collected} (preserved)`, `\n qty_invoiced ${before.qty_invoiced ?? "(unset)"} → ${fix.qtyJustInvoiced}`, `\n item_status ${before.item_status} → "invoiced"`, `\n customer_invoice_number ${before.customer_invoice_number ?? "(unset)"} → ${INVOICE_NUMBER}`, `\n invoiced_date ${before.invoiced_date ?? "(unset)"} → ${INVOICE_DATE}`, ); await pb.collection("order_items").update(fix.id, { quantity: fix.newQuantity, qty_invoiced: fix.qtyJustInvoiced, customer_invoice_number: INVOICE_NUMBER, invoiced_date: INVOICE_DATE, item_status: "invoiced", }); } // 3. Flip the parent order's invoiced flag. if (!order.invoiced) { console.log( `\n[order ] ${order.id} flipping invoiced → true (both lines are fully invoiced now)`, ); await pb.collection("orders").update(ORDER_ID, { invoiced: true }); } else { console.log(`\n[order ] ${order.id} already invoiced — leaving alone.`); } // 4. Audit summary so the operator can sanity-check what changed. console.log(`\n--- Final state ---`); const finalOrder = await pb.collection("orders").getOne(ORDER_ID); console.log(`order.invoiced = ${finalOrder.invoiced}`); for (const fix of ITEM_FIXES) { const after = await pb.collection("order_items").getOne(fix.id); console.log( `${fix.id}: quantity=${after.quantity}, qty_collected=${after.qty_collected}, qty_invoiced=${after.qty_invoiced}, item_status=${after.item_status}, customer_invoice_number=${after.customer_invoice_number}, invoiced_date=${after.invoiced_date}`, ); } console.log(`\nDone. Next steps:`); console.log(` - If QuickBooks INV10924 stays as-is: nothing else needed. The next driver pickup of the`); console.log(` outstanding 5 units (2 of 0161HDAKWF + 3 of 2161HDAKWF) will re-surface these lines in`); console.log(` the Invoicing page and a follow-up invoice goes out.`); console.log(` - If you'd rather credit-note the 5 units: do that in QuickBooks manually, then either`); console.log(` re-run this script with the credit-memo number (would need a schema add) or leave PB alone`); console.log(` and treat PB as the audit record of what was physically delivered.`); } main().catch((err) => { console.error("FAILED:", err); process.exit(1); });