/** * READ-ONLY audit: quotes whose `company` relation disagrees with * their customer's `company`. * * `quotes.company` is a denormalised copy of `customer.company` that * PocketBase does not re-cascade, so any quote whose customer was * swapped after creation is now inconsistent. This script only READS * and prints — it makes no writes. * * Run: npx tsx scripts/audit-quote-company-drift.mjs */ import PocketBase from "pocketbase"; const PB_URL = process.env.PB_URL ?? "https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io"; const PB_ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL ?? "david.els1@outlook.com"; const PB_ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD ?? "1234567890"; const pb = new PocketBase(PB_URL); await pb.collection("_superusers").authWithPassword(PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD); const quotes = await pb.collection("quotes").getFullList(200, { expand: "customer,customer.company,company", sort: "-created", }); // Company records store their display name in `company`, not `name`. const nameOf = (expand, key) => expand?.[key]?.company ?? null; // When an expand doesn't resolve, show the raw relation id rather than a // bare "(none)" — otherwise "unresolved" and "genuinely empty" look identical. const label = (name, id) => (name ? name : id ? `` : "(none)"); let checked = 0; let consistent = 0; let noCustomerCompany = 0; const drifted = []; for (const q of quotes) { if (!q.customer) continue; checked++; const cust = q.expand?.customer; const custCompanyId = cust?.company ?? ""; const custCompanyName = nameOf(cust?.expand, "company"); if (!custCompanyId) { noCustomerCompany++; if (q.company) { drifted.push({ n: q.quote_number, id: q.id, status: q.status, quoteCompany: label(nameOf(q.expand, "company"), q.company), custCompany: "(customer has none)", created: (q.created ?? "").slice(0, 10), }); } continue; } if (q.company === custCompanyId) { consistent++; } else { drifted.push({ n: q.quote_number, id: q.id, status: q.status, quoteCompany: label(nameOf(q.expand, "company"), q.company), custCompany: custCompanyName ? label(custCompanyName, custCompanyId) : label(null, custCompanyId), created: (q.created ?? "").slice(0, 10), }); } } console.log(`Scanned : ${quotes.length} quotes`); console.log(`With customer: ${checked}`); console.log(`Consistent : ${consistent}`); console.log(`No cust co : ${noCustomerCompany} (customer has no company)`); console.log(`DRIFTED : ${drifted.length}`); console.log(""); if (drifted.length) { console.log("quote created status quote.company -> customer.company"); console.log("-".repeat(84)); for (const d of drifted.slice(0, 60)) { console.log( `${String(d.n).padEnd(10)} ${d.created} ${String(d.status).padEnd(18)} ${d.quoteCompany} -> ${d.custCompany}`, ); } if (drifted.length > 60) console.log(`… and ${drifted.length - 60} more`); }