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:
2026-09-19 12:27:29 +02:00
parent 375bc64f2f
commit e4719cb9ae
71 changed files with 5705 additions and 1859 deletions
@@ -275,6 +275,42 @@ class TestSQLiteLeaveWishRepository:
assert fetched is not None
assert fetched.target_days is None
async def test_create_defaults_wants_dubbeldag_to_false(
self,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(workspace_id=seeded_ids.workspace_id)
created = await leave_wish_repo.create(entity)
assert created.wants_dubbeldag is False
fetched = await leave_wish_repo.get_by_id(created.id)
assert fetched is not None
assert fetched.wants_dubbeldag is False
async def test_create_and_update_round_trip_wants_dubbeldag(
self,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(
workspace_id=seeded_ids.workspace_id, parent_mode="both", wants_dubbeldag=True
)
created = await leave_wish_repo.create(entity)
assert created.wants_dubbeldag is True
fetched = await leave_wish_repo.get_by_id(created.id)
assert fetched is not None
assert fetched.wants_dubbeldag is True
fetched.wants_dubbeldag = False
updated = await leave_wish_repo.update(fetched.id, fetched)
assert updated.wants_dubbeldag is False
refetched = await leave_wish_repo.get_by_id(created.id)
assert refetched is not None
assert refetched.wants_dubbeldag is False
async def test_get_by_workspace(
self,
leave_wish_repo: SQLiteLeaveWishRepository,
@@ -446,6 +482,7 @@ class TestLeaveWishService:
assert result.target_weeks == 6
assert result.recurrence == "once" # default
assert result.parent_mode == "either" # default
assert result.wants_dubbeldag is False # default
@pytest.mark.parametrize("recurrence", ["once", "yearly"])
async def test_create_accepts_valid_recurrence(
@@ -522,6 +559,11 @@ class TestLeaveWishService:
"last_year",
id="last_year_before_first_year",
),
pytest.param(
{"parent_mode": "either", "wants_dubbeldag": True},
"wants_dubbeldag",
id="wants_dubbeldag_forbidden_unless_both",
),
]
@pytest.mark.parametrize("overrides,match", _REJECTION_CASES)
@@ -578,6 +620,49 @@ class TestLeaveWishService:
assert result.parent_mode == "specific"
assert result.parent_personal_data_uuid == seeded_ids.personal_data_uuid
# -- wants_dubbeldag (§Decision A, opt-in co-leave) ---------------------
async def test_create_both_parent_mode_can_opt_into_dubbeldagar(
self,
leave_wish_service: LeaveWishService,
seeded_ids: SeededIds,
) -> None:
result = await leave_wish_service.create(
workspace_id=seeded_ids.workspace_id,
name="Wish",
window_start_month=1,
window_start_day=1,
window_end_month=1,
window_end_day=10,
target_weeks=1,
first_year=2027,
parent_mode="both",
wants_dubbeldag=True,
)
assert result.parent_mode == "both"
assert result.wants_dubbeldag is True
async def test_update_can_toggle_wants_dubbeldag(
self,
leave_wish_service: LeaveWishService,
seeded_ids: SeededIds,
) -> None:
created = await leave_wish_service.create(
workspace_id=seeded_ids.workspace_id,
name="Wish",
window_start_month=1,
window_start_day=1,
window_end_month=1,
window_end_day=10,
target_weeks=1,
first_year=2027,
parent_mode="both",
)
assert created.wants_dubbeldag is False
updated = await leave_wish_service.update(leave_wish_id=created.id, wants_dubbeldag=True)
assert updated.wants_dubbeldag is True
# -- window_mode (§10.1) ------------------------------------------------
async def test_create_defaults_to_fixed_window_mode(