Give the analytics overview a per-parent, per-year balance series
Each parent projection now carries one entry per calendar year from birth to the fourth birthday: vacation earned, carried in, planned, left, carried out and expiring under Semesterlagen's 20-day floor and 5-year carry-over, plus parental-leave days used and left at year end with the age-4 clamp applied in the birthday year. The overview sums them into per-year totals. Frontend types, mapper and fixtures follow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBRLomdLK9jF7wU7a8AA6H
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""API-level tests for `GET /api/analytics/{workspace_id}/overview` and the
|
||||
|
||||
per-parent age4 endpoint — specifically that the new per-year balance series
|
||||
(`year_balances`/`year_totals`) survives the wire boundary (§4.2's
|
||||
service-computed-but-dropped-on-the-wire gotcha), matching the
|
||||
`test_calendar_router.py` pattern.
|
||||
"""
|
||||
|
||||
from datetime import date, datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from httpx import AsyncClient
|
||||
|
||||
from app.entities.child import Child
|
||||
from app.repositories_impl.sqlite.child_repository import SQLiteChildRepository
|
||||
from tests.conftest import SeededIds
|
||||
|
||||
|
||||
class TestGetOverviewYearBalances:
|
||||
async def test_wire_shape_includes_year_balances_and_year_totals(
|
||||
self,
|
||||
authed_client: AsyncClient,
|
||||
child_repo: SQLiteChildRepository,
|
||||
seeded_ids: SeededIds,
|
||||
) -> None:
|
||||
ws = seeded_ids.workspace_id
|
||||
pd_uuid = seeded_ids.personal_data_uuid
|
||||
birth = date(2025, 1, 1)
|
||||
child = await child_repo.create(
|
||||
Child(
|
||||
id=uuid4(),
|
||||
workspace_id=ws,
|
||||
name="Kid",
|
||||
birth_date=birth,
|
||||
created_at=datetime.now(),
|
||||
)
|
||||
)
|
||||
|
||||
response = await authed_client.get(
|
||||
f"/api/analytics/{ws}/overview", params={"child_id": str(child.id)}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
|
||||
assert "year_totals" in body
|
||||
assert [yt["year"] for yt in body["year_totals"]] == [2025, 2026, 2027, 2028, 2029]
|
||||
for yt in body["year_totals"]:
|
||||
assert set(yt) == {"year", "pl_left", "vacation_left"}
|
||||
|
||||
assert len(body["parent_projections"]) == 1
|
||||
projection = body["parent_projections"][0]
|
||||
assert projection["personal_data_uuid"] == str(pd_uuid)
|
||||
assert "year_balances" in projection
|
||||
assert [yb["year"] for yb in projection["year_balances"]] == [
|
||||
2025,
|
||||
2026,
|
||||
2027,
|
||||
2028,
|
||||
2029,
|
||||
]
|
||||
|
||||
birth_year = projection["year_balances"][0]
|
||||
assert birth_year["personal_data_uuid"] == str(pd_uuid)
|
||||
assert birth_year["child_id"] == str(child.id)
|
||||
|
||||
vacation = birth_year["vacation"]
|
||||
assert set(vacation) == {
|
||||
"year",
|
||||
"earned",
|
||||
"is_assumed",
|
||||
"carried_in",
|
||||
"planned_use",
|
||||
"left",
|
||||
"carried_out",
|
||||
"expiring",
|
||||
}
|
||||
assert vacation["earned"] == 25 # statutory default — no vacation_days row entered
|
||||
assert vacation["is_assumed"] is True
|
||||
assert vacation["carried_in"] == 0
|
||||
assert vacation["planned_use"] == 0
|
||||
assert vacation["left"] == 25
|
||||
|
||||
parental_leave = birth_year["parental_leave"]
|
||||
assert set(parental_leave) == {
|
||||
"year",
|
||||
"sjukpenning_used",
|
||||
"lagsta_used",
|
||||
"sjukpenning_left",
|
||||
"lagsta_left",
|
||||
}
|
||||
# `seeded_ids`'s workspace has exactly one member -> full pool.
|
||||
assert parental_leave["sjukpenning_left"] == 390
|
||||
assert parental_leave["lagsta_left"] == 90
|
||||
Reference in New Issue
Block a user