From 093521b2c81778ea6d4afdcab40a6f1f028ee2da Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Fri, 4 Sep 2026 07:57:56 +0200 Subject: [PATCH] fix(views): keep a post visible when its only figure is an unconfirmed occurrence (I4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hasActivity only checked carriedIn/allocation/charges, so an archived post whose sole activity this month was an unconfirmed recurring occurrence (zero remaining, nonzero expected/projected) was dropped from monthView.rows — its Projected figure never reached the month view even though ExpectedBand listed the same occurrence. remaining and projected are both fully derived from five raw fields; the check now covers all five. --- src/domain/views.test.ts | 34 ++++++++++++++++++++++++++++++++++ src/domain/views.ts | 12 +++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/domain/views.test.ts b/src/domain/views.test.ts index 2b2f96f..b6ac8da 100644 --- a/src/domain/views.test.ts +++ b/src/domain/views.test.ts @@ -122,6 +122,40 @@ describe("monthView", () => { "old", ]); }); + + test("an archived post with only an unconfirmed occurrence still appears, with its projected figure (I4)", () => { + // "old" has a zero-percent rule (no allocation) and no purchases at all, + // so carriedIn/allocation/charges are all zero this month — its only + // figure is the unconfirmed recurring occurrence's `expected`/`projected`. + // Before I4's fix, hasActivity ignored those two fields and dropped the + // row, even though ExpectedBand lists the same occurrence. + const withRecurring: Dataset = { + ...data, + posts: [...data.posts, post("old", 2, 0, true)], + recurring: [ + { + id: "r1", + name: "Storage unit", + order: 0, + archived: false, + amount: { amount: 400, currency: "DKK" }, + startDate: "2026-01", + recurrence: { kind: "everyNMonths", n: 1 }, + anchoring: "calendar", + splitMode: "percent", + splits: [{ postId: "old", value: 100, absorbsRemainder: true }], + }, + ], + }; + const view = monthView(withRecurring, "2026-01"); + const row = view.rows.find((r) => r.post.id === "old"); + expect(row).toBeDefined(); + expect(row!.figures.carriedIn).toBe(0); + expect(row!.figures.allocation).toBe(0); + expect(row!.figures.charges).toBe(0); + expect(row!.figures.expected).toBe(400); + expect(row!.figures.projected).toBe(-400); + }); }); describe("yearView", () => { diff --git a/src/domain/views.ts b/src/domain/views.ts index 13d239a..d5f3c21 100644 --- a/src/domain/views.ts +++ b/src/domain/views.ts @@ -28,8 +28,18 @@ export interface MonthViewModel { } function hasActivity(figures: PostMonthFigures): boolean { + // `remaining` and `projected` are both fully derived from these five raw + // fields, so checking all five is what keeps a post visible whenever either + // column would show a nonzero figure. `expected` and `projectedCarriedIn` + // are the two the recurring-costs feature added: an archived post whose + // only activity this month is an unconfirmed occurrence has a zero + // `remaining` but a nonzero `projected`, and must not be dropped. return ( - figures.carriedIn !== 0 || figures.allocation !== 0 || figures.charges !== 0 + figures.carriedIn !== 0 || + figures.allocation !== 0 || + figures.charges !== 0 || + figures.expected !== 0 || + figures.projectedCarriedIn !== 0 ); }