import { describe, it, expect } from "vitest"; import type { CategoryDelta } from "../../shared/types"; import { computeResults, sumLeaves } from "./compareResults"; // Minimal leaf factory. Amounts arrive from COMPARE_DELTA_SQL already in the // per-type sign convention (expenses = positive ABS, income/transfer = signed). function leaf( type: "expense" | "income" | "transfer", mc: number, mp: number, cc = mc, cp = mp, ): CategoryDelta { return { categoryId: 1, categoryName: type, categoryColor: "#000", previousAmount: mp, currentAmount: mc, deltaAbs: mc - mp, deltaPct: mp !== 0 ? ((mc - mp) / mp) * 100 : null, cumulativePreviousAmount: cp, cumulativeCurrentAmount: cc, cumulativeDeltaAbs: cc - cp, cumulativeDeltaPct: cp !== 0 ? ((cc - cp) / cp) * 100 : null, category_type: type, is_parent: false, }; } describe("computeResults — income-statement roll-up (Issue #253)", () => { it("resultBefore = revenues − expenses; resultNet adds net transfers", () => { const r = computeResults([ leaf("income", 4350, 4500), leaf("expense", 3900, 3700), leaf("transfer", 0, 0), ]); // Operating result: 4350 − 3900 = 450 current; 4500 − 3700 = 800 previous. expect(r.resultBefore.monthCurrent).toBe(450); expect(r.resultBefore.monthPrevious).toBe(800); expect(r.resultBefore.monthDelta).toBe(-350); // Balanced transfer (0) → net equals the operating result. expect(r.resultNet.monthCurrent).toBe(450); expect(r.hasTransfers).toBe(true); }); it("an unbalanced (one-leg) transfer moves resultNet away from resultBefore", () => { const r = computeResults([ leaf("income", 4000, 4000), leaf("expense", 3000, 3000), leaf("transfer", -200, 0), // a single categorised leg ]); expect(r.resultBefore.monthCurrent).toBe(1000); // Net folds the residual transfer in: 1000 + (−200) = 800. expect(r.resultNet.monthCurrent).toBe(800); }); it("a deficit yields a negative net result and no transfer line", () => { const r = computeResults([leaf("income", 2000, 2000), leaf("expense", 2500, 2400)]); expect(r.resultNet.monthCurrent).toBe(-500); expect(r.hasTransfers).toBe(false); }); it("ignores subtotal (is_parent) rows so a group is not double-counted", () => { const parent = { ...leaf("expense", 999, 999), is_parent: true }; const r = computeResults([parent, leaf("expense", 100, 80)]); expect(r.expense.monthCurrent).toBe(100); }); it("carries the YTD figures through the same arithmetic", () => { const r = computeResults([ leaf("income", 100, 100, 1200, 1150), leaf("expense", 60, 60, 700, 650), ]); expect(r.resultBefore.ytdCurrent).toBe(500); expect(r.resultBefore.ytdPrevious).toBe(500); }); it("sumLeaves adds only leaves", () => { const t = sumLeaves([ leaf("expense", 100, 50), { ...leaf("expense", 999, 999), is_parent: true }, ]); expect(t.monthCurrent).toBe(100); expect(t.monthPrevious).toBe(50); }); });