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
+29 -54
View File
@@ -1,7 +1,9 @@
"""Tests for the Workspace service.
Covers workspace creation/update, with a focus on the `planning_mode` field
(explicit solo/couple planning intent — not inferred from member count), plus
Covers workspace creation/update (name only — whether a family plans solo or
together is derived from member count, not a stored field; see
`test_allocation_service.py`/`test_allocation_service_branches.py` for the
allocator-side "couple wish-assignment gates on parent count" coverage), plus
renaming/removing a placeholder partner (`rename_member` / `remove_member`).
"""
@@ -49,64 +51,23 @@ async def _make_inviter(user_repo: SQLiteUserRepository, personal_data_uuid: UUI
return user.id
class TestWorkspacePlanningMode:
"""Test planning_mode defaults and validation."""
class TestWorkspaceUpdate:
"""Workspace name updates (planning mode is no longer a stored field)."""
async def test_create_workspace_defaults_to_solo(
async def test_update_name(
self,
workspace_service: WorkspaceService,
) -> None:
"""A newly created workspace defaults to solo planning mode."""
ws = await workspace_service.create_workspace("Test WS")
assert ws.planning_mode == "solo"
async def test_update_planning_mode_to_couple(
self,
workspace_service: WorkspaceService,
) -> None:
"""Updating planning_mode to a valid value ('couple') succeeds."""
ws = await workspace_service.create_workspace("Test WS")
updated = await workspace_service.update_workspace(ws.id, planning_mode="couple")
assert updated.planning_mode == "couple"
async def test_update_planning_mode_back_to_solo(
self,
workspace_service: WorkspaceService,
) -> None:
"""Updating planning_mode back to 'solo' succeeds."""
ws = await workspace_service.create_workspace("Test WS")
await workspace_service.update_workspace(ws.id, planning_mode="couple")
updated = await workspace_service.update_workspace(ws.id, planning_mode="solo")
assert updated.planning_mode == "solo"
async def test_update_planning_mode_invalid_raises(
self,
workspace_service: WorkspaceService,
) -> None:
"""An invalid planning_mode value raises ValidationError."""
ws = await workspace_service.create_workspace("Test WS")
with pytest.raises(ValidationError, match="planning_mode must be one of"):
await workspace_service.update_workspace(ws.id, planning_mode="throuple")
async def test_update_planning_mode_missing_workspace_raises(
self,
workspace_service: WorkspaceService,
) -> None:
"""Updating planning_mode on a missing workspace raises NotFoundError."""
with pytest.raises(NotFoundError):
await workspace_service.update_workspace(uuid4(), planning_mode="couple")
async def test_update_name_and_planning_mode_together(
self,
workspace_service: WorkspaceService,
) -> None:
"""Multiple fields can be updated in the same call."""
ws = await workspace_service.create_workspace("Old Name")
updated = await workspace_service.update_workspace(
ws.id, name="New Name", planning_mode="couple"
)
updated = await workspace_service.update_workspace(ws.id, name="New Name")
assert updated.name == "New Name"
assert updated.planning_mode == "couple"
async def test_update_missing_workspace_raises_not_found(
self,
workspace_service: WorkspaceService,
) -> None:
with pytest.raises(NotFoundError):
await workspace_service.update_workspace(uuid4(), name="Whatever")
class TestRenameMember:
@@ -182,6 +143,20 @@ class TestRenameMember:
with pytest.raises(ValidationError, match="empty or whitespace-only"):
await workspace_service.add_placeholder_parent(seeded_ids.workspace_id, " ")
async def test_add_placeholder_beyond_two_parents_raises_validation_error(
self,
workspace_service: WorkspaceService,
seeded_ids: SeededIds,
) -> None:
"""A workspace is capped at 2 parents — `seeded_ids.personal_data_uuid`
(1) plus one placeholder (2) already fill it; a third is refused."""
await workspace_service.add_placeholder_parent(seeded_ids.workspace_id, "Sam")
with pytest.raises(ValidationError, match="maximum number of parents"):
await workspace_service.add_placeholder_parent(seeded_ids.workspace_id, "Kim")
members = await workspace_service.list_members(seeded_ids.workspace_id)
assert {m.name for m in members} == {"Test Parent", "Sam"}
class TestRemoveMember:
"""Remove (soft-delete) a placeholder partner, guarding against orphaned data."""