Complete NextSteps roadmap: planning engine UI, calendar cluster, flexible wishes

Works through every numbered item in NextSteps260913.md section 3, plus the
three known verification issues in section 4.1.

Surface what the backend already computed:
- 3.1 render surplus_advice in PlanResults, mirroring the shortfall remedies
- 3.2 surface VacationBalance.is_assumed so assumed statutory years are visible
- 3.3 age-4 expiry given the proposed plan, via a new Age4Projection reusing
  the existing _age4_projections arithmetic (Dashboard stat verified, not rebuilt)

Calendar cluster (one pass over the holiday data):
- new GET /api/calendar/days exposing roda dagar, klamdagar and day overrides,
  paginated four years at a time
- 3.4 shade roda dagar/klamdagar, with R/K badges so colour is not the only signal
- 3.6 click-to-override behind an explicit mode toggle, leaving drag multi-select intact
- 3.7 click month/year to advance
- 3.8 per-year red-days list in Settings, statutory days plus overrides

i18n:
- 3.5 five backend-generated Swedish surfaces converted to message_key + data,
  following the tips_service pattern; old string fields replaced, not kept

Flexible wishes (3.9):
- PLANNING_ENGINE_SPEC section 10: window_mode, candidate search, cost function,
  deterministic tie-break, two-pass ordering, test matrix rows 15-24
- backend: entity, migration 006, _FULL_SCHEMA sync, repository, allocator
- frontend: season-preset toggle in WishEditor, unplaced outcome rendering

Fixes (4.1):
- pin typecheck to the local tsc rather than npx resolving a newer TypeScript
- ignore generated coverage/ and dist/ in the ESLint flat config
- fix the flaky usePlanApply test properly, with waitFor rather than a retry

Tests: backend 361 -> 404, frontend 540 -> 704. Coverage 89.47% -> 96.52% lines.

