feat: support unassigned purchases and allocation status
Continuous Integration / lint-and-test (push) Successful in 13s
Continuous Integration / docker-build-and-push (push) Successful in 35s

Allow month-only purchases to start finance plans. Keep unbudgeted posts distinct from explicit zero rules in overspend reporting.
This commit is contained in:
Lucas Winther committed 2026-09-30 13:10:10 +02:00
1 parent c1446db39f
commit b0e8145d2e
31 files changed
+469 -80

No files matched your search

+56
View File
@@ -55,6 +55,33 @@ const data: Dataset = {
};
describe("monthView", () => {
test("counts a loose purchase as spent without charging a post", () => {
const loose: Dataset = { ...data, purchases: [{ ...spend("loose", "food", 75, "2026-01"), splits: [] }] };
const view = monthView(loose, "2026-01");
expect(view.totalCharges).toBe(75);
expect(view.rows.every((row) => row.figures.charges === 0)).toBe(true);
expect(view.rows[0]!.figures.remaining).toBe(2000);
});
test("counts financed loose slices in their months and converts currency", () => {
const loose: Dataset = {
...data,
fxRates: [{ currency: "EUR", baseUnitsPerOne: 7.5, updatedAt: "2026-01-01", source: "manual" }],
purchases: [{
...spend("loose", "food", 10, "2026-01"),
total: { amount: 10, currency: "EUR" },
splits: [],
schedule: { slices: [
{ month: "2026-01", amount: { amount: 4, currency: "EUR" } },
{ month: "2026-02", amount: { amount: 6, currency: "EUR" } },
] },
}],
};
expect(monthView(loose, "2026-01").totalCharges).toBe(30);
expect(monthView(loose, "2026-02").totalCharges).toBe(45);
expect(yearView(loose, 2026).totalCharges).toBe(75);
expect(summaryView(loose, "2026-01", "2026-02").looseCharges).toBe(75);
});
test("reports income, allocations, and the unallocated remainder", () => {
const view = monthView(data, "2026-01");
expect(view.income).toBe(10000);
@@ -104,6 +131,35 @@ describe("monthView", () => {
expect(view.overspentCount).toBe(0);
});
test("no allocation rule stays distinct from an explicit zero allocation", () => {
const unbudgeted = { ...post("none", 0, 0), rules: [] };
const zero = post("zero", 1, 0);
const caseData: Dataset = {
...data,
posts: [unbudgeted, zero],
purchases: [spend("a", "none", 25, "2026-01"), spend("b", "zero", 25, "2026-01")],
};
const view = monthView(caseData, "2026-01");
expect(view.rows.map((row) => row.hasAllocationRule)).toEqual([false, true]);
expect(view.rows.map((row) => row.figures.remaining)).toEqual([-25, -25]);
expect(view.overspentCount).toBe(1);
expect(yearView(caseData, 2026).rows.map((row) => row.hasAllocationRuleByMonth[0])).toEqual([false, true]);
});
test("a monthly override counts as an allocation rule even before any standing rule", () => {
const caseData: Dataset = {
...data,
posts: [{ ...post("none", 0, 0), rules: [] }],
months: [{ id: "2026-01", income: { amount: 10000, currency: "DKK" }, ruleOverrides: {
none: { kind: "fixed", amount: { amount: 0, currency: "DKK" } },
} }],
purchases: [spend("a", "none", 25, "2026-01")],
};
const view = monthView(caseData, "2026-01");
expect(view.rows[0]!.hasAllocationRule).toBe(true);
expect(view.overspentCount).toBe(1);
});
test("archived posts appear only when they have activity", () => {
const withArchived: Dataset = {
...data,