162 lines
6.4 KiB
TypeScript
162 lines
6.4 KiB
TypeScript
|
|
/* 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);
|
||
|
|
});
|