Not yet verified in a browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
This commit is contained in:
2026-09-14 02:33:03 +02:00
parent 3ca1418d94
commit 76b1e712b9
218 changed files with 29867 additions and 6085 deletions
+151 -3
View File
@@ -4,11 +4,15 @@ Demonstrates service-layer testing with external service dependencies.
Aim for >=90% coverage on new code (ARCHITECTURE.md §10).
"""
from datetime import date
from datetime import date, datetime
from uuid import uuid4
import pytest
from app.entities.child import Child
from app.entities.personal_data import PersonalData
from app.repositories_impl.sqlite.child_repository import SQLiteChildRepository
from app.repositories_impl.sqlite.personal_data_repository import SQLitePersonalDataRepository
from app.services.planning_service import PlanningService
from app.utils.errors import NotFoundError, ValidationError
from tests.conftest import SeededIds
@@ -40,7 +44,13 @@ class TestPlanningService:
planning_service: PlanningService,
seeded_ids: SeededIds,
) -> None:
"""Adding a second planned day on same date raises ValidationError."""
"""Adding a second planned day on same date raises ValidationError.
Note: This test verifies that the same parent cannot plan the same date twice.
The error message was updated to be more accurate: "Parent already has a planned
day" instead of the previous generic "A planned day already exists" (which
incorrectly rejected different parents planning the same date as a dubbeldag).
"""
child_id = seeded_ids.child_id
await planning_service.add_planned_day(
@@ -53,7 +63,7 @@ class TestPlanningService:
)
# Try to add another on same date
with pytest.raises(ValidationError, match="already exists"):
with pytest.raises(ValidationError, match="Parent already has a planned day"):
await planning_service.add_planned_day(
workspace_id=seeded_ids.workspace_id,
child_id=child_id,
@@ -339,3 +349,141 @@ class TestPlanningService:
uuid4(),
day_type="lagsta",
)
async def test_add_planned_day_different_parents_same_date_allowed(
self,
planning_service: PlanningService,
personal_data_repo: SQLitePersonalDataRepository,
seeded_ids: SeededIds,
) -> None:
"""Two different parents can plan the same date for the same child (dubbeldagar).
This is the legitimate dubbeldagar case: parent A has the date planned,
parent B can also plan the same date (subject to dubbeldagar validation).
Note: The test uses a non-dubbeldag date for parent A and a dubbeldag date
for parent B within the 15-month window.
"""
child_id = seeded_ids.child_id
ws = seeded_ids.workspace_id
parent_a_uuid = seeded_ids.personal_data_uuid
# Create a second parent
parent_b = await personal_data_repo.create(
PersonalData(
uuid=uuid4(),
workspace_id=ws,
name="Test Parent B",
created_at=datetime.now(),
)
)
parent_b_uuid = parent_b.uuid
# Parent A plans 2024-10-01 (within dubbeldagar window, but not as a dubbeldag)
await planning_service.add_planned_day(
workspace_id=ws,
child_id=child_id,
parent_personal_data_uuid=parent_a_uuid,
date=date(2024, 10, 1),
day_type="sjukpenning",
is_dubbeldag=False,
)
# Parent B can also plan the same date (this should succeed)
# Parent B marks this as a dubbeldag
result = await planning_service.add_planned_day(
workspace_id=ws,
child_id=child_id,
parent_personal_data_uuid=parent_b_uuid,
date=date(2024, 10, 1),
day_type="lagsta",
is_dubbeldag=True,
)
assert result.parent_personal_data_uuid == parent_b_uuid
assert result.date == date(2024, 10, 1)
assert result.is_dubbeldag is True
async def test_add_planned_day_same_parent_same_date_rejected(
self,
planning_service: PlanningService,
seeded_ids: SeededIds,
) -> None:
"""Same parent cannot plan the same date twice (genuine double-booking).
This verifies that the fix for parent-aware checking still rejects
the genuine double-booking case where the same parent tries to plan
the same date twice.
"""
child_id = seeded_ids.child_id
ws = seeded_ids.workspace_id
parent_uuid = seeded_ids.personal_data_uuid
# Parent plans 2024-09-01
await planning_service.add_planned_day(
workspace_id=ws,
child_id=child_id,
parent_personal_data_uuid=parent_uuid,
date=date(2024, 9, 1),
day_type="sjukpenning",
is_dubbeldag=False,
)
# Same parent tries to plan the same date again - should be rejected
with pytest.raises(ValidationError, match="already has a planned day"):
await planning_service.add_planned_day(
workspace_id=ws,
child_id=child_id,
parent_personal_data_uuid=parent_uuid,
date=date(2024, 9, 1),
day_type="lagsta",
is_dubbeldag=False,
)
async def test_add_planned_day_same_date_different_child_allowed(
self,
planning_service: PlanningService,
child_repo: SQLiteChildRepository,
seeded_ids: SeededIds,
) -> None:
"""Same parent can plan the same date for different children.
The double-booking check is per (parent, child), so the same parent
planning the same date for different children is always allowed.
"""
ws = seeded_ids.workspace_id
parent_uuid = seeded_ids.personal_data_uuid
child_1 = seeded_ids.child_id
# Create a second child
child_2 = await child_repo.create(
Child(
id=uuid4(),
workspace_id=ws,
name="Test Child 2",
birth_date=date(2024, 6, 1),
created_at=datetime.now(),
)
)
child_2_id = child_2.id
# Parent plans 2024-09-01 for child 1
await planning_service.add_planned_day(
workspace_id=ws,
child_id=child_1,
parent_personal_data_uuid=parent_uuid,
date=date(2024, 9, 1),
day_type="sjukpenning",
is_dubbeldag=False,
)
# Same parent can plan the same date for child 2
result = await planning_service.add_planned_day(
workspace_id=ws,
child_id=child_2_id,
parent_personal_data_uuid=parent_uuid,
date=date(2024, 9, 1),
day_type="lagsta",
is_dubbeldag=False,
)
assert result.child_id == child_2_id
assert result.parent_personal_data_uuid == parent_uuid
assert result.date == date(2024, 9, 1)