feat(recurring): offer confirmation on next occurrence beyond the month

Gap 1 (spec §5): ExpectedBand only listed this month's occurrences, so a
lastCharge bill whose pending slot had rolled into next month (the phone-bill
flagship case) was not confirmable without navigating there first.

Extend the walk 12 months past the displayed month (occurrencesByMonth's own
upToMonth bound; foldBalances is untouched) and surface a "Coming up" group:
at most one row per recurring cost, its next unconfirmed occurrence. That row
confirms EARLY, so it records today rather than the future slot date — which
is exactly what rebases a lastCharge series — and shows both dates so the
owner can see what will be written. `today` is exported from src/store/index.ts
next to `currentMonth`, keeping the clock out of src/domain/.

expectedGroups() is the pure, exported core (pending/overdue/comingUp);
ExpectedBand renders it and still returns null when all three are empty.
This commit is contained in:
Lucas Winther
2026-09-04 08:46:45 +02:00
parent 042bc8597d
commit 9fe950bc7d
3 changed files with 320 additions and 94 deletions
+10
View File
@@ -2,10 +2,20 @@ import { monthOf } from "../domain/months.ts";
import { readDataset, writeDataset } from "./db.ts";
import { createSnapshotStore } from "./snapshot.ts";
import { BAKED_FX_RATES } from "./bakedRates.ts";
import type { IsoDate } from "../domain/types.ts";
/** The current month, read at module load. The UI owns the clock; the domain does not. */
export const currentMonth = monthOf(new Date().toISOString().slice(0, 10));
/**
* Today, day-granular, read at module load. The UI owns the clock; the domain
* does not. `ExpectedBand`'s "Coming up" group needs the actual day — a
* coming-up occurrence is confirmed EARLY, so the purchase it writes must be
* dated today rather than the future slot, which is exactly what rebases a
* `lastCharge` series.
*/
export const today: IsoDate = new Date().toISOString().slice(0, 10);
export const store = createSnapshotStore(
{ read: readDataset, write: writeDataset },
currentMonth,