67 lines
3.2 KiB
TypeScript
67 lines
3.2 KiB
TypeScript
|
|
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<Invoice> = {}) =>
|
||
|
|
({ 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<Invoice> = {}) =>
|
||
|
|
({ ...internal(over), debtor: "", debtor_no: "09DTE001" }) as Invoice;
|
||
|
|
|
||
|
|
const line = (invoice: string, over: Partial<BookedLine> = {}) =>
|
||
|
|
({ 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"]);
|
||
|
|
});
|