Cover the balance entry path and three unguarded rules

Entering a parent's Försäkringskassan balances had no tests at all, despite
being how every number in the planner originates and despite it becoming the
first thing a new user does. The scraper endpoints are pinned per test rather
than read from the ambient configuration, because a local .env sets the flag
opposite to the shipped default and a test that trusts it would pass here and
fail in CI.

Also covered: the hard expiry at twelve, whose rejection path nothing reached
and whose boundary turns out to be inclusive; and the dubbeldagar count cap,
which now drives past the limit and asserts the days beyond it fall back to a
single parent. Two weak assertions were strengthened, including one that
derived its expectation from the same response it was checking and so could
not have failed on a shared error.
This commit is contained in:
2026-09-17 19:46:03 +02:00
parent 9d548da7d6
commit 0fc71101ae
6 changed files with 732 additions and 2 deletions
+43 -2
View File
@@ -34,6 +34,7 @@ from dateutil.easter import easter
from dateutil.relativedelta import relativedelta
from httpx import ASGITransport, AsyncClient
from app.config import settings
from app.dependencies import get_connection
from app.main import app
from app.utils.holidays import cached_year_bounds
@@ -687,6 +688,38 @@ class TestAge4ProjectionArithmeticIsSelfConsistent:
async def test_expiring_equals_projected_minus_ceiling_clamped_at_zero(
self, client: AsyncClient, connection: aiosqlite.Connection
) -> None:
"""Independently-derived expected values, not internal consistency.
The original version of this test re-derived its expected
`expiring_at_b4` from `projection["projected_left_at_b4"]` -- a field
of the SAME response it was checking -- so it only proved the two
response fields agree with each other, not that either is correct
(a shared bug in both would still pass; the test's own docstring
said as much).
This version instead computes the expected numbers from the seeded
fixture and `settings.rules` directly, independent of the response.
The `/api/plan/propose` response's `age4_projections` come from
`allocation_service._Ledger._age4_projections`, not from
`AnalyticsService.get_days_left_at_age_4` -- that method always
passes `days_used=0` and derives `total_initial` from the
(parent, child) pool `AllocationInputLoader.load` built
(`allocation_service.py:1446-1475`). No wish is created in this
test, so nothing gets proposed and `planned_fp_before_b4` is 0.
With no `ParentalLeaveFetch` snapshot for this brand-new
registration, that pool falls back to `rules.sjukpenning_days // 2 +
rules.lagsta_niva_days // 2`, which is `rules.days_per_parent`
exactly (the "Bug A" default the two must agree on -- see
`test_planning_service_apply.py`'s
`test_no_snapshot_default_balance_matches_allocation_engine`) --
making `projected_left_at_b4 == rules.days_per_parent`. The
workspace is a fresh single-member 'solo' workspace (never set to
'couple' here), so the per-CHILD cap
(`rules.max_savable_days_at_age_4`) applies via
`resolve_age4_savable_cap`, not the per-parent one -- making
`expiring_at_b4 == max(rules.days_per_parent -
rules.max_savable_days_at_age_4, 0)`.
"""
headers, ws_id, pd_uuid = await _register(client, "age4-projection@example.com", "Alex")
birth_date = date.today() - relativedelta(months=6)
child_id = await _add_child(client, headers, ws_id, "Barn", birth_date)
@@ -695,11 +728,19 @@ class TestAge4ProjectionArithmeticIsSelfConsistent:
proposal = await _propose(client, headers, ws_id)
rules = settings.rules
expected_cap = rules.max_savable_days_at_age_4
expected_projected_left = rules.days_per_parent
expected_expiring = max(expected_projected_left - expected_cap, 0)
expected_savable = min(expected_projected_left, expected_cap)
assert proposal["age4_projections"], "expected an age4 projection for the (parent, child)"
for projection in proposal["age4_projections"]:
assert projection["child_id"] == str(child_id)
expected = max(projection["projected_left_at_b4"] - 96, 0)
assert projection["expiring_at_b4"] == expected, projection
assert projection["savable_cap_at_b4"] == expected_cap, projection
assert projection["projected_left_at_b4"] == expected_projected_left, projection
assert projection["expiring_at_b4"] == expected_expiring, projection
assert projection["savable_after_b4"] == expected_savable, projection
# ---------------------------------------------------------------------------