Link planned days to their wish, fix the age-4 cap, drop the planning goal
Three changes that together unblock treating a calendar period as one editable object. Migration 008 adds `planned_leave_day.wish_id` (nullable, ON DELETE SET NULL) so a persisted day knows which `leave_wish` produced it, `is_pinned` to mark a day the user hand-edited, and `leave_wish.target_days` so a period drawn on a calendar need not be a whole number of weeks. Apply-time identity still keys on day_type/is_dubbeldag only, so a wish_id-only difference is a metadata update rather than a collision. The age-4 savable ceiling was applying the per-child figure of 96 to each parent's pool, modelling a two-parent family as able to save 192 days past the child's fourth birthday. `resolve_age4_savable_cap` is now the single place that decides 48 (couple with two parents) versus 96 (everyone else), and both AnalyticsService and the allocator read it. Migration 009 drops `workspace.planning_goal` and `leave_wish.goal_override`. The allocator's tier-3 policy collapses to one rule; `salary_type` still branches, since that reflects what a vacation day actually costs a parent rather than a user preference.
This commit is contained in:
@@ -70,8 +70,9 @@ async def _make_service(
|
||||
leave_repo: SQLiteParentalLeaveRepository,
|
||||
planned_repo: SQLitePlannedLeaveRepository,
|
||||
pd_repo: SQLitePersonalDataRepository,
|
||||
workspace_repo: SQLiteWorkspaceRepository,
|
||||
) -> AnalyticsService:
|
||||
return AnalyticsService(leave_repo, planned_repo, child_repo, pd_repo)
|
||||
return AnalyticsService(leave_repo, planned_repo, child_repo, pd_repo, workspace_repo)
|
||||
|
||||
|
||||
class TestAge4Projection:
|
||||
@@ -81,6 +82,7 @@ class TestAge4Projection:
|
||||
parental_leave_repo: SQLiteParentalLeaveRepository,
|
||||
planned_leave_repo: SQLitePlannedLeaveRepository,
|
||||
personal_data_repo: SQLitePersonalDataRepository,
|
||||
workspace_repo: SQLiteWorkspaceRepository,
|
||||
seeded_ids: SeededIds,
|
||||
) -> None:
|
||||
ws = seeded_ids.workspace_id
|
||||
@@ -95,7 +97,7 @@ class TestAge4Projection:
|
||||
await parental_leave_repo.create(fetch)
|
||||
|
||||
service = await _make_service(
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
||||
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)
|
||||
|
||||
@@ -104,6 +106,7 @@ class TestAge4Projection:
|
||||
assert proj.projected_left_at_b4 == 140 # 240 - 100
|
||||
assert proj.savable_after_b4 == 96
|
||||
assert proj.expiring_at_b4 == 44 # 140 - 96
|
||||
assert proj.savable_cap_at_b4 == 96 # solo workspace -> per-child ceiling
|
||||
|
||||
async def test_no_fetch_data_assumes_full_entitlement(
|
||||
self,
|
||||
@@ -111,6 +114,7 @@ class TestAge4Projection:
|
||||
parental_leave_repo: SQLiteParentalLeaveRepository,
|
||||
planned_leave_repo: SQLitePlannedLeaveRepository,
|
||||
personal_data_repo: SQLitePersonalDataRepository,
|
||||
workspace_repo: SQLiteWorkspaceRepository,
|
||||
seeded_ids: SeededIds,
|
||||
) -> None:
|
||||
ws = seeded_ids.workspace_id
|
||||
@@ -121,7 +125,7 @@ class TestAge4Projection:
|
||||
|
||||
# No FK data entered
|
||||
service = await _make_service(
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
||||
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)
|
||||
|
||||
@@ -137,6 +141,7 @@ class TestAge4Projection:
|
||||
parental_leave_repo: SQLiteParentalLeaveRepository,
|
||||
planned_leave_repo: SQLitePlannedLeaveRepository,
|
||||
personal_data_repo: SQLitePersonalDataRepository,
|
||||
workspace_repo: SQLiteWorkspaceRepository,
|
||||
seeded_ids: SeededIds,
|
||||
) -> None:
|
||||
ws = seeded_ids.workspace_id
|
||||
@@ -147,7 +152,7 @@ class TestAge4Projection:
|
||||
await child_repo.create(child)
|
||||
|
||||
service = await _make_service(
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
||||
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)
|
||||
|
||||
@@ -160,6 +165,7 @@ class TestAge4Projection:
|
||||
parental_leave_repo: SQLiteParentalLeaveRepository,
|
||||
planned_leave_repo: SQLitePlannedLeaveRepository,
|
||||
personal_data_repo: SQLitePersonalDataRepository,
|
||||
workspace_repo: SQLiteWorkspaceRepository,
|
||||
seeded_ids: SeededIds,
|
||||
) -> None:
|
||||
ws = seeded_ids.workspace_id
|
||||
@@ -172,7 +178,7 @@ class TestAge4Projection:
|
||||
await parental_leave_repo.create(fetch)
|
||||
|
||||
service = await _make_service(
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
||||
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)
|
||||
|
||||
@@ -186,11 +192,12 @@ class TestAge4Projection:
|
||||
parental_leave_repo: SQLiteParentalLeaveRepository,
|
||||
planned_leave_repo: SQLitePlannedLeaveRepository,
|
||||
personal_data_repo: SQLitePersonalDataRepository,
|
||||
workspace_repo: SQLiteWorkspaceRepository,
|
||||
) -> None:
|
||||
from app.utils.errors import NotFoundError
|
||||
|
||||
service = await _make_service(
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo, workspace_repo
|
||||
)
|
||||
with pytest.raises(NotFoundError):
|
||||
await service.get_days_left_at_age_4(uuid4(), uuid4())
|
||||
@@ -203,11 +210,12 @@ class TestValidateDubbeldagar:
|
||||
parental_leave_repo: SQLiteParentalLeaveRepository,
|
||||
planned_leave_repo: SQLitePlannedLeaveRepository,
|
||||
personal_data_repo: SQLitePersonalDataRepository,
|
||||
workspace_repo: SQLiteWorkspaceRepository,
|
||||
) -> None:
|
||||
from app.utils.errors import NotFoundError
|
||||
|
||||
service = await _make_service(
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo, workspace_repo
|
||||
)
|
||||
with pytest.raises(NotFoundError):
|
||||
await service.validate_dubbeldagar(uuid4(), date.today())
|
||||
@@ -218,6 +226,7 @@ class TestValidateDubbeldagar:
|
||||
parental_leave_repo: SQLiteParentalLeaveRepository,
|
||||
planned_leave_repo: SQLitePlannedLeaveRepository,
|
||||
personal_data_repo: SQLitePersonalDataRepository,
|
||||
workspace_repo: SQLiteWorkspaceRepository,
|
||||
seeded_ids: SeededIds,
|
||||
) -> None:
|
||||
from app.utils.errors import DubbeldagError
|
||||
@@ -228,7 +237,7 @@ class TestValidateDubbeldagar:
|
||||
await child_repo.create(child)
|
||||
|
||||
service = await _make_service(
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo, workspace_repo
|
||||
)
|
||||
# Date beyond the 15-month window
|
||||
beyond_window = birth + relativedelta(months=16)
|
||||
@@ -241,6 +250,7 @@ class TestValidateDubbeldagar:
|
||||
parental_leave_repo: SQLiteParentalLeaveRepository,
|
||||
planned_leave_repo: SQLitePlannedLeaveRepository,
|
||||
personal_data_repo: SQLitePersonalDataRepository,
|
||||
workspace_repo: SQLiteWorkspaceRepository,
|
||||
seeded_ids: SeededIds,
|
||||
) -> None:
|
||||
from app.config import settings
|
||||
@@ -269,7 +279,7 @@ class TestValidateDubbeldagar:
|
||||
)
|
||||
|
||||
service = await _make_service(
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo, workspace_repo
|
||||
)
|
||||
within_window = birth + relativedelta(months=1)
|
||||
with pytest.raises(DubbeldagError):
|
||||
@@ -283,6 +293,7 @@ class TestGetOverview:
|
||||
parental_leave_repo: SQLiteParentalLeaveRepository,
|
||||
planned_leave_repo: SQLitePlannedLeaveRepository,
|
||||
personal_data_repo: SQLitePersonalDataRepository,
|
||||
workspace_repo: SQLiteWorkspaceRepository,
|
||||
seeded_ids: SeededIds,
|
||||
) -> None:
|
||||
ws = seeded_ids.workspace_id
|
||||
@@ -296,7 +307,7 @@ class TestGetOverview:
|
||||
await parental_leave_repo.create(fetch)
|
||||
|
||||
service = await _make_service(
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo, workspace_repo
|
||||
)
|
||||
overview = await service.get_overview(ws, child.id)
|
||||
|
||||
@@ -312,16 +323,115 @@ class TestGetOverview:
|
||||
parental_leave_repo: SQLiteParentalLeaveRepository,
|
||||
planned_leave_repo: SQLitePlannedLeaveRepository,
|
||||
personal_data_repo: SQLitePersonalDataRepository,
|
||||
workspace_repo: SQLiteWorkspaceRepository,
|
||||
) -> None:
|
||||
from app.utils.errors import NotFoundError
|
||||
|
||||
service = await _make_service(
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo, workspace_repo
|
||||
)
|
||||
with pytest.raises(NotFoundError):
|
||||
await service.get_overview(uuid4(), uuid4())
|
||||
|
||||
|
||||
class TestCoupleWorkspacePerParentCap:
|
||||
"""Headline regression: a two-parent `couple` workspace must cap each
|
||||
|
||||
parent's own savable balance at 48 (not 96), so the combined savable
|
||||
figure across both parents tops out at 96 total — never 192. This is
|
||||
the bug this task exists to fix: the age-4 ceiling is per CHILD, not
|
||||
per PARENT pool.
|
||||
"""
|
||||
|
||||
async def test_two_parent_couple_caps_48_each_96_combined(
|
||||
self,
|
||||
child_repo: SQLiteChildRepository,
|
||||
parental_leave_repo: SQLiteParentalLeaveRepository,
|
||||
planned_leave_repo: SQLitePlannedLeaveRepository,
|
||||
personal_data_repo: SQLitePersonalDataRepository,
|
||||
workspace_repo: SQLiteWorkspaceRepository,
|
||||
) -> None:
|
||||
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)
|
||||
)
|
||||
parent_a = await personal_data_repo.create(
|
||||
PersonalData(uuid=uuid4(), workspace_id=ws.id, name="A", created_at=now)
|
||||
)
|
||||
parent_b = await personal_data_repo.create(
|
||||
PersonalData(uuid=uuid4(), workspace_id=ws.id, name="B", created_at=now)
|
||||
)
|
||||
|
||||
birth = date.today() + relativedelta(years=2)
|
||||
child = _make_child(ws.id, birth)
|
||||
await child_repo.create(child)
|
||||
|
||||
# Both parents sit on a huge untouched pool -> both should be
|
||||
# clamped to the per-parent cap, never the per-child one.
|
||||
fetch_a = _make_fetch(parent_a.uuid, ws.id, child.id, total=240, used=0)
|
||||
fetch_b = _make_fetch(parent_b.uuid, ws.id, child.id, total=240, used=0)
|
||||
await parental_leave_repo.create(fetch_a)
|
||||
await parental_leave_repo.create(fetch_b)
|
||||
|
||||
service = await _make_service(
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo, workspace_repo
|
||||
)
|
||||
|
||||
proj_a = await service.get_days_left_at_age_4(parent_a.uuid, child.id)
|
||||
proj_b = await service.get_days_left_at_age_4(parent_b.uuid, child.id)
|
||||
assert proj_a.savable_cap_at_b4 == 48
|
||||
assert proj_b.savable_cap_at_b4 == 48
|
||||
assert proj_a.savable_after_b4 == 48
|
||||
assert proj_b.savable_after_b4 == 48
|
||||
assert proj_a.expiring_at_b4 == 192 # 240 - 48
|
||||
assert proj_b.expiring_at_b4 == 192
|
||||
|
||||
overview = await service.get_overview(ws.id, child.id)
|
||||
combined_savable = sum(p.savable_after_b4 for p in overview.parent_projections)
|
||||
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(
|
||||
self,
|
||||
child_repo: SQLiteChildRepository,
|
||||
parental_leave_repo: SQLiteParentalLeaveRepository,
|
||||
planned_leave_repo: SQLitePlannedLeaveRepository,
|
||||
personal_data_repo: SQLitePersonalDataRepository,
|
||||
workspace_repo: SQLiteWorkspaceRepository,
|
||||
) -> None:
|
||||
"""Safety belt: `planning_mode == 'couple'` with only one member so
|
||||
|
||||
far (partner not 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)
|
||||
)
|
||||
parent = await personal_data_repo.create(
|
||||
PersonalData(uuid=uuid4(), workspace_id=ws.id, name="A", created_at=now)
|
||||
)
|
||||
birth = date.today() + relativedelta(years=2)
|
||||
child = _make_child(ws.id, birth)
|
||||
await child_repo.create(child)
|
||||
fetch = _make_fetch(parent.uuid, ws.id, 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(parent.uuid, child.id)
|
||||
assert proj.savable_cap_at_b4 == 96
|
||||
assert proj.savable_after_b4 == 96
|
||||
assert proj.expiring_at_b4 == 144 # 240 - 96
|
||||
|
||||
|
||||
class TestAgreesWithPlanProposalForTheSameCommittedState:
|
||||
"""Bug A regression: the Dashboard (`AnalyticsService`) and the Plan tab's
|
||||
propose-time projection (`allocation_service._Ledger.build_age4_projections`,
|
||||
@@ -374,7 +484,7 @@ class TestAgreesWithPlanProposalForTheSameCommittedState:
|
||||
|
||||
# -- Dashboard side --
|
||||
analytics = await _make_service(
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
||||
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo, workspace_repo
|
||||
)
|
||||
dashboard_proj = await analytics.get_days_left_at_age_4(parent.uuid, child.id)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user