// The month rules on the running-cost pages. // // Run: node --import tsx --test lib/running-month.test.ts // // Two rules that keep three screens from disagreeing: // // 1. A year-to-date figure counts the months that have HAPPENED. A premium or an // instalment for a month that has not happened yet is a rate on file, not money the // client has spent, so it is not part of the year to date. // // 2. The Overview and the financing register read the SAME finance rule, so the // fleet's finance for a month is the same rand on both. // // A third one is checked here because it is the whole point of the exercise: the // insurance page and the Overview must quote the SAME year to date for insurance. import { test } from "node:test"; import assert from "node:assert/strict"; import { elapsedMonths } from "./income-month"; import { buildInsuranceRows, buildRunningRows } from "./rows"; import { financeByAsset, fleetFinanceByMonth, machineRunningCosts } from "./running-costs"; const asset = (id: string, code = "TLB01") => ({ id, code, name: code, category: "Plant", type: "", gl_account: "", supplier: "", monthly_excl: 0, monthly_incl: 0, purchase_value: 0, is_provisional: false, is_active: true, }) as any; const rate = (assetId: string, from: number, to: number, amount: number) => ({ id: `r-${assetId}-${from}`, asset: assetId, year: 2026, month_from: from, month_to: to, rate: amount, }) as any; const term = ( assetId: string, installment: number, settled = false, end: [number, number] = [2026, 12], ) => ({ id: `t-${assetId}`, asset: assetId, installment, is_settled: settled, start_year: 2026, start_month: 1, end_year: end[0], end_month: end[1], }) as any; /** Rands to the cent — the monthly figures are floats and must not be compared exactly. */ const cents = (n: number) => Math.round(n * 100) / 100; test("elapsed months count the year as it stands", () => { assert.equal(elapsedMonths(2026, new Date("2026-11-05T09:00:00")), 11); assert.equal(elapsedMonths(2026, new Date("2026-01-15T09:00:00")), 1); // A year that is over is a whole year. assert.equal(elapsedMonths(2026, new Date("2027-03-01T09:00:00")), 12); }); test("the insurance YTD stops at the month that has happened", () => { const rates = [rate("a1", 1, 0, 1300)]; const full = buildInsuranceRows([asset("a1")], rates); const toNovember = buildInsuranceRows([asset("a1")], rates, 11); assert.equal(full[0].ytd, 1300 * 12, "a whole year, when the whole year is the window"); assert.equal(toNovember[0].ytd, 1300 * 11, "Jan-Nov only"); assert.equal(toNovember[0].chargedMonths, 11, "the months the YTD actually sums"); assert.equal(toNovember[0].coveredMonths, 12, "the year's coverage is still reported in full"); assert.equal(toNovember[0].months[10], 1300, "November still carries its premium"); }); test("a month with no premium on record is unknown, not free", () => { // Insured from June only: January-May are unknown, and the YTD says so. const rows = buildInsuranceRows([asset("a1")], [rate("a1", 6, 0, 1300)], 11); assert.equal(rows[0].months[0], null, "January has no premium on record"); assert.equal(rows[0].ytd, 1300 * 6, "June to November, and not one rand less"); assert.equal(rows[0].chargedMonths, 6); }); test("a live agreement runs to the current month; a settled one with no paid-off date costs nothing", () => { const months = fleetFinanceByMonth( // The settled one has NO paid-off date: we cannot say when it stopped, so it is // charged for nothing at all. [term("a1", 1000), term("a2", 500, true, [0, 0])], 11, ); assert.equal(months[0], 1000, "January: the live agreement only"); assert.equal(months[10], 1000, "November: still the live agreement"); assert.equal(months[11], 0, "a month past the horizon is not charged"); assert.equal( months.slice(0, 11).reduce((s, v) => s + v, 0), 11 * 1000, "the year's instalments", ); }); test("a settled agreement whose paid-off date is still ahead is still paying", () => { // "Settled" means paid off, OR will be paid off by the paid-off date — so the money // is still moving until then. const months = fleetFinanceByMonth([term("a2", 500, true, [2026, 12])], 11); assert.equal(months[10], 500, "November is still inside its window"); }); test("an agreement paid off in October is charged to October and NOT to November", () => { // TLB06: settled, paid off in October 2026. The instalments it was paying in // January-October are real spend and belong in the year; November is not its month. const tlb06 = { id: "t-tlb06", asset: "a1", installment: 29036.08, is_settled: true, start_year: 2023, start_month: 9, end_year: 2026, end_month: 10, } as any; const months = fleetFinanceByMonth([tlb06], 11); assert.equal(months[0], 29036.08, "January is charged"); assert.equal(months[8], 29036.08, "September is charged"); assert.equal(months[9], 29036.08, "October is charged"); assert.equal(months[10], 0, "NOVEMBER IS NOT — the agreement ended in October"); assert.equal( cents(months.slice(0, 11).reduce((s, v) => s + v, 0)), cents(29036.08 * 10), "ten months of instalments, not eleven", ); }); test("a live agreement stops at its own paid-off date", () => { // RV04: paid off in November 2026, so it IS charged for November. const rv04 = { id: "t-rv04", asset: "a1", installment: 28641, is_settled: false, start_year: 2024, start_month: 1, end_year: 2026, end_month: 11, } as any; const months = fleetFinanceByMonth([rv04], 11); assert.equal(months[10], 28641, "November is its last month"); assert.equal( cents(months.slice(0, 11).reduce((s, v) => s + v, 0)), cents(28641 * 11), "charged for every month of the year it was live", ); }); test("the register and the Overview read the same finance rule", () => { const terms = [term("a1", 1000)]; const perAsset = financeByAsset(terms, 11); assert.equal(perAsset.get("a1")?.[0], 1000); assert.equal(perAsset.get("a1")?.[10], 1000); // The fleet total is the sum of the per-asset months — never a second rule. assert.equal(fleetFinanceByMonth(terms, 11)[10], 1000); }); test("the Overview's finance year to date IS the financing register's year to date", () => { const assets = [asset("a1"), asset("a2", "TLB06")]; const terms = [ { id: "t-tlb06", asset: "a2", installment: 29036.08, is_settled: true, start_year: 2023, start_month: 9, end_year: 2026, end_month: 10, } as any, { id: "t-rv04", asset: "a1", installment: 28641, is_settled: false, start_year: 2024, start_month: 1, end_year: 2026, end_month: 11, } as any, ]; const { byAsset } = machineRunningCosts( { assets, repairs: [], purchases: [], purchaseLines: [], insurance: [], finance: terms }, 11, ); const running = buildRunningRows(assets, byAsset); const overviewYtd = running.reduce((s, r) => s + r.byKindYtd.finance, 0); const registerYtd = fleetFinanceByMonth(terms, 11).slice(0, 11).reduce((s, v) => s + v, 0); assert.equal(cents(overviewYtd), cents(registerYtd), "the two screens must quote the same rands"); assert.equal(cents(overviewYtd), cents(29036.08 * 10 + 28641 * 11)); }); test("the insurance page and the Overview quote the same year to date", () => { const assets = [asset("a1"), asset("a2", "TLB02")]; const rates = [rate("a1", 1, 0, 1300), rate("a2", 1, 6, 900), rate("a2", 7, 0, 1000)]; const insuranceRows = buildInsuranceRows(assets, rates, 11); const { byAsset } = machineRunningCosts( { assets, repairs: [], purchases: [], purchaseLines: [], insurance: rates, finance: [] }, 11, ); const running = buildRunningRows(assets, byAsset); const insuranceYtd = insuranceRows.reduce((s, r) => s + r.ytd, 0); const overviewYtd = running.reduce((s, r) => s + r.byKindYtd.insurance, 0); assert.equal(insuranceYtd, overviewYtd, "the two screens must not be a month apart"); assert.equal(insuranceYtd, 1300 * 11 + 900 * 6 + 1000 * 5); });