Browse Source

docs: plan category hierarchy and tags

master
Codex 2 months ago
parent
commit
e994e505cb
  1. 200
      docs/superpowers/plans/2026-09-14-category-hierarchy-tags.md

200
docs/superpowers/plans/2026-09-14-category-hierarchy-tags.md

@ -0,0 +1,200 @@
# Category Hierarchy and Tags Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add category groups and leaf categories while laying down persistence-only transaction tags.
**Architecture:** `CategoryGroup` owns optional groups of transaction-assignable `Category` leaves. Existing categories remain ungrouped. `Tag` is separate from accounting classification and is joined to transactions but has no product behavior in this release.
**Tech Stack:** Python, Django 5.2, PostgreSQL, pytest, Docker Compose.
**Spec:** `docs/superpowers/specs/2026-09-14-category-hierarchy-tags-design.md`
## Global Constraints
- Preserve every category primary key and transaction category relationship.
- Use only synthetic data; do not read or commit local YNAB data.
- Category leaves remain the only transaction-assignable classification.
- Tags have no UI, service mutation, filter, report, import, or export behavior.
- All writes use `finance.services`; run the health endpoint after implementation.
---
### Task 1: Add the hierarchy and tag schema
**Files:**
- Modify: `finance/models/reference.py`, `finance/models/ledger.py`, `finance/models/__init__.py`
- Create: `finance/migrations/0004_category_groups_and_tags.py`, `tests/finance/test_category_hierarchy_migration.py`
- Modify: `tests/finance/test_constraints.py`
**Interfaces:** `CategoryGroup(name, archived)`, `Tag(name, archived)`, nullable `Category.group`, and `Transaction.tags` with `related_name="transactions"`.
- [ ] **Step 1: Write failing schema tests**
```python
def test_category_names_are_unique_within_a_group_and_tags_can_relate(account):
home = CategoryGroup.objects.create(name="Synthetic Home")
travel = CategoryGroup.objects.create(name="Synthetic Travel")
Category.objects.create(name="Synthetic Fees", kind="EXPENSE", group=home)
Category.objects.create(name="Synthetic Fees", kind="EXPENSE", group=travel)
row = post(account)
tag = Tag.objects.create(name="Synthetic Tax")
row.tags.add(tag)
assert list(row.tags.all()) == [tag]
```
- [ ] **Step 2: Run the tests to verify they fail**
Run: `docker compose run --rm test pytest -q tests/finance/test_constraints.py tests/finance/test_category_hierarchy_migration.py`
Expected: FAIL because the new models and relations do not exist.
- [ ] **Step 3: Implement models and migration**
Create `CategoryGroup` and `Tag` from `TimestampedModel`, both with nonblank globally unique names and `archived`. Add `Category.group = ForeignKey("finance.CategoryGroup", null=True, blank=True, on_delete=PROTECT, related_name="categories")`; replace `Category.name` global uniqueness with `UniqueConstraint(fields=["group", "name"], name="category_group_name_unique")` and conditional `UniqueConstraint(fields=["name"], condition=Q(group__isnull=True), name="category_ungrouped_name_unique")`. Add `Transaction.tags = ManyToManyField("finance.Tag", blank=True, related_name="transactions")`. Generate the migration; it must not update rows.
- [ ] **Step 4: Run schema verification**
Run: `docker compose run --rm manage migrate; docker compose run --rm test pytest -q tests/finance/test_constraints.py tests/finance/test_category_hierarchy_migration.py; docker compose run --rm manage makemigrations --check --dry-run`
Expected: PASS.
- [ ] **Step 5: Commit the schema slice**
Run: `git add finance/models finance/migrations/0004_category_groups_and_tags.py tests/finance/test_constraints.py tests/finance/test_category_hierarchy_migration.py; git diff --cached --check; git diff --cached; git -c user.name=Codex -c user.email=codex@localhost commit -m "feat: add category groups and tags"`
### Task 2: Support category groups in services, choices, and reference pages
**Files:**
- Modify: `finance/services/reference.py`, `finance/services/posting.py`, `finance/services/__init__.py`, `finance/queries.py`
- Modify: `core/forms.py`, `core/views.py`, `config/urls.py`, `core/templates/core/references/list.html`, `core/templates/core/transactions/list.html`
- Modify: `tests/finance/test_services.py`, `tests/finance/test_queries.py`, `tests/test_web_workflows.py`
**Interfaces:** `create_category_group(*, name, actor)`, `create_category(*, name, kind, group_id=None, actor)`, `active_category_groups()`, and `category_label(category)`.
- [ ] **Step 1: Write failing behavior tests**
```python
def test_archived_group_blocks_new_child_assignment():
group = create_category_group(name="Synthetic Home", actor=ACTOR)
leaf = create_category(name="Synthetic Utilities", kind="EXPENSE", group_id=group.pk, actor=ACTOR)
archive(group, actor=ACTOR)
with pytest.raises(DomainError, match="archived_reference"):
record_transaction(account_id=account.pk, amount="-1", posting_date=date(2026, 1, 1), kind="EXPENSE", category_id=leaf.pk, actor=ACTOR)
```
- [ ] **Step 2: Run focused tests to verify failure**
Run: `docker compose run --rm test pytest -q tests/finance/test_services.py tests/finance/test_queries.py tests/test_web_workflows.py`
Expected: FAIL because groups cannot be created or archived.
- [ ] **Step 3: Implement active hierarchy behavior**
Add audited create/archive handling for `CategoryGroup`; extend category creation with optional locked active `group_id`. In posting, lock the category and then its group; reject an archived group using `DomainError("archived_reference")`. Make `active_categories()` exclude leaves with archived groups. Use `category_label` (`"Group · Leaf"` or `"Leaf"`) in form choices, filter choices, reference lists, and transaction rows. Add create/list/archive routes for groups only; add no tag route.
- [ ] **Step 4: Run focused tests**
Run: `docker compose run --rm test pytest -q tests/finance/test_services.py tests/finance/test_queries.py tests/test_web_workflows.py`
Expected: PASS; existing flat-category flows still pass.
- [ ] **Step 5: Commit the workflow slice**
Run: `git add finance/services finance/queries.py core/forms.py core/views.py config/urls.py core/templates/core/references/list.html core/templates/core/transactions/list.html tests/finance/test_services.py tests/finance/test_queries.py tests/test_web_workflows.py; git diff --cached --check; git diff --cached; git -c user.name=Codex -c user.email=codex@localhost commit -m "feat: manage grouped categories"`
### Task 3: Add hierarchy roll-ups to spending reports
**Files:**
- Modify: `finance/reports.py`, `core/templates/core/dashboard.html`, `core/templates/core/reports/monthly.html`
- Modify: `tests/finance/test_reports.py`, `tests/test_report_views.py`
**Interfaces:** `spending_by_category()` returns display rows with `group_name`, `category_name`, `is_group_total`, `expense`, `refund`, and `net_spending`.
- [ ] **Step 1: Write a failing grouped-total test**
```python
assert [(row.is_group_total, row.group_name, row.category_name, row.net_spending) for row in rows] == [
(True, "Synthetic Food", None, Decimal("50.00")),
(False, "Synthetic Food", "Synthetic Groceries", Decimal("30.00")),
(False, "Synthetic Food", "Synthetic Dining", Decimal("20.00")),
]
```
- [ ] **Step 2: Run report tests to verify failure**
Run: `docker compose run --rm test pytest -q tests/finance/test_reports.py tests/test_report_views.py`
Expected: FAIL because rows are flat.
- [ ] **Step 3: Implement query-time group roll-ups**
Aggregate by group and leaf fields, build each leaf row, and derive a group-total row by summing its leaves. Sort group totals by net spending then group name and their leaves by net spending then category name. Keep ungrouped categories and `Uncategorized` as leaf rows. Render visibly distinct group totals and indented child rows in both templates; do not change monthly summary arithmetic.
- [ ] **Step 4: Run report verification**
Run: `docker compose run --rm test pytest -q tests/finance/test_reports.py tests/test_report_views.py tests/test_mvp_acceptance.py`
Expected: PASS.
- [ ] **Step 5: Commit the report slice**
Run: `git add finance/reports.py core/templates/core/dashboard.html core/templates/core/reports/monthly.html tests/finance/test_reports.py tests/test_report_views.py; git diff --cached --check; git diff --cached; git -c user.name=Codex -c user.email=codex@localhost commit -m "feat: roll up spending by category group"`
### Task 4: Export category hierarchy and document the supported behavior
**Files:**
- Modify: `finance/exports.py`, `tests/finance/test_exports.py`, `docs/financial-domain.md`, `docs/local-workflows.md`
**Interfaces:** Add `category_groups.csv` before `categories.csv`; append `group_id` to `categories.csv`; do not export tags or transaction-tag joins.
- [ ] **Step 1: Write failing export tests**
```python
assert "category_groups.csv" in package.namelist()
assert rows(package, "categories.csv")[0]["group_id"] == str(group.pk)
assert "tags.csv" not in package.namelist()
assert "transaction_tags.csv" not in package.namelist()
```
- [ ] **Step 2: Run the export test to verify failure**
Run: `docker compose run --rm test pytest -q tests/finance/test_exports.py`
Expected: FAIL because group data is not exported.
- [ ] **Step 3: Implement export and documentation changes**
Add `CategoryGroup` to export schemas ahead of categories and add `group_id` to the category fields. Update synthetic export seed data, expected member order, headers, manifest checks, and row assertions. Update the financial-domain and local-workflow guides: leaves classify transactions; groups organize/report; archiving a group disables its children for new assignments; tags are schema-only.
- [ ] **Step 4: Run export verification**
Run: `docker compose run --rm test pytest -q tests/finance/test_exports.py tests/test_mvp_acceptance.py`
Expected: PASS with no tag archive members.
- [ ] **Step 5: Commit the export slice**
Run: `git add finance/exports.py tests/finance/test_exports.py docs/financial-domain.md docs/local-workflows.md; git diff --cached --check; git diff --cached; git -c user.name=Codex -c user.email=codex@localhost commit -m "feat: export category hierarchy"`
### Task 5: Run complete verification and health check
**Files:** Modify only to correct a task-scoped defect found below.
- [ ] **Step 1: Run migrations and the relevant suite**
Run: `docker compose build; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; docker compose run --rm manage migrate; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; docker compose run --rm test pytest -q tests/finance tests/test_web_workflows.py tests/test_report_views.py tests/test_mvp_acceptance.py; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; docker compose run --rm manage makemigrations --check --dry-run; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; docker compose run --rm manage migrate --check`
Expected: PASS.
- [ ] **Step 2: Start the app and check health**
Run: `docker compose up -d web; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; Invoke-RestMethod http://127.0.0.1:8000/health/ | ConvertTo-Json -Compress`
Expected: healthy JSON response.
- [ ] **Step 3: Perform final privacy and diff review**
Run: `git diff --check; git status --short; git log --oneline -4`
Expected: no whitespace errors or real financial data, with the pre-existing `AGENTS.md` edit left untouched.
Loading…
Cancel
Save