docs: design budget folders and recurring cost placement
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user