aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-07-03-budget-management-design.md
blob: 854425fef8c617dcff385425b6c9da0e08a689df (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
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.