1 changed files with 124 additions and 0 deletions
@ -0,0 +1,124 @@
|
||||
# Category Hierarchy and Tags Design |
||||
|
||||
**Date:** 2026-09-14 |
||||
**Status:** Approved |
||||
**Milestone:** Post-MVP financial reference-data evolution |
||||
|
||||
## Goal |
||||
|
||||
Represent YNAB-style category groups and their assignable subcategories in |
||||
Gizmoduck without changing existing transaction classifications. Establish a |
||||
minimal tag data model now, without exposing tag behavior, so a later feature |
||||
can use tags without reworking the transaction schema. |
||||
|
||||
This is a prerequisite for a later, separately designed YNAB reference-data |
||||
synchronization that imports category hierarchy and payees and proposes |
||||
annotations for existing transactions. |
||||
|
||||
## Scope |
||||
|
||||
This change includes: |
||||
|
||||
- category groups, leaf categories, hierarchy-aware category management, and |
||||
hierarchy-aware spending presentation; |
||||
- a migration that preserves every existing `Category` and its transaction |
||||
references as an ungrouped leaf category; |
||||
- a `Tag` model and a zero-or-more `Transaction.tags` relationship; and |
||||
- focused migration, model, service, query, and report tests using synthetic |
||||
data only. |
||||
|
||||
It excludes YNAB parsing/import, account mapping, transaction matching, |
||||
payee synchronization, tag creation or assignment workflows, tag filters, |
||||
tag reports, and tag export. Those need their own approved designs. |
||||
|
||||
## Category hierarchy |
||||
|
||||
Add `CategoryGroup` as a UUID reference model with a nonblank unique `name`, |
||||
an `archived` flag, and timestamps. A group is organizational only: it has no |
||||
income/expense kind and cannot be assigned to a transaction. |
||||
|
||||
Add nullable `group = ForeignKey(CategoryGroup, on_delete=PROTECT, |
||||
related_name="categories")` to `Category`. `Category` remains the sole |
||||
transaction-assignable leaf and retains its required `INCOME` or `EXPENSE` |
||||
kind. Existing categories retain a null group after migration, so their |
||||
identity and all historical transaction foreign keys remain unchanged. |
||||
|
||||
Replace global category-name uniqueness with these PostgreSQL constraints: |
||||
|
||||
- `(group, name)` is unique for grouped categories; and |
||||
- an ungrouped category name is unique among ungrouped categories. |
||||
|
||||
This permits, for example, a `Fees` leaf under more than one group while |
||||
preventing ambiguous duplicates in the same group or among legacy ungrouped |
||||
categories. Group names remain globally unique. |
||||
|
||||
Archived groups and categories remain visible on historical transactions but |
||||
are unavailable for new assignments. An otherwise unarchived leaf under an |
||||
archived group is likewise unavailable for new assignment. No archive action |
||||
cascades to children or changes transaction history. |
||||
|
||||
## Application behavior |
||||
|
||||
Add public reference-data operations to create and archive category groups, |
||||
and extend category creation to accept an optional group UUID. Category lists |
||||
and creation choices present active groups with their active leaf categories, |
||||
then active ungrouped categories. Existing ungrouped categories continue to |
||||
work unchanged. |
||||
|
||||
Transaction category filtering remains a leaf-category filter. Transaction |
||||
creation, annotation, and import-review assignment continue to receive a |
||||
category UUID; their active-reference validation must reject both archived |
||||
leaves and leaves whose group is archived. |
||||
|
||||
The spending report retains its existing accounting totals. It presents rows |
||||
under category groups and includes a roll-up total for each group; ungrouped |
||||
categories and `Uncategorized` remain separate. Group totals are derived from |
||||
the leaf-category transactions at query time and do not store cached amounts. |
||||
|
||||
Exports include group identity and category-to-group relationship so a restore |
||||
or future migration can reconstruct the hierarchy. Tag tables and transaction |
||||
tag relationships are not exported until tags can be assigned through a |
||||
supported product workflow. |
||||
|
||||
## Tags |
||||
|
||||
Add `Tag` as a UUID reference model with a nonblank globally unique `name`, |
||||
an `archived` flag, and timestamps. Add a many-to-many relationship between |
||||
`Transaction` and `Tag` with a stable reverse relation. |
||||
|
||||
No current public service, web endpoint, import decision, report, filter, or |
||||
export writes or reads tags. The migration creates no tags and attaches none to |
||||
existing transactions. The relation is deliberately separate from category: |
||||
a category remains the single accounting classification, while future tags |
||||
will be optional cross-cutting labels. |
||||
|
||||
## Integrity and migration |
||||
|
||||
The schema migration adds tables and nullable relations before removing the |
||||
old global category-name uniqueness constraint, so existing data remains |
||||
valid throughout. It does not create category groups, alter category names, |
||||
or update transaction records. |
||||
|
||||
The category group foreign key uses `PROTECT`; supported application behavior |
||||
does not delete groups, categories, or tags. |
||||
Service-level active-reference checks enforce effective archival state because |
||||
the database cannot express a cross-table archived-group rule in a check |
||||
constraint. |
||||
|
||||
## Verification |
||||
|
||||
Tests will demonstrate that: |
||||
|
||||
- the migration preserves existing categories and transaction category links; |
||||
- category groups cannot be assigned to transactions, while leaf category |
||||
kind rules remain intact; |
||||
- duplicate leaf names are allowed only across distinct groups; |
||||
- archived group membership prevents new assignments without hiding history; |
||||
- grouped report totals equal the sum of their leaf totals; and |
||||
- tags can exist and relate to transactions at the database level without |
||||
changing existing category, report, or export behavior. |
||||
|
||||
Apply migrations against an existing local database and a clean database, run |
||||
the focused tests and the relevant finance suite, then start the local service |
||||
and verify its health endpoint. No real YNAB files or financial records may be |
||||
included in tests, commits, diagnostics, or generated artifacts. |
||||
Loading…
Reference in new issue