import { test } from "node:test"; import assert from "node:assert/strict"; import { buildSiteSummaries } from "./sites"; import type { Asset, BookedLine, Invoice, Site } from "./data"; const site = (name: string) => ({ id: `s-${name}`, name }) as Site; // Internal = billed to Mega Infrastructure (09MIS001). External = a third-party debtor // number, which is all three of PHI 178/186/192 actually carry. const internal = (over: Partial = {}) => ({ id: `inv-${Math.random()}`, invoice_no: "PHI 166", invoice_date: "2026-09-18", debtor: "Mega Infrastructure", debtor_no: "09MIS001", site: "West Park", total_excl: 100, total_incl: 115, vat: 15, period_to: "2026-09-18", booked_month: 9, booked_year: 2026, ...over }) as Invoice; const external = (over: Partial = {}) => ({ ...internal(over), debtor: "", debtor_no: "09DTE001" }) as Invoice; const line = (invoice: string, over: Partial = {}) => ({ id: `l-${Math.random()}`, invoice, amount_excl: 100, vat: 15, total_incl: 115, booked_asset: "", booked_site: "", machine_code: "CT01", isBooked: true, ...over }) as BookedLine; const summaries = (sites: Site[], invoices: Invoice[], lines: BookedLine[]) => buildSiteSummaries(sites, invoices, lines, [] as Asset[]); test("an internal invoice's site is summarised with its money", () => { const inv = internal(); const [row] = summaries([site("West Park")], [inv], [line(inv.id)]); assert.ok(row, "West Park should have a row"); assert.equal(row.name, "West Park"); assert.equal(row.revenueIncl, 115); assert.equal(row.lineCount, 1); }); test("an external invoice does NOT put its name on the sites page", () => { // The real case: PHI 192's description "Crane Truck Hire 17 September 2026 Move // containers" produced a site literally called "Move containers". An external invoice // is not billed to a place, so its name is the supplier and must not become a site row. const inv = external({ invoice_no: "PHI 192", site: "Move containers" }); const rows = summaries([site("Move containers")], [inv], [line(inv.id)]); assert.deepEqual(rows, [], "an external-only site must not appear on /sites"); }); test("a site with both internal and external lines keeps only the internal money", () => { const internalInv = internal({ site: "Castle Gate" }); const externalInv = external({ invoice_no: "PHI 186", site: "Castle Gate" }); const rows = summaries( [site("Castle Gate")], [internalInv, externalInv], [line(internalInv.id, { total_incl: 115 }), line(externalInv.id, { total_incl: 500 })], ); assert.equal(rows.length, 1); // 500 of external billing must not inflate the site's revenue. assert.equal(rows[0].revenueIncl, 115); assert.equal(rows[0].lineCount, 1); }); test("the sites total cannot silently drop an internal site", () => { // Guards the filter against over-reaching: internal sites must all survive. const invoices = ["West Park", "Cosmo City", "The Hills"].map((s) => internal({ site: s })); const rows = summaries( ["West Park", "Cosmo City", "The Hills"].map(site), invoices, invoices.map((i) => line(i.id)), ); assert.deepEqual(rows.map((r) => r.name).sort(), ["Cosmo City", "The Hills", "West Park"]); });