320 lines
12 KiB
Python
320 lines
12 KiB
Python
"""Tests for the core calculation engine (days-left-at-age-4).
|
|
|
|
These tests cover the headline feature and must cover edge cases:
|
|
leap years, unborn child (due date), no FK data yet, full planned calendar.
|
|
"""
|
|
|
|
from datetime import date, datetime
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from app.entities.child import Child
|
|
from app.entities.parental_leave import ParentalLeaveFetch, TierData
|
|
from app.repositories_impl.sqlite.child_repository import SQLiteChildRepository
|
|
from app.repositories_impl.sqlite.parental_leave_repository import SQLiteParentalLeaveRepository
|
|
from app.repositories_impl.sqlite.personal_data_repository import SQLitePersonalDataRepository
|
|
from app.repositories_impl.sqlite.planned_leave_repository import SQLitePlannedLeaveRepository
|
|
from app.services.analytics_service import AnalyticsService
|
|
from tests.conftest import SeededIds
|
|
|
|
|
|
def _make_child(workspace_id, birth_date: date) -> Child:
|
|
return Child(
|
|
id=uuid4(),
|
|
workspace_id=workspace_id,
|
|
name="TestChild",
|
|
birth_date=birth_date,
|
|
created_at=datetime.now(),
|
|
)
|
|
|
|
|
|
def _make_fetch(
|
|
personal_data_uuid, workspace_id, child_id, total=240, used=100
|
|
) -> ParentalLeaveFetch:
|
|
now = datetime.now()
|
|
tier = TierData(
|
|
sjukpenning_total=390,
|
|
sjukpenning_used=used,
|
|
sjukpenning_left=390 - used,
|
|
lagsta_niva_total=90,
|
|
lagsta_niva_used=0,
|
|
lagsta_niva_left=90,
|
|
)
|
|
return ParentalLeaveFetch(
|
|
id=uuid4(),
|
|
personal_data_uuid=personal_data_uuid,
|
|
workspace_id=workspace_id,
|
|
child_id=child_id,
|
|
source="manual",
|
|
fetched_at=now,
|
|
total_days=total,
|
|
days_used=used,
|
|
days_left=total - used,
|
|
reserved_days_total=90,
|
|
reserved_days_left=90,
|
|
transferable_days_left=50,
|
|
dubbeldagar_left=60,
|
|
tier_data=tier,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
|
|
|
|
async def _make_service(
|
|
child_repo: SQLiteChildRepository,
|
|
leave_repo: SQLiteParentalLeaveRepository,
|
|
planned_repo: SQLitePlannedLeaveRepository,
|
|
pd_repo: SQLitePersonalDataRepository,
|
|
) -> AnalyticsService:
|
|
return AnalyticsService(leave_repo, planned_repo, child_repo, pd_repo)
|
|
|
|
|
|
class TestAge4Projection:
|
|
async def test_no_planned_days_projection(
|
|
self,
|
|
child_repo: SQLiteChildRepository,
|
|
parental_leave_repo: SQLiteParentalLeaveRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
personal_data_repo: SQLitePersonalDataRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
ws = seeded_ids.workspace_id
|
|
pd_uuid = seeded_ids.personal_data_uuid
|
|
|
|
# Use a birth date far in the future so today < B4
|
|
birth = date.today() + relativedelta(years=2)
|
|
child = _make_child(ws, birth)
|
|
await child_repo.create(child)
|
|
|
|
fetch = _make_fetch(pd_uuid, ws, child.id, total=240, used=100)
|
|
await parental_leave_repo.create(fetch)
|
|
|
|
service = await _make_service(
|
|
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
|
)
|
|
proj = await service.get_days_left_at_age_4(pd_uuid, child.id)
|
|
|
|
assert proj.days_used == 100
|
|
assert proj.planned_days_before_b4 == 0
|
|
assert proj.projected_left_at_b4 == 140 # 240 - 100
|
|
assert proj.savable_after_b4 == 96
|
|
assert proj.expiring_at_b4 == 44 # 140 - 96
|
|
|
|
async def test_no_fetch_data_assumes_full_entitlement(
|
|
self,
|
|
child_repo: SQLiteChildRepository,
|
|
parental_leave_repo: SQLiteParentalLeaveRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
personal_data_repo: SQLitePersonalDataRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
ws = seeded_ids.workspace_id
|
|
pd_uuid = seeded_ids.personal_data_uuid
|
|
birth = date.today() + relativedelta(years=2)
|
|
child = _make_child(ws, birth)
|
|
await child_repo.create(child)
|
|
|
|
# No FK data entered
|
|
service = await _make_service(
|
|
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
|
)
|
|
proj = await service.get_days_left_at_age_4(pd_uuid, child.id)
|
|
|
|
assert proj.total_days == 240 # default entitlement
|
|
assert proj.days_used == 0
|
|
assert proj.projected_left_at_b4 == 240
|
|
assert proj.savable_after_b4 == 96
|
|
assert proj.expiring_at_b4 == 144 # 240 - 96
|
|
|
|
async def test_leap_year_birthday(
|
|
self,
|
|
child_repo: SQLiteChildRepository,
|
|
parental_leave_repo: SQLiteParentalLeaveRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
personal_data_repo: SQLitePersonalDataRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
ws = seeded_ids.workspace_id
|
|
pd_uuid = seeded_ids.personal_data_uuid
|
|
# Child born 2024-02-29 (leap day); 4th birthday must be 2028-02-29
|
|
birth = date(2024, 2, 29)
|
|
child = _make_child(ws, birth)
|
|
await child_repo.create(child)
|
|
|
|
service = await _make_service(
|
|
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
|
)
|
|
proj = await service.get_days_left_at_age_4(pd_uuid, child.id)
|
|
|
|
# dateutil.relativedelta gives 2028-02-29 (2028 is a leap year)
|
|
assert proj.fourth_birthday == date(2028, 2, 29)
|
|
|
|
async def test_all_days_used_nothing_left(
|
|
self,
|
|
child_repo: SQLiteChildRepository,
|
|
parental_leave_repo: SQLiteParentalLeaveRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
personal_data_repo: SQLitePersonalDataRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
ws = seeded_ids.workspace_id
|
|
pd_uuid = seeded_ids.personal_data_uuid
|
|
birth = date.today() + relativedelta(years=2)
|
|
child = _make_child(ws, birth)
|
|
await child_repo.create(child)
|
|
|
|
fetch = _make_fetch(pd_uuid, ws, child.id, total=240, used=240)
|
|
await parental_leave_repo.create(fetch)
|
|
|
|
service = await _make_service(
|
|
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
|
)
|
|
proj = await service.get_days_left_at_age_4(pd_uuid, child.id)
|
|
|
|
assert proj.projected_left_at_b4 == 0
|
|
assert proj.savable_after_b4 == 0
|
|
assert proj.expiring_at_b4 == 0
|
|
|
|
async def test_child_not_found_raises(
|
|
self,
|
|
child_repo: SQLiteChildRepository,
|
|
parental_leave_repo: SQLiteParentalLeaveRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
personal_data_repo: SQLitePersonalDataRepository,
|
|
) -> None:
|
|
from app.utils.errors import NotFoundError
|
|
|
|
service = await _make_service(
|
|
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
|
)
|
|
with pytest.raises(NotFoundError):
|
|
await service.get_days_left_at_age_4(uuid4(), uuid4())
|
|
|
|
|
|
class TestValidateDubbeldagar:
|
|
async def test_child_not_found_raises(
|
|
self,
|
|
child_repo: SQLiteChildRepository,
|
|
parental_leave_repo: SQLiteParentalLeaveRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
personal_data_repo: SQLitePersonalDataRepository,
|
|
) -> None:
|
|
from app.utils.errors import NotFoundError
|
|
|
|
service = await _make_service(
|
|
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
|
)
|
|
with pytest.raises(NotFoundError):
|
|
await service.validate_dubbeldagar(uuid4(), date.today())
|
|
|
|
async def test_outside_window_raises(
|
|
self,
|
|
child_repo: SQLiteChildRepository,
|
|
parental_leave_repo: SQLiteParentalLeaveRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
personal_data_repo: SQLitePersonalDataRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
from app.utils.errors import DubbeldagError
|
|
|
|
ws = seeded_ids.workspace_id
|
|
birth = date(2024, 1, 1)
|
|
child = _make_child(ws, birth)
|
|
await child_repo.create(child)
|
|
|
|
service = await _make_service(
|
|
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
|
)
|
|
# Date beyond the 15-month window
|
|
beyond_window = birth + relativedelta(months=16)
|
|
with pytest.raises(DubbeldagError):
|
|
await service.validate_dubbeldagar(child.id, beyond_window)
|
|
|
|
async def test_quota_exceeded_raises(
|
|
self,
|
|
child_repo: SQLiteChildRepository,
|
|
parental_leave_repo: SQLiteParentalLeaveRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
personal_data_repo: SQLitePersonalDataRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
from app.config import settings
|
|
from app.entities.planned_leave import PlannedLeaveDay
|
|
from app.utils.errors import DubbeldagError
|
|
|
|
ws = seeded_ids.workspace_id
|
|
pd_uuid = seeded_ids.personal_data_uuid
|
|
birth = date(2024, 1, 1)
|
|
child = _make_child(ws, birth)
|
|
await child_repo.create(child)
|
|
|
|
# Fill up the dubbeldagar quota
|
|
for i in range(settings.rules.max_dubbeldagar):
|
|
day = birth + relativedelta(days=i)
|
|
await planned_leave_repo.create(
|
|
PlannedLeaveDay(
|
|
id=uuid4(),
|
|
workspace_id=ws,
|
|
child_id=child.id,
|
|
parent_personal_data_uuid=pd_uuid,
|
|
date=day,
|
|
day_type="sjukpenning",
|
|
is_dubbeldag=True,
|
|
)
|
|
)
|
|
|
|
service = await _make_service(
|
|
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
|
)
|
|
within_window = birth + relativedelta(months=1)
|
|
with pytest.raises(DubbeldagError):
|
|
await service.validate_dubbeldagar(child.id, within_window)
|
|
|
|
|
|
class TestGetOverview:
|
|
async def test_overview_single_parent(
|
|
self,
|
|
child_repo: SQLiteChildRepository,
|
|
parental_leave_repo: SQLiteParentalLeaveRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
personal_data_repo: SQLitePersonalDataRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
ws = seeded_ids.workspace_id
|
|
pd_uuid = seeded_ids.personal_data_uuid
|
|
|
|
birth = date.today() + relativedelta(years=2)
|
|
child = _make_child(ws, birth)
|
|
await child_repo.create(child)
|
|
|
|
fetch = _make_fetch(pd_uuid, ws, child.id, total=240, used=80)
|
|
await parental_leave_repo.create(fetch)
|
|
|
|
service = await _make_service(
|
|
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
|
)
|
|
overview = await service.get_overview(ws, child.id)
|
|
|
|
assert overview.workspace_id == ws
|
|
assert len(overview.parent_projections) == 1
|
|
assert overview.combined_days_used == 80
|
|
assert overview.combined_days_left == 160 # 240 - 80
|
|
assert overview.combined_expiring_at_b4 == 64 # 160 - 96
|
|
|
|
async def test_overview_no_parents_raises(
|
|
self,
|
|
child_repo: SQLiteChildRepository,
|
|
parental_leave_repo: SQLiteParentalLeaveRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
personal_data_repo: SQLitePersonalDataRepository,
|
|
) -> None:
|
|
from app.utils.errors import NotFoundError
|
|
|
|
service = await _make_service(
|
|
child_repo, parental_leave_repo, planned_leave_repo, personal_data_repo
|
|
)
|
|
with pytest.raises(NotFoundError):
|
|
await service.get_overview(uuid4(), uuid4())
|