120 lines
4.9 KiB
TypeScript
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);
|
|
});
|
|
}); |