mega-plant/lib/running-month.test.ts
Dave 72900878ba fix(running): charge TLB06 for the months it was live, and not for November
TLB06 was paid off in October 2026, but the running-cost rule dropped a SETTLED
agreement from EVERY month, so the ten instalments it was really paying
(January-October, R290 360,80) vanished from the year to date.

An agreement is now charged for the months it was live, and its paid-off date
decides when the money stopped:

- LIVE: from its start through its paid-off date, or through the current month
  when no paid-off date is on record.
- SETTLED: up to its paid-off date - those instalments really were paid - and
  nothing at all when there is no paid-off date, because there is then no month
  we can say it paid for. "Settled" means paid off, or will be paid off by
  that date, so a settled agreement whose date is still ahead is still charged.

TLB06 is charged January-October and NOT November. Nothing else moves: every
other agreement's window already ran to November or beyond.

- Overview YTD R6 241 894,70 -> R6 532 255,50. The November figure is
  unchanged at R441 493,12.
- Financing register YTD instalments R4 395 038,56 -> R4 685 399,36, and
  "Committed for November" stays R399 548,96.

The Overview and the register still read ONE rule, so their finance year to
date is the same figure - asserted in lib/running-month.test.ts, together with
the TLB06 case and the live-agreement cases.

committedMonthly() in lib/financing.ts was never called and stated the old
rule, so it is gone rather than left lying next to the real one.
2026-11-05 01:56:26 +02:00

226 lines
8.1 KiB
TypeScript

// 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);
});