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