// Part D + Part B.2 — verify term 2 (insurance) reconciliation AND overheads roll-up. // Run: node --env-file=.env.local scripts/verify-cost-reconciliation.mjs // // Part A.1: load .env.local explicitly, wrap in try/catch, exit 1 on any failure. // The script must NEVER claim success unless every check actually passed. import PocketBase from "pocketbase"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; // Load .env.local (Round 4 Part A.1) const __dirname = dirname(fileURLToPath(import.meta.url)); try { const env = readFileSync(resolve(__dirname, "..", ".env.local"), "utf8"); for (const line of env.split("\n")) { const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.+?)\s*$/); if (m && !process.env[m[1]]) process.env[m[1]] = m[2]; } } catch { // .env.local optional; env may already be set by --env-file flag } const PB = process.env.NEXT_PUBLIC_PB_URL; const EMAIL = process.env.PB_ADMIN; const PASS = process.env.PB_PASS; if (!PB || !EMAIL || !PASS) { console.error("✗ Missing required env (NEXT_PUBLIC_PB_URL, PB_ADMIN, PB_PASS). Copy .env.example to .env.local."); process.exit(1); } let exitCode = 0; try { await main(); } catch (e) { console.error(`✗ Script failed: ${e?.stack || e?.message || e}`); exitCode = 1; } process.exit(exitCode); async function main() { const pb = new PocketBase(PB); pb.autoCancellation(false); await pb.collection("_superusers").authWithPassword(EMAIL, PASS); const financed = ["TLB06", "TLB07", "MG02", "RV04", "TB01"]; let exitCode = 0; console.log("=== Part D — fixed_expense reconstruction ==="); console.log("Hypothesis: fixed_expenses = instalment + insurance + term3"); console.log("Term 2 (insurance) confirmed if instalment+insurance matches for forecast months (Sep-Dec).\n"); for (const code of financed) { const assets = await pb.collection("assets").getFullList({ filter: `code = '${code}'` }); if (!assets.length) { console.log(` ! ${code}: asset not found`); continue; } const a = assets[0]; const cl = await pb.collection("cost_lines").getFullList({ filter: `asset = '${a.id}'` }); const inc = await pb.collection("monthly_income").getFullList({ filter: `asset = '${a.id}'` }); let actualMatch = 0, forecastMatch = 0; let actualDiffs = []; for (const r of inc) { if (!r.fixed_expenses) continue; const parts = cl.filter((c) => c.month === r.month && c.year === r.year); const isForecast = parts.length === 1 && parts[0].kind === "instalment"; if (parts.length === 0) continue; const expected = parts.reduce((s, p) => s + p.amount, 0); const diff = r.fixed_expenses - expected; if (Math.abs(diff) < 0.5) { if (isForecast) forecastMatch++; else actualMatch++; } else { actualDiffs.push({ month: r.month, expected: expected.toFixed(2), actual: r.fixed_expenses.toFixed(2), diff: diff.toFixed(2) }); } } const kinds = [...new Set(cl.map((c) => c.kind))].sort().join("+"); const status = actualDiffs.length === 0 ? "✓" : "✗"; console.log(` ${status} ${code} (${kinds}):`); console.log(` forecast (Sep-Dec): ${forecastMatch} match`); console.log(` actuals (Jan-Aug): ${actualMatch}/${actualMatch + actualDiffs.length} match`); if (actualDiffs.length > 0) { const avg = actualDiffs.reduce((s, d) => s + parseFloat(d.diff), 0) / actualDiffs.length; const min = Math.min(...actualDiffs.map((d) => parseFloat(d.diff))); const max = Math.max(...actualDiffs.map((d) => parseFloat(d.diff))); console.log(` term 3 ranges: avg ${avg.toFixed(2)}, min ${min.toFixed(2)}, max ${max.toFixed(2)}`); } } console.log("\n=== Part B.2 — Overheads Roll-up reconciliation ==="); const overheads = await pb.collection("overheads").getFullList(); const bySection = {}; for (const o of overheads) { bySection[o.section] = (bySection[o.section] || 0) + (o.amount_incl || 0); } const rollupTotal = Object.values(bySection).reduce((s, v) => s + v, 0); // Round 4 Part A.4: corrected target const WORKBOOK_GRAND_TOTAL = 1110486.87; // Cost sheet row 98 const PLANT_INSTALMENTS = 415523.94; // "Plant Overheads" section (correctly excluded) const TARGET_ROLLUP = WORKBOOK_GRAND_TOTAL - PLANT_INSTALMENTS; // R 694,962.93 // Two known reconciling items (Round 4 Part A.4): const KNOWN_PERSONNEL_OVERSTATEMENT = 19800.00; // workbook's own Personnel total R 412,000 vs R 392,200 line items const KNOWN_SANTAM_DELTA = -5322.44; // app is correct; workbook subtotal skips Santam console.log(`Workbook grand total (Cost row 98): R ${WORKBOOK_GRAND_TOTAL.toFixed(2)}`); console.log(`Target (workbook grand total - Plant instalments): R ${TARGET_ROLLUP.toFixed(2)}`); console.log(""); console.log("Per-section rollup (Plant Overheads already excluded):"); for (const [s, t] of Object.entries(bySection)) { console.log(` ${s.padEnd(22)} R ${t.toFixed(2).padStart(12)}`); } console.log(` ${"TOTAL".padEnd(22)} R ${rollupTotal.toFixed(2).padStart(12)}`); const driftTarget = Math.abs(rollupTotal - TARGET_ROLLUP); const explainedDrift = (TARGET_ROLLUP - rollupTotal) - (KNOWN_PERSONNEL_OVERSTATEMENT + KNOWN_SANTAM_DELTA); console.log(""); console.log(`Drift (vs target): R ${driftTarget.toFixed(2)}`); console.log(` = Personnel overstatement (workbook error): R +${KNOWN_PERSONNEL_OVERSTATEMENT.toFixed(2)}`); console.log(` + Santam delta (app is right; workbook wrong): R ${KNOWN_SANTAM_DELTA.toFixed(2)}`); console.log(` = Unexplained: R ${explainedDrift.toFixed(2)}`); if (Math.abs(explainedDrift) > 1) { console.log(`\n✗ Roll-up drift beyond the two known reconciling items. Investigate.`); exitCode = 2; } else { console.log(`\n✓ Roll-up reconciles to target within R 1.00, with both known deltas itemised.`); } console.log("\n=== Part B.4 — Vehicle Costs subtotal ==="); const vehicleRows = (bySection["Vehicle Costs"] || 0); // Round 4 Part A.2: app deliberately INCLUDES Santam (R 5,322.44) so Vehicle Costs = R 14,996.31 // matching the workbook's stated subtotal. Workbook row 50's SUM skips Santam — that is the workbook's error. console.log(`App Vehicle Costs total: R ${vehicleRows.toFixed(2)} (includes Santam per A.2)`); console.log(` Workbook subtotal (row 50) = R 9,673.87 (incorrectly skips Santam)`); console.log(` Workbook row-by-row sum = R 14,996.31 (correct)`); console.log("\n=== Known discrepancies — flagged, NOT silently fixed ==="); console.log(" • Personnel: workbook's own total R 412,000, but R 157,200 + R 235,000 = R 392,200. Diff R 19,800."); console.log(" • Subscriptions: 2 rows have broken VAT (Charl - CCS incl