## Summary
Adds nested categories (parent → child, up to **3 levels**) across the
app. Children inherit their parent's type and cashflow direction, and
every category selector now renders the hierarchy as an indented tree.
Gated behind the `CategoryTree` Pennant flag (off by default).
## Backend
- **Migration**: nullable self-referencing `parent_id`; uniqueness
scoped per-parent via a `parent_unique_marker` virtual column (root
names stay unique). New composite unique created before dropping the old
one so the `user_id` FK keeps a supporting index.
- **`CategoryTree` service**: descendant/ancestor resolution, depth &
cycle checks, type cascade, subtree deletion.
- **Validation**: depth limit, cycle prevention, inherited + locked
child type.
- **Delete strategies**: reparent (default), promote to root, or cascade
(uncategorizes affected transactions).
- **Transaction filter** expands a selected parent to its descendants.
- **Cashflow Sankey & breakdown** roll up to top-level parents with
click-to-drill (children + a parent "direct" node).
- **Budgets** tracking a parent also count their children's
transactions.
- **Unified** the frontend category query behind
`Category::forDisplay()` / `FRONTEND_COLUMNS` (8 call sites) so every
selector receives the full Category shape, including `parent_id`.
## Frontend
- New `category-tree.ts` helpers (build/flatten/descendants/path,
tree-aware selection toggle + tri-state).
- **Settings page**: indented tree, sortable by name/color/type
(siblings sorted, hierarchy preserved), parent picker, delete-strategy
dialog.
- **Combobox** (transaction table cell + edit/create modal + parent
picker): indented tree, search keeps matches with their ancestors.
- **Transaction filter**: indented tree, tri-state cascading selection
(parent ↔ children), themed checkboxes, selected branches float to top
on open, ancestor-aware search.
- **Categorizer palette** and **budget multi-select**: same indented
tree + ancestor-aware search.
- **Sankey**: click a parent node to drill into its children, with
breadcrumb.
- Pennant `CategoryTree` flag gates the parent UI.
## Tests
- Pest: model/validation, delete strategies, filter expansion, cashflow
rollup/drill, budget child inclusion.
- Vitest: tree-aware selection logic.
- All green; Pint / ESLint / Prettier clean.
## Rollout
Flag is off by default — enable per user with:
\`\`\`
php artisan feature:enable "App\\Features\\CategoryTree" you@example.com
\`\`\`
## Summary
- Always show a simplified Cashflow and charts explanation in the
category form
- Explain how each category type affects cashflow, income/spending
charts, top spending categories, savings, and investments
- For transfer categories, keep a cashflow chart visibility selector so
users can choose hidden/inflow/outflow
- Show savings and investment categories as outflows in the cashflow
chart and store default/new categories as outflow
- Add a new migration to update existing production savings/investment
categories to outflow
- Avoid user-facing Sankey terminology and update Spanish copy to use
dinero/saldo instead of efectivo
- Add missing Spanish translations and browser/feature coverage
## Tests
- php artisan test --compact tests/Feature/LocalizationTest.php
- php artisan test --compact
tests/Feature/Console/ResetUserCategoriesCommandTest.php
- php artisan test --compact tests/Feature/Settings/CategoryTest.php
--filter="migration updates existing default saving and investment
categories|migration sets saving and investment categories to cashflow
outflow|users can create savings and investment categories|default
categories are created when user registers"
- php artisan test --compact tests/Feature/CashflowAnalyticsTest.php
--filter="sankey includes savings and investment categories on the
expense side|sankey includes outflow transfer categories on the expense
side"
- php artisan test --compact tests/Browser/CategoriesTest.php
--filter="explains savings and investment category cashflow impact in
the create dialog|can create a new transfer category with a cashflow
analytics direction"
- php artisan test --compact tests/Browser/CategoriesTest.php
- ./vendor/bin/pest --testsuite=Performance --filter="account show page
does not exceed query threshold"
- vendor/bin/pint --dirty --format agent
- npx prettier --write
resources/js/components/categories/category-cashflow-direction-fields.tsx
- npm run build (assets emitted; Sentry sourcemap upload reports local
SSL error)
## Video
- screenshots/category-create-cashflow-setting.webm
## Notes
- npm run types still fails on existing unrelated TypeScript errors.
## Why
### Problem
As new features are added, dashboard and settings pages have been
accumulating extra database queries — risking N+1 regressions and
degraded response times. There was no automated way to detect these
regressions before they reach production.
## What
### Changes
- **New `Performance` test suite** (`tests/Performance/`) with 25 tests
that enforce query count ceilings on every page and API endpoint
accessible from the sidebar
- `PageQueryCountTest.php` — 15 tests covering Dashboard, Accounts,
Transactions, Budgets, Cashflow, and all Settings pages, plus scaling
tests that prove query count stays constant as data volume grows
- `ApiQueryCountTest.php` — 10 tests covering all Dashboard and Cashflow
analytics API endpoints, plus scaling tests
- **Separate CI job** (`performance-tests`) that runs in parallel with
`tests` and `linter`, and gates both `build-image` and `deploy`
- **Excluded from the main `tests` job** to avoid running them twice
(`--exclude-testsuite=Performance`)
- Helper functions (`performanceSeedUser`, `countQueries`,
`assertMaxQueries`) added to `tests/Pest.php` for reuse
- `phpunit.xml` updated with the new `Performance` testsuite entry
### How it works
Each test seeds a user with realistic data (3 accounts, 30 transactions,
15 balances, 5 categories, 3 labels, 1 budget) and asserts the endpoint
executes **at most N queries**. Thresholds have a small buffer (~3
queries) above current counts — tight enough to catch N+1 regressions
(which add dozens of queries) but loose enough to not break on minor
legitimate changes. On failure, every executed SQL query is dumped for
easy debugging.
### Run locally
```bash
php artisan test --testsuite=Performance
```
## Verification
### Tests
All 25 performance tests pass. Existing Feature (633) and Unit (22)
suites unaffected.