#!/usr/bin/env node /** * NUMid recycle audit. * * Prints which portal NUMids (customer_legacy_refs, utilities/DATGRAL) are dead * enough to hand to a new customer, and which are merely quiet. Read-only: it * writes nothing and reassigns nothing. * * node scripts/numid-audit.mjs # summary + both candidate tiers * node scripts/numid-audit.mjs --csv # full per-NUMid table on stdout * node scripts/numid-audit.mjs --numid 501 * * Needs DATABASE_URL. Run it against PROD before acting on anything: the tiers * describe whatever database it is pointed at, and a stale copy will happily * report a NUMid as empty that prod has been billing all year. */ import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; // Relative, not "@jorgecuadros/database": the workspace link is not always // present at the repo root, and this script has to run from a bare checkout and // from inside the API container alike. import pkg from "../packages/database/generated/client/index.js"; const { PrismaClient } = pkg; const HERE = dirname(fileURLToPath(import.meta.url)); /** * Anything here means the id carries history a new owner would inherit. * Counted, not sampled: a single row in any of them disqualifies. */ const HISTORY_COLUMNS = [ "veh", "trust", "stmt", "ocr", "enl", "elog", "ash", "nopago", ]; const n = (v) => (v == null ? 0 : Number(v)); const hasHistory = (r) => HISTORY_COLUMNS.some((c) => n(r[c]) > 0); const zeroBalance = (r) => n(r.balMxn) === 0 && n(r.balUsd) === 0; /** * EMPTY — the id was created and never used. Safe for an allocator to take * without a human looking, subject to the legacy check below. * * "Never transacted" means zero movements once the synthetic opening-balance row * is removed; that row exists for all 1,171 NUMids and is not evidence of use. * Services are checked as anySvc rather than activeSvc, because a deactivated * water account still says a person once lived behind this id. */ const isEmpty = (r) => n(r.realTx) === 0 && zeroBalance(r) && n(r.anySvc) === 0 && n(r.anyPol) === 0 && n(r.insRef) === 0 && n(r.hasEmail) === 0 && !hasHistory(r); /** * DORMANT — used once, quiet for years, owes nothing. NOT auto-allocatable: * a returning snowbird is indistinguishable from an abandoned account here. */ const isDormant = (r) => !isEmpty(r) && n(r.realTx36m) === 0 && zeroBalance(r) && n(r.activeSvc) === 0 && n(r.activePol) === 0 && !hasHistory(r); function line(r) { return ( ` ${String(r.numid).padStart(5)} ${(r.name || "(sin nombre)").slice(0, 30).padEnd(30)}` + ` last=${(r.lastRealTx ? new Date(r.lastRealTx).toISOString().slice(0, 10) : "never").padStart(10)}` + ` tx=${String(n(r.realTx)).padStart(3)}` + ` bal=${n(r.balMxn).toFixed(2).padStart(10)}` + ` svc=${n(r.anySvc)}` + ` pol=${n(r.anyPol)}` ); } async function main() { const args = process.argv.slice(2); const prisma = new PrismaClient(); try { const sql = readFileSync(join(HERE, "numid-audit.sql"), "utf8"); const rows = await prisma.$queryRawUnsafe(sql); const one = args.indexOf("--numid"); if (one !== -1) { const want = Number(args[one + 1]); const r = rows.find((x) => Number(x.numid) === want); if (!r) { console.log(`NUMid ${want} is not in the utilities/DATGRAL pool.`); return; } console.log(JSON.stringify(r, (_k, v) => (typeof v === "bigint" ? Number(v) : v), 2)); console.log( `\nverdict: ${isEmpty(r) ? "EMPTY" : isDormant(r) ? "DORMANT" : "IN USE"}`, ); return; } if (args.includes("--csv")) { const cols = Object.keys(rows[0]); console.log(cols.join(",")); for (const r of rows) { console.log(cols.map((c) => JSON.stringify(r[c] ?? "")).join(",")); } return; } const empty = rows.filter(isEmpty); const dormant = rows.filter(isDormant); const max = rows.reduce((m, r) => Math.max(m, Number(r.numid)), 0); console.log(`pool: ${rows.length} NUMids, max ${max}`); console.log(` EMPTY (never used, auto-allocatable): ${empty.length}`); console.log(` DORMANT (quiet, needs a human): ${dormant.length}`); console.log(` IN USE: ${rows.length - empty.length - dormant.length}`); console.log("\nEMPTY"); empty.forEach((r) => console.log(line(r))); console.log("\nDORMANT"); dormant.forEach((r) => console.log(line(r))); console.log( "\nNOTE: this audit sees the platform only. Every NUMid here also exists in\n" + "Access, and freakma republishes DATGRAL in full on each export, so an id\n" + "reassigned here comes back under its old owner unless it is removed at the\n" + "source or the NUMid is routed to the platform. Confirm against prod\n" + "datosfreak before reassigning.", ); } finally { await prisma.$disconnect(); } } main().catch((err) => { console.error(err); process.exit(1); });