From 2addbc9f0aeff3926fd8d227117c1865f8e6e8d7 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 14 Sep 2026 17:24:33 -0300 Subject: [PATCH] docs: design category hierarchy and tags --- ...26-09-14-category-hierarchy-tags-design.md | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-14-category-hierarchy-tags-design.md diff --git a/docs/superpowers/specs/2026-09-14-category-hierarchy-tags-design.md b/docs/superpowers/specs/2026-09-14-category-hierarchy-tags-design.md new file mode 100644 index 0000000..c560a78 --- /dev/null +++ b/docs/superpowers/specs/2026-09-14-category-hierarchy-tags-design.md @@ -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.