From e3a1fb366b9d34b1a8ba9d90430ce609463859b3 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Fri, 4 Sep 2026 09:15:20 +0200 Subject: [PATCH] fix(recurring): updateRecurringCost can't touch archived/endedFrom (X4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setRecurringCostEndedFrom is the only place that keeps archived and endedFrom paired, and endRecurringCost/restoreRecurringCost/the Ends column already delegate to it — but updateRecurringCost still typed its `changes` as Partial> and Object.assign'd it, so a caller passing either field alone would desynchronise the pair. No current call site does this, so it was latent; excluding both keys from the accepted type makes it impossible rather than merely unexercised. --- src/store/actions.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/store/actions.ts b/src/store/actions.ts index 0652f29..c117c63 100755 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -446,13 +446,22 @@ export function addRecurringCost( return created; } +/** + * `archived` and `endedFrom` are excluded on purpose: they are a coherent + * pair (`archived` mirrors "`endedFrom` is set") maintained ONLY by + * `setRecurringCostEndedFrom`, and a caller able to pass either through here + * could desynchronise them — a bill that reads as live but is dimmed, or + * ended but not. Route a change to either field through + * `setRecurringCostEndedFrom` (or its `endRecurringCost`/`restoreRecurringCost` + * convenience wrappers) instead. + */ export function updateRecurringCost( draft: Dataset, id: RecurringCostId, - changes: Partial>, + changes: Partial>, ): void { const cost = requireRecurringCost(draft, id); - const resolved: Partial> = { ...changes }; + const resolved: Partial> = { ...changes }; if (changes.recurrence) { resolved.recurrence = requireRecurrence(changes.recurrence);