feat: refine month totals and recurring purchase entry
Continuous Integration / lint-and-test (push) Successful in 29s
Continuous Integration / docker-build-and-push (push) Successful in 40s

This commit is contained in:
Lucas Winther committed 2026-10-03 16:40:00 +02:00
1 parent 7cccd7a41a
commit c250bbe476
30 files changed
+634 -67

No files matched your search

+26
View File
@@ -62,6 +62,12 @@ 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.
The month header's Actual available is entered income minus all actual Spent
for that month. It includes refunds, unassigned spending and finance-plan slices,
but no allocation, carry or unpaid expectations. Before the rollover start,
the month view reads the existing charge calculation directly because the fold
has deliberately produced no envelope figures yet.
A recurring cost adds a SECOND balance beside it, on its own carry:
```
@@ -157,6 +163,11 @@ still works.
## Money
The default currency table seeds new datasets only. JPY, AUD, CAD and MXN join
DKK, USD, EUR and GBP; existing currency tables are not rewritten. The build
derives its FX targets from the seed table and carries complete committed
fallback rates. These rates never replace one an owner has cleared.
A float, deliberately, with two rules that make it safe:
1. **Round at every boundary** via `roundMoney` — after any division, after any
@@ -187,6 +198,11 @@ which also says what re-adding a per-currency override would take.
## Splits and finance plans compose
Purchase entry defaults to creating a new recurring cost when the owner chooses
to make a purchase recur. Selecting an existing bill is enabled explicitly in
Settings by the optional `allowExistingRecurringCost` boolean (absent is false),
validated on JSON import. Existing purchase associations remain editable.
A purchase can be split across posts *and* spread over months at once. The trick
is in `charges.ts`: first compute how the **whole** purchase divides across posts
(`splitPartsOfTotal`), then distribute each month's slice proportionally *to
@@ -211,6 +227,16 @@ 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.
## Display preferences and build identity
Monthly chart visibility and Expected's expanded state persist per browser,
alongside the theme preference. They never enter an exported dataset or change
the month calculations. Storage errors leave the display controls usable.
Colophon receives a source ref and UTC build date separately from the build.
The date describes the artifact, not the browser's current day. A source archive
has no Git ref but still receives a unique service-worker cache version.
## Things the app deliberately does NOT validate
Adding a "missing" guard here is a regression, not a fix:
+6 -2
View File
@@ -71,6 +71,9 @@ Confirmed and in place:
- A purchase may have no post. It remains visible in its month and counts in
Spent, while leaving envelope balances unchanged.
- Per-month income, entered directly, varying month to month.
- The month header shows actual available: that month's entered income minus
actual spending, including unassigned purchases and refunds. Monthly breakdown
can be hidden and Expected collapsed; these choices persist in this browser.
- Multi-currency (DKK/USD/EUR) against a user-maintained exchange-rate table,
with an optional on-demand fetch that fails soft and is never required.
- JSON export/import and ODS (spreadsheet) export, both generated in the
@@ -92,8 +95,9 @@ Confirmed and in place:
whichever charge was most recent — a phone plan that recharges early when
its data runs out is the case this exists for. Each due date on the
calendar model is a projected occurrence the owner confirms into a real
purchase; a recurring cost can also be tagged onto a purchase, or created
from one, while it is being entered, rather than only from Settings. The
purchase; a recurring cost can be created from a purchase while it is being
entered. Choosing an existing bill in Add purchase is enabled explicitly in
Settings; existing purchase associations remain editable. The
month view shows what is still expected alongside a second, projected
balance per post, so a known bill is visible before it is paid without
touching the reconciled figure a bank statement is checked against.
+1 -1
View File
@@ -10,7 +10,7 @@ task moved to `AGENTS.md`.
**Current state:** the anchored model is merged with budget folders and
placement. The month pie showcase, browser-stored session backups, and
unassigned recurring costs are implemented. 881 tests pass across 64 files;
unassigned recurring costs are implemented. 911 tests pass across 66 files;
`bunx tsc --noEmit` is clean, and `SKIP_FX_FETCH=1 bun run build` succeeds
using the committed seed rates.
@@ -0,0 +1,96 @@
# Month and purchase refinements
The owner requested this round on 2026-10-03, with the work queued in TODO
before implementation. The follow-up added a visible Total-field reset bug.
## Actual available
The month header shows entered income minus actual spending beside Spent.
This includes unassigned purchases, refunds, foreign-currency charges and the
selected month's finance-plan slices. Unpaid recurring occurrences do not
reduce it. Allocation and envelope carry do not enter this calculation.
Negative values remain visible, and arithmetic and display use dataset digits.
Purchases before the envelope fold's start still count in Spent and Actual
available. The month view uses the existing charge calculation for those
months; the rollover fold continues to begin at its configured start.
## Display choices
Monthly breakdown has a Show chart checkbox. Its header and checkbox remain
visible when the chart and detail list are hidden. Expected has an Expand /
Collapse button and shows counts for expected, coming-up and overdue entries
when closed. Closing it preserves any typed confirmation amount.
Both choices default to visible/expanded and persist across months and reloads
in browser localStorage. This follows the theme decision: display preferences
belong to this browser, not to the exported budget. Blocked localStorage must
not stop the app or the controls from working. Neither choice changes money,
occurrences or confirmation behavior.
## Purchase entry
New purchases begin with Recurring cost set to None. Their recurring option
creates a new bill from this purchase, rather than selecting an existing bill.
Creating a bill and its first charge remains one atomic mutation. Calendar
slots and last-charge date requirements retain their existing rules.
Settings → Recurring costs includes a checkbox to allow selecting existing
bills in Add purchase. The optional dataset boolean is
`settings.allowExistingRecurringCost`; absent means false, so existing budgets
adopt the new default without a migration. JSON import accepts only booleans
when this field is present. The choice travels with a JSON export.
Editing a linked purchase continues to show its saved association even with
the setting off, and choosing None can unlink it. Ordinary purchases do not
automatically become recurring bills. This resolves the default-selection
ambiguity in favor of an explicit recurring action.
The Total field has its own text state to preserve partly typed money inputs.
Resetting the dialog now resets that text alongside the numeric draft: new
purchases start blank and existing purchases start with their saved amount.
The expanded new-bill form exceeded a phone viewport in a browser check. The
purchase dialog now caps its height to the viewport and scrolls its contents,
keeping both the first fields and Save reachable.
## Default currencies
New datasets include Japanese yen (JPY, ¥), Australian dollars (AUD, AU$),
Canadian dollars (CAD, CA$) and Mexican pesos (MXN, MX$). Existing tables are
not migrated: the request specified defaults, and editing an established
table is the owner's action. Every currency still uses dataset-wide precision.
The build's FX targets derive from the seed table. All four have committed
fallback rates as well as live build-time fetching. The committed rates were
obtained from Frankfurter's DKK-base quote dated 2026-10-02 and inverted to
DKK per unit, preserving six decimal places. Like the previous constants,
these rows are marked manual and seed only new datasets.
Source: https://api.frankfurter.dev/v1/2026-10-02?base=DKK&symbols=JPY,AUD,CAD,MXN
The amount parser strips longer symbols before the plain dollar sign, so
AU$, CA$ and MX$ work without leaving stray letters in the amount. An explicit
currency code still disambiguates a symbol collision.
## Build stamp
Colophon shows the source ref and a separate build-time UTC date. An exact
commit keeps its source link; an uncommitted checkout retains the dirty suffix.
Source archives say source archive rather than inventing a Git ref, while
the service worker still receives a unique cache version. Development says
development and no build date. Browser accessors tolerate an absent process
global, following the existing Bun environment-inlining rule.
## Verification
Regression coverage exercises the actual money calculation, purchase save /
reopen, new recurring creation, enabled existing-bill selection, linked editing,
display persistence, collapse/expand preserving a typed amount, Settings write
errors, JSON preference validation, symbol parsing and new seed conversion.
Chrome checks verify the month controls, saving and reopening a purchase,
persisting the Settings choice to IndexedDB, creating a new bill and its first
charge together, keeping mobile Save reachable, and production build metadata.
Desktop and mobile captures cover light and dark themes. Screenshots establish
layout and console behavior; visual feel still requires the owner.
The complete suite passes 911 tests across 66 files, TypeScript is clean, and
production builds succeed at both root and subpath base URLs with seed rates.