Files
SkyMoney/web/tests/allocatorPreview.test.ts

120 lines
4.9 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { previewAllocation, type FixedPlan, type VariableCategory } from "../src/utils/allocatorPreview";
const cats = (defs: Array<Partial<VariableCategory> & { name: string }>): VariableCategory[] =>
defs.map((d, i) => ({
id: i + 1,
name: d.name,
percent: d.percent ?? 0,
isSavings: d.isSavings ?? false,
priority: d.priority ?? 100,
}));
const plans = (defs: Array<Partial<FixedPlan> & { name: string; totalCents: number }>): FixedPlan[] =>
defs.map((d, i) => ({
id: i + 1,
name: d.name,
totalCents: d.totalCents,
fundedCents: d.fundedCents ?? 0,
priority: d.priority ?? 100,
dueOn: d.dueOn ?? new Date().toISOString(),
}));
describe("previewAllocation — basics", () => {
it("single bucket 100% gets all remaining after fixed", () => {
const fp = plans([]);
const vc = cats([{ name: "Only", percent: 100 }]);
const r = previewAllocation(1_000, fp, vc);
expect(r.fixed.length).toBe(0);
expect(r.variable).toEqual([{ id: 1, name: "Only", amountCents: 1_000 }]);
expect(r.unallocatedCents).toBe(0);
});
it("zero allocations when amount is 0", () => {
const r = previewAllocation(0, plans([]), cats([{ name: "A", percent: 100 }]));
expect(r.fixed.length).toBe(0);
expect(r.variable.length).toBe(0);
expect(r.unallocatedCents).toBe(0);
});
it("no variable split if no categories", () => {
const r = previewAllocation(500, plans([]), []);
expect(r.fixed.length).toBe(0);
expect(r.variable.length).toBe(0);
expect(r.unallocatedCents).toBe(500);
});
});
describe("previewAllocation — fixed first", () => {
it("funds plans in priority then due order up to need", () => {
const fp = plans([
{ name: "B", totalCents: 8000, fundedCents: 7000, priority: 2, dueOn: "2025-12-31T00:00:00Z" }, // need 1000
{ name: "A", totalCents: 5000, fundedCents: 0, priority: 1, dueOn: "2025-12-01T00:00:00Z" }, // need 5000 (but priority=1 => first)
]);
const vc = cats([{ name: "Var", percent: 100 }]);
const r = previewAllocation(4000, fp, vc);
// Fixed plan A (priority 1) gets as much as possible: 4000 (need 5000)
expect(r.fixed).toEqual([{ id: 2, name: "A", amountCents: 4000 }]);
expect(r.variable.length).toBe(0);
expect(r.unallocatedCents).toBe(0);
});
it("leftover goes to variables after satisfying plan needs", () => {
const fp = plans([{ name: "Rent", totalCents: 10000, fundedCents: 9000, priority: 1 }]); // need 1000
const vc = cats([{ name: "Groceries", percent: 100 }]);
const r = previewAllocation(2500, fp, vc);
expect(r.fixed).toEqual([{ id: 1, name: "Rent", amountCents: 1000 }]);
expect(r.variable).toEqual([{ id: 1, name: "Groceries", amountCents: 1500 }]);
expect(r.unallocatedCents).toBe(0);
});
});
describe("previewAllocation — largest remainder with savings-first tiebreak", () => {
it("splits by integer floor and then leftover by remainder", () => {
// 3 cats: 50%, 30%, 20%; amount 101 -> floors: 50, 30, 20 => sumBase=100, 1 leftover to largest remainder (all remainders 0, so go to savings-first if any)
const vc = cats([
{ name: "A", percent: 50, isSavings: false, priority: 10 },
{ name: "B", percent: 30, isSavings: true, priority: 10 },
{ name: "C", percent: 20, isSavings: false, priority: 10 },
]);
const r = previewAllocation(101, plans([]), vc);
// Base: A50, B30, C20 = 100; leftover=1 -> goes to B (savings-first)
expect(r.variable).toEqual([
{ id: 1, name: "A", amountCents: 50 },
{ id: 2, name: "B", amountCents: 31 },
{ id: 3, name: "C", amountCents: 20 },
]);
});
it("ties on remainder resolved by savings-first, then priority asc, then name asc", () => {
// Force equal remainders with 3 categories @ 33.333…%
const vc = cats([
{ name: "Zeta", percent: 33, isSavings: false, priority: 2 },
{ name: "Alpha", percent: 33, isSavings: false, priority: 1 },
{ name: "Saver", percent: 34, isSavings: true, priority: 5 },
]);
// Amount 4: exacts ~ 1.32, 1.32, 1.36 => floors 1,1,1 sumBase 3 leftover 1 -> goes to Saver (savings-first)
const r = previewAllocation(4, plans([]), vc);
expect(r.variable).toEqual([
{ id: 1, name: "Zeta", amountCents: 1 },
{ id: 2, name: "Alpha", amountCents: 1 },
{ id: 3, name: "Saver", amountCents: 2 },
]);
});
});
describe("previewAllocation — normalization safety", () => {
it("normalizes percents when sum != 100 to avoid crashing UI", () => {
const vc = cats([
{ name: "A", percent: 40 },
{ name: "B", percent: 40 },
{ name: "C", percent: 40 }, // sum = 120 -> normalize
]);
const r = previewAllocation(120, plans([]), vc);
// Expect all cents allocated (no unallocated) and proportions roughly 1/3 each.
const total = r.variable.reduce((s, v) => s + v.amountCents, 0);
expect(total).toBe(120);
expect(r.unallocatedCents).toBe(0);
});
});