## 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
Budgets previously tracked a single category **or** label (mutually
exclusive). This lets a budget span **multiple** categories and labels
at once, all pooling spend against one allocated amount per period.
Scope decided with the requester:
- **Shared pool** — one allocated amount; any tracked category/label
counts against it.
- **Multi categories + multi labels** on a single budget.
- **Create-only** — tracking is chosen at creation and locked afterward
(edit dialog shows it read-only).
## Changes
**Backend**
- New `budget_category` + `budget_label` pivot tables; data migration
copies existing `category_id`/`label_id` into them, then drops those
columns.
- `Budget` model: `categories()` / `labels()` belongsToMany.
- `BudgetTransactionService` matches transactions across **all** tracked
categories OR labels (live assignment + historical backfill).
- `StoreBudgetRequest` accepts `category_ids` / `label_ids` arrays,
requires ≥1 across both, validates ownership. `update` no longer touches
tracking.
**Frontend**
- Reusable `MultiSelect` (popover + command + badge).
- Create dialog uses multi-selects; cards and show page render tracked
categories/labels as badges; edit dialog shows them read-only.
- Dexie bumped to v10 (drops unused per-category allocations table, adds
`budget_labels`).
## Testing
- Updated all budget/transaction/listener/browser tests for the pivot
model; added cases for multi-category matching, mixed category+label
pooling, and store validation (empty selection + foreign-ownership).
- Added the new `__()` strings to `lang/es.json`.
- Local `pint`, `lint`, `format` pass. Relying on CI for the full suite.
## Summary
- lock the transaction row before reconciling `budget_transactions` so
concurrent assignment work serializes per transaction
- retry the assignment transaction on deadlocks using Laravel's built-in
transaction attempts
- add regression coverage that asserts the deadlock retry path remains
configured for `PHP-LARAVEL-D`
## Testing
- php artisan test --compact
tests/Feature/BudgetTransactionServiceTest.php
tests/Feature/Listeners/AssignTransactionToBudgetTest.php
## Summary
- replace wipe-and-reinsert budget assignment with idempotent
reconciliation in `BudgetTransactionService`
- keep the fix scoped to the service layer instead of relying on queued
listener uniqueness or event metadata
- add regression coverage for duplicate reruns, stale assignment
cleanup, historical reruns, and label-only listener updates
## Testing
- php artisan test --compact
tests/Feature/BudgetTransactionServiceTest.php
tests/Feature/Listeners/AssignTransactionToBudgetTest.php
tests/Feature/TransactionTest.php
tests/Feature/BulkUpdateTransactionsTest.php
## Why
### Problem
When a refund (positive transaction amount) is assigned to a budget, it
incorrectly **increases** the cumulative spending instead of
**reducing** it. This causes:
- The spending chart line to go **up** on refunds instead of down
- The "Spent" amount to be inflated
- The "Remaining" amount to be understated
- Period rollover calculations to carry over incorrect amounts
### Root Cause
`BudgetTransactionService` uses `abs($transaction->amount)` when
creating budget transactions, which forces all amounts to be positive —
including refunds. Since expenses are stored as negative in the
`transactions` table and refunds as positive, `abs()` treats both as
spending.
## What
### Changes
- Replace `abs($transaction->amount)` with `-$transaction->amount` in
both `assignTransaction()` and `assignHistoricalTransactionsToPeriod()`
— expenses (`-5000`) become positive spending (`5000`), refunds
(`+1000`) become negative spending (`-1000`)
- Remove redundant `abs()` in `BudgetPeriodService::closePeriod()`
rollover calculation
- Add data migration to fix existing `budget_transactions` rows where
the original transaction was a refund
- No frontend changes needed — the chart and budget card already sum
`t.amount` directly
## Verification
### Tests
- Updated `assignHistoricalTransactionsToPeriod stores negated
transaction amount for expenses` — verifies expense sign is preserved
- Added `assignHistoricalTransactionsToPeriod stores refund as negative
amount` — verifies refunds reduce spending
- Added `budget spending correctly reflects mix of expenses and refunds`
— verifies net spending (expense - refund)
- Added `assignTransaction stores refund as negative budget transaction
amount` — verifies real-time assignment handles refunds
## Overview
We're excited to introduce budgeting capabilities to Whisper Money! This
feature helps you take control of your finances by setting spending
limits and tracking your progress over time.
## Screenshots
<img width="1316" height="793" alt="image"
src="https://github.com/user-attachments/assets/ac394d36-cded-4ea4-9883-120785e260f1"
/>
<img width="1315" height="907" alt="image"
src="https://github.com/user-attachments/assets/7c682474-5aa7-4388-b626-29b56f5ebbef"
/>
<img width="1315" height="992" alt="image"
src="https://github.com/user-attachments/assets/21eace45-23c6-472d-9aa0-0feb6db3fba4"
/>
## What's New
### Create Flexible Budgets
- Set budgets for specific categories or labels
- Choose from monthly, weekly, bi-weekly, or custom periods
- Set your own budget start date for better alignment with your pay
schedule
### Track Your Spending
- Visual spending charts show how much you've spent vs. your budget
- See at a glance which budgets are on track and which need attention
- View all transactions that count toward each budget
### Smart Budget Management
- **Carry Over**: Unused budget amounts automatically roll into the next
period
- **Reset**: Unused amounts return to your available money pool
- Edit or delete budgets anytime as your needs change
### Easy Access
- New Budgets section in the main navigation
- Quick overview cards showing budget status
- Detailed budget pages with spending history and transaction lists
## How It Works
1. Create a budget by selecting a category or label and setting your
spending limit
2. Your transactions are automatically matched to relevant budgets
3. Track your progress with visual charts and spending summaries
4. Adjust your budgets as needed to stay on track with your financial
goals
This feature is now available behind a feature flag and can be enabled
for users who want to start budgeting their expenses.