109 lines
3.8 KiB
TypeScript
109 lines
3.8 KiB
TypeScript
|
|
// Regression tests for buildIncomeRows month aggregation.
|
||
|
|
//
|
||
|
|
// Run: node --import tsx --test lib/rows.test.ts
|
||
|
|
//
|
||
|
|
// Background: a machine can legitimately have SEVERAL income rows in one month — the
|
||
|
|
// workbook row plus one row per invoice line booked to it (one machine hired to two sites
|
||
|
|
// in a month produces two invoice rows). buildIncomeRows used to assign into months[i],
|
||
|
|
// so the last row read won and the others vanished from the month view while still being
|
||
|
|
// counted in the YTD total. SS01 September showed R20,700 when it was really R35,305.
|
||
|
|
import { test } from "node:test";
|
||
|
|
import assert from "node:assert/strict";
|
||
|
|
import { buildIncomeRows } from "./rows";
|
||
|
|
|
||
|
|
const asset = (id: string, code = "SS01") => ({
|
||
|
|
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 inc = (assetId: string, month: number, income: number, forecast = false) =>
|
||
|
|
({
|
||
|
|
id: `${assetId}-${month}-${income}`,
|
||
|
|
asset: assetId,
|
||
|
|
year: 2026,
|
||
|
|
month,
|
||
|
|
income,
|
||
|
|
fixed_expenses: 0,
|
||
|
|
is_forecast: forecast,
|
||
|
|
}) as any;
|
||
|
|
|
||
|
|
test("two invoice rows in one month are summed, not overwritten", () => {
|
||
|
|
// The real SS01 September 2026 case: PHI 167 + PHI 171 booked to the same machine.
|
||
|
|
const rows = buildIncomeRows(
|
||
|
|
[asset("a1")],
|
||
|
|
[inc("a1", 9, 14605), inc("a1", 9, 20700)],
|
||
|
|
);
|
||
|
|
assert.equal(rows.length, 1);
|
||
|
|
const r = rows[0];
|
||
|
|
assert.equal(r.months[8], 35305, "September cell must be both invoices");
|
||
|
|
assert.equal(r.income, 35305, "YTD total must agree with the month cell");
|
||
|
|
// The invariant that was broken: the cells must add up to the headline.
|
||
|
|
const sumOfMonths = r.months.reduce((s: number, v: any) => s + (v ?? 0), 0);
|
||
|
|
assert.equal(sumOfMonths, r.income, "month cells must sum to YTD income");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("workbook row plus invoice rows in the same month all count", () => {
|
||
|
|
const rows = buildIncomeRows([asset("a1")], [inc("a1", 9, 0), inc("a1", 9, 14605), inc("a1", 9, 20700)]);
|
||
|
|
assert.equal(rows[0].months[8], 35305);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("months with no actual data stay null", () => {
|
||
|
|
// getActualMonthlyIncome() filters forecast rows out before this point, so the guard
|
||
|
|
// here is simply "no row for that month -> render a dash".
|
||
|
|
const rows = buildIncomeRows([asset("a1")], [inc("a1", 9, 14605)]);
|
||
|
|
assert.equal(rows[0].months[8], 14605);
|
||
|
|
assert.equal(rows[0].months[9], null, "a month with no actual row must render as null");
|
||
|
|
assert.equal(rows[0].months[11], null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("per-month profit uses the accumulated month, not the last row", () => {
|
||
|
|
// Default: profit = income - fixed. Both invoices must be in the month's profit.
|
||
|
|
const rows = buildIncomeRows([asset("a1")], [inc("a1", 9, 14605), inc("a1", 9, 20700)]);
|
||
|
|
assert.equal(rows[0].profit[8], 35305);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("multi-site machine attributes the VAT across both invoices", () => {
|
||
|
|
const line = (id: string, amount: number, vat: number) =>
|
||
|
|
({
|
||
|
|
id,
|
||
|
|
invoice: "i1",
|
||
|
|
line_no: 1,
|
||
|
|
machine_code: "SS-01-CASE",
|
||
|
|
asset_code: "SS01",
|
||
|
|
description: "430 Skidsteer",
|
||
|
|
qty: 1,
|
||
|
|
unit: "Hr",
|
||
|
|
rate: amount,
|
||
|
|
rate_type: "Dry Rate",
|
||
|
|
amount_excl: amount - vat,
|
||
|
|
vat,
|
||
|
|
total_incl: amount,
|
||
|
|
booked_asset: "a1",
|
||
|
|
booked_month: 9,
|
||
|
|
booked_year: 2026,
|
||
|
|
}) as any;
|
||
|
|
|
||
|
|
const rows = buildIncomeRows(
|
||
|
|
[asset("a1")],
|
||
|
|
[inc("a1", 9, 14605), inc("a1", 9, 20700)],
|
||
|
|
[],
|
||
|
|
{},
|
||
|
|
[line("l1", 14605, 1905), line("l2", 20700, 2700)],
|
||
|
|
);
|
||
|
|
|
||
|
|
const r = rows[0];
|
||
|
|
assert.equal(r.invoiceIncome, 35305, "combined invoice total for the vehicle");
|
||
|
|
assert.equal(r.invoiceVat, 4605, "VAT inside that total, across both invoices");
|
||
|
|
assert.equal(r.invoiceIncome, r.months[8], "invoice total matches the September cell");
|
||
|
|
});
|