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.
226 lines
8.1 KiB
TypeScript
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);
|
|
});
|