docs: design budget folders and recurring cost placement

This commit is contained in:
Lucas Winther
2026-09-29 20:13:45 +02:00
parent 4905986292
commit f1e42d9d77
@@ -0,0 +1,123 @@
# Budget folders and recurring cost placement
## Intent
The owner wants to organize a budget in folders and subfolders without making
every recurring bill an envelope. Rent may have its own post, while phone and
internet may share one post; the recurring rules remain individually editable.
The month page should show everyday posts such as Video Games, Events, and
Social cleanly alongside larger commitments. A bill's place in the folder tree
is independent of the post or posts its money uses.
Success means the owner can choose or create a destination post when adding a
bill, change that destination later, organize posts and bills together in nested
folders, and read the scheduled bill total of a folder for a selected month.
The post fold, two-stage bill confirmation, and purchase ledger remain the
sources of financial truth.
## Model
Add `Dataset.folders: BudgetFolder[]`, with each folder carrying `id`, `name`,
`parentId: FolderId | null`, and `order`. Add
`folderId: FolderId | null` to both `Post` and `RecurringCost`. `null` means
Unfiled, a visible virtual root group rather than a stored folder. Existing
records migrate with `folderId: null`; the v7 -> v8 migration adds an empty
folder array and assigns one deterministic sibling order to the existing posts
and bills. A new dataset starts with an empty folder array too.
Folders may nest to any depth. Sibling folders, posts, and bills share a
display order within their parent. The mutation layer assigns that order when
adding or moving an item and prevents a folder from becoming its own ancestor.
An item has one folder location; a recurring bill can still have splits into
several posts, including posts in other folders. Moving an item changes its
current organization across views, including past months, but never changes a
stored purchase, allocation rule, or computed post figure.
A folder is never an envelope: it has no currency, allocation rule, rollover,
or purchase split target. Only posts have those behaviors. Bill splits retain
their current `Split[]` format and remainder handling. No occurrence is stored
to support folders.
## Folder totals
On the month page, each folder heading shows **Bills due** in the dataset's
base currency. The figure is the sum of the prices of recurring occurrences
whose scheduled slot falls in the displayed month and whose bill is placed in
that folder or a descendant folder. A bill in a different folder contributes
there, even when it charges a post in this folder. Each occurrence contributes
once, so a 28-day bill can contribute twice in a month. An ended bill contributes
only before its end date. An empty folder shows zero; a folder containing only
posts also shows zero bills due.
For an unconfirmed occurrence, use its current expected amount. For a confirmed
occurrence, use the confirming purchase's actual total, because that is the
historical price paid. The slot month determines where the price appears;
`Purchase.date` continues to determine where the actual charge folds. Convert
each price to base using the same FX and `settings.digits` rules as the existing
money calculations, then round the sum at the boundary. A missing FX rate must
surface as an error, never a fallback. Negative prices remain allowed.
This heading is a schedule total, not a balance. It does not add post
allocations, remaining balances, or purchase charges to bill prices. Thus a
folder holding both a Rent post and its 8,000 rent bill shows 8,000 in **Bills
due**, not 16,000. Post balances remain visible on the post row. Parent folder
totals include each descendant bill exactly once.
## Month page and editing
Replace the flat post section with a tree of collapsible folders. Within an
open folder, posts keep their existing balance figures, meter, post links, and
allocation editing. Recurring bills appear as distinct rows showing their
price, cadence, assigned posts, and next scheduled occurrence (or Ended); an
Edit action opens the bill editor. The folder heading shows its Bills due total for this
month. The Unfiled group holds existing items and any newly created item not
placed in a folder. Collapsed folders still show their total so the first page
stays scannable. Keyboard users can expand and collapse folders, and the mobile
view uses a list rather than squeezing the desktop financial table.
Keep the due/overdue area as the fast reconciliation queue. It continues to
confirm occurrences into purchases using the existing rules, including
off-schedule payment dates. Bill rows in folders supply context and an editor,
not a second confirmation path or a second balance.
In the add-bill flow, select an existing destination post, create a dedicated
post (for example Rent), or expand a split editor to assign several posts.
Saving is disabled until the split has a destination. Creating a post and bill
is one store mutation so a failed write leaves neither behind. A newly created
dedicated post starts in the bill's selected folder, but either item can be
moved independently afterward. Editing an existing bill exposes the same split
controls. The bill amount remains a text field using `parseMoneyInput`.
Settings shows the same hierarchy for creating, renaming, reordering, moving,
and nesting folders and items. It also retains direct bill-rule editing and
post allocation history. Archived posts keep their fold history; ended bills
keep confirmations. The placement controls do not present folders as purchase
or allocation targets.
## Boundaries and verification
The domain provides a pure selector for the folder tree and scheduled totals,
using `occurrencesByMonth` rather than a second recurrence walk. It accepts the
displayed month from the caller and reads no clock. It never reimplements the
rollover fold. Store actions validate parent references, moves, cycles, and
split destinations. JSON import validates unique folder IDs, valid parent and
item references, and acyclic ancestry after migration. JSON export includes
the complete hierarchy; whole-dataset import keeps its existing backup gate.
Tests cover migration from v7, nested totals without double counting, multiple
occurrences in one month, confirmed actual prices and off-schedule dates, bills
assigned to posts in another folder, moves and cycle rejection, and import
validation. UI checks cover choosing or creating a post, editing a bill split,
Unfiled, collapse controls, and desktop/mobile month layouts. Browser captures
check layout and console errors; they do not establish whether the design feels
good in use.
## Alternatives considered
- **Labels on a flat list:** simpler storage but no folder/subfolder hierarchy
or useful collapse behavior for the first page.
- **Nested posts as folders:** would make parent items look budgetable and mix
bill prices with envelope balances, obscuring where money is actually counted.
- **One tree containing folders, posts, and bill rules:** selected. It makes
placement explicit while keeping bill assignment and envelope accounting
separate.