# Budget management design Date: 2026-07-03 Status: approved (design), pending spec review Firefly reference: v6.6.6 (matches running instance) ## Goal Let the agent (and user) manage Firefly III budgets from the `firefly` CLI: see spending vs budget, set/change monthly limits, create/delete budgets, enable/disable them, and assign a budget to a transaction when adding it. Ships as ONE MINOR release (new `budget` group + one new optional flag on `tx add`, no breaking change to the existing contract). ## Firefly API facts (verified against v6.6.6) - `GET /api/v1/budgets?start=&end=` — list budgets. With a start/end range the budget objects carry `spent` (array per currency) for that range. Without it, `spent` is null. Budget transformer fields include: `id`, `name`, `active`, `auto_budget_type`, `auto_budget_period`, `auto_budget_amount`, `currency_code`, `spent`. - `POST /api/v1/budgets` — create. Accepts `name` (required, unique per user), `active` (bool), `auto_budget_type` (in: reset,rollover,adjusted,none), `auto_budget_amount`, `auto_budget_period`, `currency_code`. - `PUT /api/v1/budgets/{id}` — update. Accepts `name`, `active`, and the same auto-budget fields. There is NO dedicated enable/disable endpoint; toggling `active` is done via this update. - `DELETE /api/v1/budgets/{id}` — delete. - `GET /api/v1/budgets/{id}/limits` — list budget limits (the period caps). - `POST /api/v1/budgets/{id}/limits` — create a limit. Accepts `start` (required, date, before end), `end` (required, after start), `amount` (required, positive), `currency_id`/`currency_code`, `notes`. BudgetLimit transformer fields include: `id`, `budget_id`, `start`, `end`, `amount`, `period`, `spent`, `currency_code`. - Transaction split accepts `budget_id` (and `budget_name`) — see `StoreRequest.php` lines 262-263. Firefly does NOT auto-create budgets from a transaction: the budget must already exist. So we resolve name -> id and pass `budget_id`. Budget names are unique per user (`uniqueObjectForUser:budgets,name`), so name->id resolution is unambiguous in practice; the resolver still errors hard on a miss, matching account handling. ## Commands New module `firefly_cli/commands/budget.py`. All handlers register via `@registry.command(...)` with the decorator immediately above the handler def (the v0.3.7 misbinding bug — keep decorator adjacent to `cmd_*`). | Command | HTTP | Args | |---|---|---| | `budget list` | `GET /budgets?start&end` | `--start`, `--end` (default: current month) | | `budget create ` | `POST /budgets` | `--active`/`--inactive` (default active), `--auto-budget-amount`, `--auto-budget-period`, `--auto-budget-type`, `--currency` | | `budget delete ` | `DELETE /budgets/{id}` | `--yes` (required, matches `tx delete`) | | `budget enable ` | `PUT /budgets/{id}` `{active:true}` | — | | `budget disable ` | `PUT /budgets/{id}` `{active:false}` | — | | `budget limit list ` | `GET /budgets/{id}/limits` | — | | `budget limit set ` | `POST /budgets/{id}/limits` | `--amount` (required), `--start`, `--end` (default current month), `--currency` | `` is a budget name OR id, resolved by the new `resolver.budget()`. `budget limit` is a two-level group (`budget limit list`, `budget limit set`), consistent with how the registry keys multi-word command names. ### tx add --budget Add one optional flag to `tx add`: `--budget ` — resolve name/id via `resolver.budget()`, set `budget_id` on the transaction split. Omitted = no budget (unchanged behavior). Not added to `tx edit` this release (YAGNI; add when a re-budgeting task needs it). ## Resolver Add `Resolver.budget(name_or_id)` mirroring `Resolver.account`: - numeric-looking arg -> fetch `GET /budgets/{id}` (via a `budget_by_id` path or inline), return the object; miss -> `ResolutionError`. - otherwise list budgets and match by name; ambiguous or missing -> `ResolutionError` listing candidates. Reuse the existing `_match` helper and `_list` pattern. `GET /budgets/{id}` returns a single budget; if Firefly returns a non-404 (e.g. the 401 quirk seen for unknown account ids) the resolver still fails closed. Add a `ponytail:` note only if the id path shows the same 401-for-unknown-id quirk in smoke. ## Period helper `_current_month()` -> `(first_iso, last_iso)` using `datetime.date`: first = `date.today().replace(day=1)`, last = first day of next month minus one day (via `calendar.monthrange`). Shared by `budget list` and `budget limit set` default range. Stdlib only. ## Output views (--human) JSON stays the default. Add to `output.py` `_VIEWS`: - budget view: columns `id`, `name`, `active`, `spent` (summed/trimmed like tx amounts), plus limit info if a limit is present in the range. - budget-limit view: `id`, `budget_id`, `start`, `end`, `amount`, `spent`, `period`. Match the existing `_VIEWS` signature-matching mechanism (a set of keys -> column list). If a resource shape doesn't match cleanly, fall back to raw JSON (existing behavior), no crash. ## Completion Regenerate `completions/firefly.bash` via `python scripts/gen_completion.py > completions/firefly.bash`. Add fixed-enum values to `FLAG_VALUES` in `gen_completion.py` for `--auto-budget-type` (reset|rollover|adjusted|none), `--auto-budget-period`, and the `--active/--inactive` pair. ## Errors All failures are `FireflyError` subclasses (existing contract): unresolved budget ref -> `ResolutionError` with candidates; a missing required `--amount` on `limit set` -> argparse error; Firefly 4xx/5xx -> surfaced by `client.py`. `budget delete` without `--yes` -> hard error, no call made. ## Testing - Unit tests under `tests/unit/` per command, mocking `ctx.client` / `ctx.resolver` (the established pattern). Cover: list with/without period, create (active + inactive + auto-budget), delete (with and without --yes), enable/disable body shape, limit list, limit set (default + explicit period), tx add --budget sets `budget_id`, resolver.budget name + id + miss + ambiguous. - One handler-registration regression test asserting each registered `budget*` command binds to its `cmd_*` (registry misbinding is invisible to direct-call unit tests — the v0.3.7 lesson). - Live smoke against the test instance (source `~/.config/firefly-cli/test-creds.env`) after implementation: create a budget, set a limit, list, assign via tx add (create-then-delete own data), because the mocked suite bypasses the registry. ## SKILL.md Document the new `budget` group, name->id resolution for budgets (must pre-exist, not auto-created), the current-month default period, and the `tx add --budget` flag. ## Versioning New command group + new optional flag, contract-additive -> MINOR bump. Bump `pyproject.toml` and `firefly_cli/__init__.py` together, tag `vX.Y.Z` signed, push `--follow-tags`. ## Out of scope (YAGNI) - `budget update` for rename / auto-budget edits (delete+recreate, or web UI). - `budget limit update` / `budget limit delete`. - `tx edit --budget`. - available-budgets endpoints, object-groups. Add any of these when a concrete task needs it.