/* eslint-disable no-console */ /** * scripts/nss-backfill-in-stock-qty.ts * * One-shot backfill for in_stock=true order_items rows whose * `qty_collected` was never stamped to match the customer quantity. * * The order process page's "in stock" toggle now stamps qty_collected * = quantity automatically (so the Invoicing page can bill those * rows at the customer-facing qty), but rows that were marked * in_stock before that fix shipped will still have qty_collected=0 * — the Invoicing page reads min(qty_collected, quantity), so the * invoice line would land at 0 and the operator can't invoice the * customer. * * This script: * 1. Finds every order_item with in_stock=true AND * qty_collected < quantity. * 2. Stamps qty_collected = quantity. * 3. Reports a count of rows touched. * * Idempotent — re-running is a no-op once all rows are aligned. * * Usage: * npx tsx scripts/nss-backfill-in-stock-qty.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 pb = new PocketBase(url); pb.autoCancellation(false); async function main() { await pb.collection("_superusers").authWithPassword(email, password); console.log(`Connected to ${url} as ${email}.`); // Pull every order_item with in_stock=true. We filter in JS rather // than PB because qty_collected < quantity isn't trivially expressible // in PB filter syntax (would need to subtract via JS anyway). const all = await pb.collection("order_items").getFullList({ filter: "in_stock = true", fields: "id,description,quantity,qty_collected,item_status,customer,order", batch: 500, }); const stale = all.filter((it) => { const q = Number(it.quantity ?? 0); const qc = Number(it.qty_collected ?? 0); return q > 0 && qc < q; }); console.log( `\nScanned ${all.length} in_stock rows; ${stale.length} have qty_collected < quantity and need a backfill.`, ); if (stale.length === 0) { console.log("Nothing to do — all in_stock rows already have qty_collected = quantity."); return; } let updated = 0; for (const row of stale) { const q = Number(row.quantity ?? 0); try { await pb.collection("order_items").update(row.id, { qty_collected: q, }); updated += 1; console.log(` ✓ ${row.id} qty=${q} → qty_collected=${q} (${String(row.description).slice(0, 50)})`); } catch (err) { console.error(` ✗ ${row.id} failed:`, err instanceof Error ? err.message : err); } } console.log(`\nDone. Stamped qty_collected = quantity on ${updated} rows.`); } main().catch((err) => { console.error("FAILED:", err); process.exit(1); });