whatif: live data refresh, inflation-adjusted spending, legend fix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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:
parent
3bfa46ad4f
commit
e12e8f9290
8 changed files with 263 additions and 68 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -69,18 +69,20 @@ async def test_spending_annual_excludes_investments_by_default(client: AsyncClie
|
|||
_mock_two_months(rmock)
|
||||
# The endpoint asks for 12 months by default but we mocked only 2;
|
||||
# the remaining 10 are silently dropped (list_months only returns 2).
|
||||
resp = await client.get("/spending/annual?months=2")
|
||||
resp = await client.get("/spending/annual?months=2&inflation_pct=0")
|
||||
assert resp.status_code == 200, resp.text
|
||||
body = resp.json()
|
||||
# Usual: 3000 + 4000 = 7000 pence = £70
|
||||
# 0% inflation → real == nominal. Usual: 3000 + 4000 = 7000p = £70
|
||||
assert Decimal(body["total_gbp"]) == Decimal("70.00")
|
||||
# Raw includes investments: 7000 + 60000 = 67000 pence = £670
|
||||
assert Decimal(body["nominal_total_gbp"]) == Decimal("70.00")
|
||||
# Raw includes investments: 7000 + 60000 = 67000p = £670
|
||||
assert Decimal(body["raw_total_gbp"]) == Decimal("670.00")
|
||||
assert body["window_start"] == "2025-01"
|
||||
assert body["window_end"] == "2025-02"
|
||||
assert body["excluded_groups"] == ["Investments and Savings", "Budget Reset"]
|
||||
assert body["by_group_total_gbp"]["Usual Expenses"] == "70.00"
|
||||
assert body["by_group_total_gbp"]["Investments and Savings"] == "600.00"
|
||||
assert Decimal(body["inflation_pct"]) == Decimal("0")
|
||||
|
||||
|
||||
async def test_spending_annual_custom_exclude(client: AsyncClient) -> None:
|
||||
|
|
@ -93,7 +95,9 @@ async def test_spending_annual_custom_exclude(client: AsyncClient) -> None:
|
|||
patch("fire_planner.actualbudget._today", lambda: date(2025, 3, 15)), \
|
||||
respx.mock(assert_all_called=False) as rmock:
|
||||
_mock_two_months(rmock)
|
||||
resp = await client.get("/spending/annual?months=2&exclude=Usual%20Expenses")
|
||||
resp = await client.get(
|
||||
"/spending/annual?months=2&exclude=Usual%20Expenses&inflation_pct=0"
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
# Excluding "Usual Expenses" leaves only Investments: £600
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue