Rework balances around per-parent days left and deduce the partner from FK
Manual entry now takes one parent's sjukpenningnivå, lägstanivå and dubbeldagar left (plus optional reserved days) and derives the rest in build_snapshot, shared with the scraper. The scraper also reads the family panel and exposes a partner deduction; with two parents it is saved as a 'deduced' snapshot (migration 002). Parent count is the single source of entitlement: one member gets 480 days and 96 savable at age 4, two get 240 and 48 each. Workspaces are capped at two parents. Also lands the in-flight squash of migrations 002–011 into the initial schema and the removal of planning_mode. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -129,11 +129,14 @@ class TestAge4Projection:
|
||||
)
|
||||
proj = await service.get_days_left_at_age_4(pd_uuid, child.id)
|
||||
|
||||
assert proj.total_days == 240 # default entitlement
|
||||
# `seeded_ids`'s workspace has exactly one member -> a sole parent's
|
||||
# default entitlement is the FULL pool (480), not a partnered
|
||||
# parent's half-share (240) -- see `default_tier_pool_share`.
|
||||
assert proj.total_days == 480 # default entitlement
|
||||
assert proj.days_used == 0
|
||||
assert proj.projected_left_at_b4 == 240
|
||||
assert proj.projected_left_at_b4 == 480
|
||||
assert proj.savable_after_b4 == 96
|
||||
assert proj.expiring_at_b4 == 144 # 240 - 96
|
||||
assert proj.expiring_at_b4 == 384 # 480 - 96
|
||||
|
||||
async def test_leap_year_birthday(
|
||||
self,
|
||||
@@ -355,9 +358,7 @@ class TestCoupleWorkspacePerParentCap:
|
||||
from app.entities.workspace import Workspace
|
||||
|
||||
now = datetime.now()
|
||||
ws = await workspace_repo.create(
|
||||
Workspace(id=uuid4(), name="Familj", planning_mode="couple", created_at=now)
|
||||
)
|
||||
ws = await workspace_repo.create(Workspace(id=uuid4(), name="Familj", created_at=now))
|
||||
parent_a = await personal_data_repo.create(
|
||||
PersonalData(uuid=uuid4(), workspace_id=ws.id, name="A", created_at=now)
|
||||
)
|
||||
@@ -394,7 +395,7 @@ class TestCoupleWorkspacePerParentCap:
|
||||
assert combined_savable == 96 # never 192 — this is the headline regression
|
||||
assert overview.combined_expiring_at_b4 == 384 # (240-48) * 2
|
||||
|
||||
async def test_couple_mode_with_single_parent_falls_back_to_96(
|
||||
async def test_single_member_workspace_falls_back_to_96(
|
||||
self,
|
||||
child_repo: SQLiteChildRepository,
|
||||
parental_leave_repo: SQLiteParentalLeaveRepository,
|
||||
@@ -402,18 +403,16 @@ class TestCoupleWorkspacePerParentCap:
|
||||
personal_data_repo: SQLitePersonalDataRepository,
|
||||
workspace_repo: SQLiteWorkspaceRepository,
|
||||
) -> None:
|
||||
"""Safety belt: `planning_mode == 'couple'` with only one member so
|
||||
"""Safety belt: a workspace with only one member so far (partner not
|
||||
|
||||
far (partner not added yet) must still use the per-child (96)
|
||||
figure — there is no second parent to divide it with.
|
||||
added yet) must still use the per-child (96) figure — there is no
|
||||
second parent to divide it with.
|
||||
"""
|
||||
from app.entities.personal_data import PersonalData
|
||||
from app.entities.workspace import Workspace
|
||||
|
||||
now = datetime.now()
|
||||
ws = await workspace_repo.create(
|
||||
Workspace(id=uuid4(), name="Familj", planning_mode="couple", created_at=now)
|
||||
)
|
||||
ws = await workspace_repo.create(Workspace(id=uuid4(), name="Familj", created_at=now))
|
||||
parent = await personal_data_repo.create(
|
||||
PersonalData(uuid=uuid4(), workspace_id=ws.id, name="A", created_at=now)
|
||||
)
|
||||
@@ -432,6 +431,39 @@ class TestCoupleWorkspacePerParentCap:
|
||||
assert proj.expiring_at_b4 == 144 # 240 - 96
|
||||
|
||||
|
||||
class TestSingleParentWorkspaceCap:
|
||||
"""The age-4 cap is purely a function of parent count (no separate
|
||||
|
||||
"planning mode" flag exists): a workspace with a single member gets the
|
||||
full per-child figure (96); `TestCoupleWorkspacePerParentCap` above
|
||||
covers the two-parent 48/48 split.
|
||||
"""
|
||||
|
||||
async def test_genuine_single_parent_workspace_gets_96(
|
||||
self,
|
||||
child_repo: SQLiteChildRepository,
|
||||
parental_leave_repo: SQLiteParentalLeaveRepository,
|
||||
planned_leave_repo: SQLitePlannedLeaveRepository,
|
||||
personal_data_repo: SQLitePersonalDataRepository,
|
||||
workspace_repo: SQLiteWorkspaceRepository,
|
||||
seeded_ids: SeededIds,
|
||||
) -> None:
|
||||
ws = seeded_ids.workspace_id
|
||||
pd_uuid = seeded_ids.personal_data_uuid
|
||||
birth = date.today() + relativedelta(years=2)
|
||||
child = _make_child(ws, birth)
|
||||
await child_repo.create(child)
|
||||
fetch = _make_fetch(pd_uuid, ws, child.id, total=240, used=0)
|
||||
await parental_leave_repo.create(fetch)
|
||||
|
||||
service = await _make_service(
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo, workspace_repo
|
||||
)
|
||||
proj = await service.get_days_left_at_age_4(pd_uuid, child.id)
|
||||
assert proj.savable_cap_at_b4 == 96
|
||||
assert proj.savable_after_b4 == 96
|
||||
|
||||
|
||||
class TestAgreesWithPlanProposalForTheSameCommittedState:
|
||||
"""Bug A regression: the Dashboard (`AnalyticsService`) and the Plan tab's
|
||||
propose-time projection (`allocation_service._Ledger.build_age4_projections`,
|
||||
@@ -439,13 +471,14 @@ class TestAgreesWithPlanProposalForTheSameCommittedState:
|
||||
`expiring_at_b4` for a parent with no FK snapshot yet and no leave wishes
|
||||
(so the proposal adds nothing new) — i.e. the *committed* state.
|
||||
|
||||
Before the fix, `AllocationInputLoader`'s no-snapshot fallback assumed a
|
||||
single parent was entitled to the WHOLE child's combined tier totals
|
||||
(`rules.sjukpenning_days` + `rules.lagsta_niva_days` == 390 + 90 == 480)
|
||||
instead of that parent's fair half-share (195 + 45 == 240, matching
|
||||
`AnalyticsService`'s own fallback, `rules.days_per_parent`). That made the
|
||||
Plan tab report roughly double what the Dashboard showed for the exact
|
||||
same (empty) committed plan — this test pins the two together.
|
||||
Both surfaces derive their no-snapshot default through the same shared
|
||||
`app.utils.tip_predicates.default_tier_pool_share(rules, parent_count)`
|
||||
helper, so they can never disagree again: a genuinely SOLE parent (this
|
||||
workspace has exactly one member, "Sam") is entitled to the WHOLE
|
||||
child's combined tier totals (`rules.sjukpenning_days` +
|
||||
`rules.lagsta_niva_days` == 390 + 90 == 480) since there's no partner to
|
||||
split the pool with; a partnered parent would instead get their fair
|
||||
half-share (195 + 45 == 240). This test pins the two surfaces together.
|
||||
"""
|
||||
|
||||
async def test_no_snapshot_no_wishes_matches_dashboard(
|
||||
@@ -463,9 +496,7 @@ class TestAgreesWithPlanProposalForTheSameCommittedState:
|
||||
from app.services.allocation_service import AllocationInputLoader, Allocator
|
||||
|
||||
now = datetime.now()
|
||||
ws = await workspace_repo.create(
|
||||
Workspace(id=uuid4(), name="Familj", planning_mode="couple", created_at=now)
|
||||
)
|
||||
ws = await workspace_repo.create(Workspace(id=uuid4(), name="Familj", created_at=now))
|
||||
parent = await personal_data_repo.create(
|
||||
PersonalData(
|
||||
uuid=uuid4(),
|
||||
@@ -512,5 +543,5 @@ class TestAgreesWithPlanProposalForTheSameCommittedState:
|
||||
assert dashboard_proj.expiring_at_b4 == plan_proj.expiring_at_b4
|
||||
# Pin the actual figures too, so a future regression of either
|
||||
# fallback (240 vs. 480) fails loudly rather than merely disagreeing.
|
||||
assert dashboard_proj.projected_left_at_b4 == 240
|
||||
assert dashboard_proj.expiring_at_b4 == 144
|
||||
assert dashboard_proj.projected_left_at_b4 == 480
|
||||
assert dashboard_proj.expiring_at_b4 == 384
|
||||
|
||||
Reference in New Issue
Block a user