Files
F-r-ldraledigplannerare/backend/tests/unit/test_preschool_planning_days_repository.py
T
jacobh2 0e9b20ec37 Track preschool planning days, partner invites and real vacation forfeiture
Three strands of work that grew together across the same settings screen and
API client, so they land as one change.

Per-year preschool planning days ("studiedagar"): new table, repository
(SQLite + Postgres), service, router and migration 007, plus the hooks and
Settings UI to plan and record them per child and year. Allocation now
reserves the recorded figure for a year where one exists and only falls back
to the child-level estimate otherwise, marking that fallback as assumed.

Partner invitations: workspaces issue and list invitations, registration
accepts an invite token to join the inviter's workspace instead of creating a
personal one, and a /join/:token page plus the settings invite panel drive it.

Vacation forfeiture: tips and the dashboard now distinguish days a parent
genuinely loses from days that merely age, so the warning stops firing on
balances that were never at risk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 11:22:07 +02:00

240 lines
9.3 KiB
Python

"""Tests for the PreschoolPlanningDays repository + service.
Follows the pattern in `test_vacation_days_repository.py`: arrange an
entity, exercise the repo/service, assert on the result.
"""
from datetime import date, datetime
from uuid import uuid4
import pytest
from app.entities.child import Child
from app.entities.preschool_planning_days import PreschoolPlanningDays
from app.repositories_impl.sqlite.child_repository import SQLiteChildRepository
from app.repositories_impl.sqlite.preschool_planning_days_repository import (
SQLitePreschoolPlanningDaysRepository,
)
from app.services.preschool_planning_days_service import PreschoolPlanningDaysService
from app.utils.errors import NotFoundError, ValidationError
from tests.conftest import SeededIds
def _make(child_id=None, **overrides) -> PreschoolPlanningDays:
base = dict(
id=uuid4(),
child_id=child_id or uuid4(),
year=2026,
planned_days=6,
taken_days=None,
created_at=datetime.now(),
updated_at=datetime.now(),
)
base.update(overrides)
return PreschoolPlanningDays(**base)
class TestSQLitePreschoolPlanningDaysRepository:
async def test_create_and_get_by_id(
self,
preschool_planning_days_repo: SQLitePreschoolPlanningDaysRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(child_id=seeded_ids.child_id)
await preschool_planning_days_repo.create(entity)
fetched = await preschool_planning_days_repo.get_by_id(entity.id)
assert fetched is not None
assert fetched.id == entity.id
assert fetched.planned_days == 6
assert fetched.taken_days is None
async def test_get_by_child_year(
self,
preschool_planning_days_repo: SQLitePreschoolPlanningDaysRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(child_id=seeded_ids.child_id)
await preschool_planning_days_repo.create(entity)
found = await preschool_planning_days_repo.get_by_child_year(entity.child_id, 2026)
assert found is not None
assert found.id == entity.id
missing = await preschool_planning_days_repo.get_by_child_year(uuid4(), 2026)
assert missing is None
async def test_update_preserves_null_taken_days_distinction(
self,
preschool_planning_days_repo: SQLitePreschoolPlanningDaysRepository,
seeded_ids: SeededIds,
) -> None:
"""Not-recorded (None) must round-trip distinctly from recorded-as-zero."""
entity = _make(child_id=seeded_ids.child_id, taken_days=None)
await preschool_planning_days_repo.create(entity)
fetched = await preschool_planning_days_repo.get_by_id(entity.id)
assert fetched is not None
assert fetched.taken_days is None
fetched.taken_days = 0
updated = await preschool_planning_days_repo.update(fetched.id, fetched)
assert updated.taken_days == 0
async def test_update_missing_raises(
self, preschool_planning_days_repo: SQLitePreschoolPlanningDaysRepository
) -> None:
with pytest.raises(NotFoundError):
await preschool_planning_days_repo.update(uuid4(), _make())
async def test_delete(
self,
preschool_planning_days_repo: SQLitePreschoolPlanningDaysRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(child_id=seeded_ids.child_id)
await preschool_planning_days_repo.create(entity)
await preschool_planning_days_repo.delete(entity.id)
assert await preschool_planning_days_repo.get_by_id(entity.id) is None
async def test_delete_missing_raises(
self, preschool_planning_days_repo: SQLitePreschoolPlanningDaysRepository
) -> None:
with pytest.raises(NotFoundError):
await preschool_planning_days_repo.delete(uuid4())
async def test_list_filters_by_child(
self,
preschool_planning_days_repo: SQLitePreschoolPlanningDaysRepository,
seeded_ids: SeededIds,
child_repo: SQLiteChildRepository,
) -> None:
other_child = await child_repo.create(
Child(
id=uuid4(),
workspace_id=seeded_ids.workspace_id,
name="Other Child",
birth_date=date(2022, 1, 1),
created_at=datetime.now(),
)
)
await preschool_planning_days_repo.create(_make(child_id=seeded_ids.child_id, year=2025))
await preschool_planning_days_repo.create(_make(child_id=seeded_ids.child_id, year=2026))
await preschool_planning_days_repo.create(_make(child_id=other_child.id))
rows = await preschool_planning_days_repo.list(child_id=seeded_ids.child_id)
assert len(rows) == 2
assert [r.year for r in rows] == [2026, 2025] # ordered year DESC
async def test_list_rejects_unsupported_filter(
self, preschool_planning_days_repo: SQLitePreschoolPlanningDaysRepository
) -> None:
with pytest.raises(ValueError, match="invalid_filter"):
await preschool_planning_days_repo.list(invalid_filter="value")
class TestPreschoolPlanningDaysService:
async def test_upsert_planned_creates_then_updates(
self,
preschool_planning_days_service: PreschoolPlanningDaysService,
seeded_ids: SeededIds,
child_repo: SQLiteChildRepository,
) -> None:
child = await child_repo.get_by_id(seeded_ids.child_id)
assert child is not None
created = await preschool_planning_days_service.upsert_planned(child, 2026, 8)
assert created.planned_days == 8
assert created.taken_days is None
updated = await preschool_planning_days_service.upsert_planned(child, 2026, 10)
assert updated.id == created.id
assert updated.planned_days == 10
async def test_upsert_planned_rejects_negative(
self,
preschool_planning_days_service: PreschoolPlanningDaysService,
child_repo: SQLiteChildRepository,
seeded_ids: SeededIds,
) -> None:
child = await child_repo.get_by_id(seeded_ids.child_id)
assert child is not None
with pytest.raises(ValidationError):
await preschool_planning_days_service.upsert_planned(child, 2026, -1)
async def test_record_taken_seeds_default_planned_days(
self,
preschool_planning_days_service: PreschoolPlanningDaysService,
child_repo: SQLiteChildRepository,
seeded_ids: SeededIds,
) -> None:
"""With no existing row, `planned_days` is seeded from the child's default."""
child = await child_repo.get_by_id(seeded_ids.child_id)
assert child is not None
assert child.preschool_planning_days_per_year == 6 # entity default
recorded = await preschool_planning_days_service.record_taken(child, 2026, 4)
assert recorded.planned_days == 6
assert recorded.taken_days == 4
async def test_record_taken_preserves_planned_days_on_existing_row(
self,
preschool_planning_days_service: PreschoolPlanningDaysService,
child_repo: SQLiteChildRepository,
seeded_ids: SeededIds,
) -> None:
child = await child_repo.get_by_id(seeded_ids.child_id)
assert child is not None
await preschool_planning_days_service.upsert_planned(child, 2026, 12)
recorded = await preschool_planning_days_service.record_taken(child, 2026, 3)
assert recorded.planned_days == 12
assert recorded.taken_days == 3
async def test_record_taken_rejects_negative(
self,
preschool_planning_days_service: PreschoolPlanningDaysService,
child_repo: SQLiteChildRepository,
seeded_ids: SeededIds,
) -> None:
child = await child_repo.get_by_id(seeded_ids.child_id)
assert child is not None
with pytest.raises(ValidationError):
await preschool_planning_days_service.record_taken(child, 2026, -1)
async def test_record_taken_zero_distinguishable_from_unrecorded(
self,
preschool_planning_days_service: PreschoolPlanningDaysService,
child_repo: SQLiteChildRepository,
seeded_ids: SeededIds,
) -> None:
child = await child_repo.get_by_id(seeded_ids.child_id)
assert child is not None
unrecorded = await preschool_planning_days_service.get_by_child_year(child.id, 2026)
assert unrecorded is None
recorded_zero = await preschool_planning_days_service.record_taken(child, 2026, 0)
assert recorded_zero.taken_days == 0
async def test_get_missing_raises(
self, preschool_planning_days_service: PreschoolPlanningDaysService
) -> None:
with pytest.raises(NotFoundError):
await preschool_planning_days_service.get(uuid4())
async def test_list_for_child(
self,
preschool_planning_days_service: PreschoolPlanningDaysService,
child_repo: SQLiteChildRepository,
seeded_ids: SeededIds,
) -> None:
child = await child_repo.get_by_id(seeded_ids.child_id)
assert child is not None
await preschool_planning_days_service.upsert_planned(child, 2025, 6)
await preschool_planning_days_service.upsert_planned(child, 2026, 6)
rows = await preschool_planning_days_service.list_for_child(child.id)
assert [r.year for r in rows] == [2026, 2025]