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
2026-09-30 13:10:10 +02:00
parent c1446db39f
commit b0e8145d2e
31 changed files with 469 additions and 80 deletions
+14 -2
View File
@@ -47,8 +47,9 @@ remaining = carriedIn + allocation − charges (carried forward, monthly)
Unspent carries; overspend carries as debt. **Nothing else may recompute this.**
The month view, year matrix, summary, per-post views and the ODS export all
aggregate over `foldBalances` / `figuresFor`. A closing balance is read from
`byMonth[11].remaining`, never re-summed.
aggregate envelope figures over `foldBalances` / `figuresFor`. Overall Spent
also includes unassigned purchase slices, which have no envelope to fold. A
closing balance is read from `byMonth[11].remaining`, never re-summed.
A recurring cost adds a SECOND balance beside it, on its own carry:
@@ -156,6 +157,17 @@ those parts*. Both split modes collapse to one code path per slice.
Applying a split's raw value per-slice looks simpler and passes every percentage
test — and charges a fixed "2000 to Video Games" in full on every slice.
A purchase with no post has an empty `splits` array. Its dated amount counts in
monthly, yearly and summary Spent totals, but charges no envelope and changes no
rollover balance. Recurring bills still require a destination post. JSON import
and ODS export preserve these unassigned purchases.
A post with no applicable allocation rule has no overspend status, even if its
balance is negative. The balance remains visible and carries normally. An
explicit zero rule, including a zero monthly override, does have an overspend
status when its balance is negative. The views use the presence of a rule rather
than infer it from the numeric allocation, since both cases fold to zero.
## Things the app deliberately does NOT validate
Adding a "missing" guard here is a regression, not a fix: