spending: prefill annual £ from actualbudget trailing 12mo
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

Adds a thin read-only client for the actualbudget HTTP API
(`fire_planner/actualbudget.py`) and a `GET /spending/annual` endpoint
that returns trailing-N-month spending broken out by category group.

Default exclusions ("Investments and Savings", "Budget Reset") strip
out wealth transfers so the headline number reflects actual
consumption — for Viktor's data, ~£41k/yr instead of the raw £210k
total. Caller can pass `?exclude=...` to override.

Frontend uses the headline `total_gbp` to autofill the Annual spending
input (same pattern as nw_seed from networth), with a small
provenance line below the input showing the window + which groups
were excluded.

Auth: 3 new env vars (ACTUALBUDGET_API_URL/KEY/SYNC_ID) sourced from
Vault `secret/fire-planner` via the existing ExternalSecret —
infra/stacks/fire-planner applied separately. Backend silently keeps
the hardcoded default if the upstream is unreachable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-05-10 11:11:51 +00:00
parent 2c51954790
commit 3bfa46ad4f
8 changed files with 617 additions and 8 deletions

View file

@ -133,6 +133,35 @@ class NetWorthHistory(BaseModel):
points: list[NetWorthHistoryPoint]
# ── annual spending (from actualbudget) ──────────────────────────────
class SpendingMonth(BaseModel):
"""One month's outflows (positive £) by category group, after
income groups have been dropped upstream."""
month: str # "YYYY-MM"
by_group: dict[str, Decimal]
total_gbp: Decimal
class AnnualSpending(BaseModel):
"""Aggregated trailing-N-month spending pulled from actualbudget.
`total_gbp` is what we suggest as the "Annual spending" default in
the WhatIf form: the sum of all category groups across the trailing
window, *minus* whichever groups the caller asked to exclude
(default: investments/savings transfers, which aren't consumption).
"""
months: int
window_start: str # "YYYY-MM" (oldest month included)
window_end: str # "YYYY-MM" (newest)
excluded_groups: list[str]
total_gbp: Decimal # the headline number
raw_total_gbp: Decimal # before exclusions, for transparency
by_group_total_gbp: dict[str, Decimal] # 12-mo group sums (incl. excluded)
monthly: list[SpendingMonth]
# ── life events ──────────────────────────────────────────────────────