/* 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); });