feat(recurring): datable cancellation via an "Ends" column
Gap 2 (spec §10): endedFrom is typed as any IsoDate and the spec calls it "cancellation", but Settings only ever wrote currentMonth — backdating or forward-dating a cancellation was import-only. Add an "Ends" column beside "Starts" in the Recurring costs table, edited and validated the same way (isValidEndedFrom — no granularity rule, per §10, but still refuses a calendar-impossible date like "2026-02-30"). Empty means "not ended" and clears the cancellation. Coherence rule: `archived` mirrors "endedFrom is set". setRecurringCostEndedFrom is the one place that now enforces it — endRecurringCost and restoreRecurringCost (the one-click row buttons) delegate to it, so the column and the buttons can never leave a bill dimmed-but-live or ended-but-undimmed. Also closes the same calendar-impossible gap in the JSON importer, which checked endedFrom's shape but not its calendar validity, unlike startDate.
This commit is contained in:
1 parent
9fe950bc7d
commit
b276573ffe
6 files changed
+219
-12
No files matched your search
@@ -302,6 +302,27 @@ describe("recurring cost validation", () => {
|
||||
.toThrow(/ended-from date/);
|
||||
});
|
||||
|
||||
// Same residual-C1 gap as startDate: PURCHASE_DATE checks shape and digit
|
||||
// range, not calendar validity, so "2026-02-30" passes it. §10: endedFrom
|
||||
// carries no granularity rule, but it must still be a real date.
|
||||
test("a calendar-impossible endedFrom is refused (day- or month-shaped)", () => {
|
||||
expect(() => parseDatasetJson(withRecurring([{ ...valid, endedFrom: "2026-02-30" }])))
|
||||
.toThrow(/calendar-impossible ended-from/);
|
||||
expect(() => parseDatasetJson(withRecurring([{ ...valid, endedFrom: "2026-02-29" }]))) // not a leap year
|
||||
.toThrow(/calendar-impossible ended-from/);
|
||||
});
|
||||
|
||||
test("a valid endedFrom of either granularity is accepted, regardless of recurrence kind", () => {
|
||||
expect(
|
||||
parseDatasetJson(withRecurring([{ ...valid, endedFrom: "2026-06" }])).recurring.length,
|
||||
).toBe(1);
|
||||
expect(
|
||||
parseDatasetJson(
|
||||
withRecurring([{ ...valid, endedFrom: "2026-06-17" }]), // month-only recurrence, day-granular end
|
||||
).recurring.length,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test("an interval below 1 is refused, not clamped", () => {
|
||||
// Clamping would silently change the bill's schedule. The walk in
|
||||
// occurrences.ts cannot terminate without this.
|
||||
|
||||
Reference in new issue
Block a user