whatif: live data refresh, inflation-adjusted spending, legend fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

Three follow-ups to the actualbudget integration:

**Always-fresh autofill.** Drop the one-shot `*AutoFilled` boolean
gates; replace with `nwUserEdited` / `spendingUserEdited` flags. Until
the user types into either field, every refetch (mount, window
focus) updates the form value. Once they edit, we leave it alone. A
small ↻ button next to each anchor input flips the edited flag back
off so the user can re-snap to live data on demand. React Query
configured with staleTime=0 + refetchOnMount='always' +
refetchOnWindowFocus=true so the cache never serves stale numbers.
NW provenance shows the snapshot date.

**Inflation-adjusted spending.** Backend now revalues each trailing
month's nominal pence forward to today's £ using monthly compounding
of `inflation_pct` (default 0.03 ≈ UK CPI 2024-26). Headline
`total_gbp` is the real-£ figure — matches the simulator's
real-GBP convention. Response also includes `nominal_total_gbp` and
`inflation_pct` for transparency. New /spending/annual?inflation_pct=
override param. 10/10 actualbudget tests pass.

**FanChart legend.** The bottom-anchored legend was overlapping the
x-axis label. Moved to top: 8 with itemGap=18 + type=scroll for
narrow viewports; bumped grid top→48 / bottom→56 + xAxis nameGap→28
so nothing collides.

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

View file

@ -100,15 +100,44 @@ async def test_fetch_trailing_spending_excludes_groups(respx_mock: respx.MockRou
}
}))
spends, total_gbp = await fetch_trailing_spending(
spends, nominal_gbp, real_gbp = await fetch_trailing_spending(
months=2,
exclude_groups=frozenset(["Investments and Savings"]),
annual_inflation_pct=0.0, # no adjustment for the simple assertion
client_factory=_client(),
today=date(2025, 3, 15),
)
assert [ms.month for ms in spends] == ["2025-01", "2025-02"]
# Usual: 1000 + 2000 = 3000 pence = £30
assert total_gbp == Decimal("30.00")
assert nominal_gbp == Decimal("30.00")
assert real_gbp == Decimal("30.00") # 0% inflation → same number
@pytest.mark.asyncio
async def test_fetch_trailing_spending_inflation_adjusts(respx_mock: respx.MockRouter) -> None:
"""At 12% annual inflation, a £10 expense from 12 months ago is
worth £11.20 in today's £. Sanity-check the compounding via the
oldest month of a 12-month window (today=2025-04 2024-04 is 12mo
old)."""
# Only one month available in the window.
respx_mock.get(f"{API_URL}/v1/budgets/{SYNC_ID}/months").mock(
return_value=httpx.Response(200, json={"data": ["2024-04"]}))
respx_mock.get(f"{API_URL}/v1/budgets/{SYNC_ID}/months/2024-04").mock(
return_value=httpx.Response(200, json={
"data": {"categoryGroups": [
{"name": "Usual Expenses", "is_income": False,
"categories": [{"spent": -1000}]}, # £10
]}
}))
_, nominal_gbp, real_gbp = await fetch_trailing_spending(
months=12,
exclude_groups=frozenset(),
annual_inflation_pct=0.12,
client_factory=_client(),
today=date(2025, 4, 15),
)
assert nominal_gbp == Decimal("10.00")
assert real_gbp == Decimal("11.20")
@pytest.mark.asyncio
@ -126,11 +155,12 @@ async def test_fetch_trailing_spending_drops_unavailable_months(
"categories": [{"spent": -5000}]},
]}
}))
spends, total_gbp = await fetch_trailing_spending(
spends, nominal_gbp, _real_gbp = await fetch_trailing_spending(
months=3,
exclude_groups=frozenset(),
annual_inflation_pct=0.0,
client_factory=_client(),
today=date(2025, 3, 15),
)
assert [ms.month for ms in spends] == ["2025-02"]
assert total_gbp == Decimal("50.00")
assert nominal_gbp == Decimal("50.00")