diff options
Diffstat (limited to 'docs/superpowers/specs/2026-07-03-budget-management-design.md')
| -rw-r--r-- | docs/superpowers/specs/2026-07-03-budget-management-design.md | 156 |
1 files changed, 156 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-07-03-budget-management-design.md b/docs/superpowers/specs/2026-07-03-budget-management-design.md new file mode 100644 index 0000000..854425f --- /dev/null +++ b/docs/superpowers/specs/2026-07-03-budget-management-design.md @@ -0,0 +1,156 @@ +# 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 <name>` | `POST /budgets` | `--active`/`--inactive` (default active), `--auto-budget-amount`, `--auto-budget-period`, `--auto-budget-type`, `--currency` | +| `budget delete <ref>` | `DELETE /budgets/{id}` | `--yes` (required, matches `tx delete`) | +| `budget enable <ref>` | `PUT /budgets/{id}` `{active:true}` | — | +| `budget disable <ref>` | `PUT /budgets/{id}` `{active:false}` | — | +| `budget limit list <ref>` | `GET /budgets/{id}/limits` | — | +| `budget limit set <ref>` | `POST /budgets/{id}/limits` | `--amount` (required), `--start`, `--end` (default current month), `--currency` | + +`<ref>` 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 <ref>` — 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. |
