89 lines
3 KiB
JavaScript
89 lines
3 KiB
JavaScript
|
|
/**
|
||
|
|
* 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 ? `<id ${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`);
|
||
|
|
}
|