782415122d
The calendar mixed two kinds of truth in one table. `planned_leave_day` was simultaneously the allocator's output cache, the user's override store (`is_pinned`, plus ad-hoc rows with no `wish_id`), and the resource ledger that analytics and the age-4 maths read. The planner therefore had to ask "am I allowed to touch this row?" on every sync, and got it wrong at the edges. Every user assertion is now one `day_assertion` row — a concrete `day_type` replaces a pinned day, a row with no `wish_id` replaces an ad-hoc day, and `day_type='none'` replaces `planned_day_exclusion`. `planned_leave_day` becomes purely derived with a single writer, and `PlanningService.pinned_keys()` is the single answer to "is this day pinned", used by the allocator's loader, by `sync_plan` and by the read router alike. On top of that, every calendar band is now a real event: an ad-hoc band opens the same full editor as a wish-backed one and saving it promotes it to a `LeaveWish`, so it can be named, repeated yearly, assigned to either or both parents, and handed to the planner. "Planering" is a `moveable` toggle plus a from/to month bound plus a separate lock/unlock action on the days themselves, replacing a three-rung ladder whose "planner decides" rung set a search window equal to the drawn dates and so could never move anything. Bugs found and fixed along the way: - The plan erased its own history. Fixed windows clip to today, so an elapsed auto-placed day fell into `stale_keys` and was deleted on the next sync — and a background sync runs on every workspace load. Past rows are now frozen and counted as spent. - Migration 010's downgrade silently destroyed every pinned day and exclusion (`is_pinned` returned as DEFAULT 0, the exclusion table came back empty, exit 0, no warning). It now reconstructs both from `day_assertion`, and 009 itemises what it cannot recover before dropping the table. - Deleting a period left its assertions behind, locking those dates forever with no UI path to clear them. Wish deletion now clears them by `wish_id`. - `DayAssertionService` and `LeaveWishService` committed multi-statement writes without a transaction; a crash mid-write diverged the tables or half-deleted a series. Both now run in a unit of work. - A sibling year's shortfall was never surfaced: the toast only inspected the edited occurrence's outcome, so a fully unfunded later year rendered as an empty band with no warning. - A flexible window whose bound had entirely elapsed was dropped from the proposal with no outcome at all. It now reports as `unplaced`. - Unlocking a period deleted its days instead of returning them to the planner. - The e2e global setup posted to a route deleted in this change, which would have failed the whole suite before its first test. Gates: backend 938 tests / 91% coverage, frontend 1351 / 97.4 | 92.2 | 94.37 | 97.4. Migrations 009 and 010 audited against a hand-built realistic database: the upgrade reconciles exactly and a constraint violation fails safe, leaving the schema at 008. Not complete. Step 4's toggle, month picker and lock/unlock action are verified by browser walk and unit tests, but the partial-lock readout was not exercised. `ideas.md` §7 adds a full plan for the vacation-forfeiture defect (the planner detects expiring days and then does nothing about them) — not implemented here. Remaining smaller items are listed in the session's restructure notes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T4wjgbVL4HAC5Rdaw75VbV
512 lines
19 KiB
Python
512 lines
19 KiB
Python
"""Tests for the DayAssertion repository and service.
|
|
|
|
Restructure step 3 (intent vs. derived output — see
|
|
`~/.claude/plans/please-make-a-plan-cheerful-beacon.md.restructure`).
|
|
`TestDayAssertionServiceMirroring`/`TestDayAssertionServiceGuards` below
|
|
cover what step 3b's `DayAssertionService` newly took on from the deleted
|
|
`PlannedDayExclusionService`/`PlanningService.add_planned_day`: mirroring a
|
|
write into `planned_leave_day` and enforcing the free-day guard.
|
|
"""
|
|
|
|
from datetime import date, datetime
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from app.entities.day_assertion import DAY_TYPE_NONE, DayAssertion
|
|
from app.repositories_impl.sqlite.day_assertion_repository import SQLiteDayAssertionRepository
|
|
from app.repositories_impl.sqlite.day_override_repository import SQLiteDayOverrideRepository
|
|
from app.repositories_impl.sqlite.planned_leave_repository import SQLitePlannedLeaveRepository
|
|
from app.repositories_impl.sqlite.workspace_repository import SQLiteWorkspaceRepository
|
|
from app.services.day_assertion_service import DayAssertionService
|
|
from app.utils.errors import NotFoundError, ValidationError
|
|
from tests.conftest import SeededIds
|
|
|
|
_DAY = date(2027, 2, 3)
|
|
|
|
|
|
def _make(seeded: SeededIds, **overrides) -> DayAssertion:
|
|
base = dict(
|
|
id=uuid4(),
|
|
workspace_id=seeded.workspace_id,
|
|
child_id=seeded.child_id,
|
|
parent_personal_data_uuid=seeded.personal_data_uuid,
|
|
date=_DAY,
|
|
day_type="sjukpenning",
|
|
is_dubbeldag=False,
|
|
wish_id=None,
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now(),
|
|
)
|
|
base.update(overrides)
|
|
return DayAssertion(**base)
|
|
|
|
|
|
class TestSQLiteDayAssertionRepository:
|
|
async def test_create_and_get_by_id(
|
|
self, day_assertion_repo: SQLiteDayAssertionRepository, seeded_ids: SeededIds
|
|
) -> None:
|
|
entity = _make(seeded_ids)
|
|
await day_assertion_repo.create(entity)
|
|
|
|
fetched = await day_assertion_repo.get_by_id(entity.id)
|
|
assert fetched is not None
|
|
assert fetched.day_type == "sjukpenning"
|
|
assert fetched.wish_id is None
|
|
|
|
async def test_get_by_natural_key(
|
|
self, day_assertion_repo: SQLiteDayAssertionRepository, seeded_ids: SeededIds
|
|
) -> None:
|
|
entity = _make(seeded_ids)
|
|
await day_assertion_repo.create(entity)
|
|
|
|
found = await day_assertion_repo.get_by_natural_key(
|
|
seeded_ids.workspace_id, seeded_ids.child_id, seeded_ids.personal_data_uuid, _DAY
|
|
)
|
|
assert found is not None
|
|
assert found.id == entity.id
|
|
|
|
assert (
|
|
await day_assertion_repo.get_by_natural_key(
|
|
seeded_ids.workspace_id, seeded_ids.child_id, uuid4(), _DAY
|
|
)
|
|
is None
|
|
)
|
|
|
|
async def test_update_and_delete(
|
|
self, day_assertion_repo: SQLiteDayAssertionRepository, seeded_ids: SeededIds
|
|
) -> None:
|
|
entity = _make(seeded_ids)
|
|
await day_assertion_repo.create(entity)
|
|
|
|
entity.day_type = "vacation"
|
|
entity.is_dubbeldag = True
|
|
updated = await day_assertion_repo.update(entity.id, entity)
|
|
assert updated.day_type == "vacation"
|
|
assert updated.is_dubbeldag is True
|
|
|
|
await day_assertion_repo.delete(entity.id)
|
|
assert await day_assertion_repo.get_by_id(entity.id) is None
|
|
|
|
with pytest.raises(NotFoundError):
|
|
await day_assertion_repo.delete(entity.id)
|
|
with pytest.raises(NotFoundError):
|
|
await day_assertion_repo.update(entity.id, entity)
|
|
|
|
async def test_delete_by_natural_key_is_idempotent(
|
|
self, day_assertion_repo: SQLiteDayAssertionRepository, seeded_ids: SeededIds
|
|
) -> None:
|
|
entity = _make(seeded_ids)
|
|
await day_assertion_repo.create(entity)
|
|
|
|
await day_assertion_repo.delete_by_natural_key(
|
|
seeded_ids.workspace_id, seeded_ids.child_id, seeded_ids.personal_data_uuid, _DAY
|
|
)
|
|
assert await day_assertion_repo.get_by_id(entity.id) is None
|
|
|
|
# A no-op the second time — never raises.
|
|
await day_assertion_repo.delete_by_natural_key(
|
|
seeded_ids.workspace_id, seeded_ids.child_id, seeded_ids.personal_data_uuid, _DAY
|
|
)
|
|
|
|
async def test_upsert_creates_then_overwrites(
|
|
self, day_assertion_repo: SQLiteDayAssertionRepository, seeded_ids: SeededIds
|
|
) -> None:
|
|
first = await day_assertion_repo.upsert(_make(seeded_ids, day_type="sjukpenning"))
|
|
second = await day_assertion_repo.upsert(
|
|
_make(seeded_ids, id=uuid4(), day_type="vacation", is_dubbeldag=True)
|
|
)
|
|
|
|
# Same natural key -> same underlying row, not a duplicate.
|
|
assert second.id == first.id
|
|
rows = await day_assertion_repo.list_by_workspace(seeded_ids.workspace_id)
|
|
assert len(rows) == 1
|
|
assert rows[0].day_type == "vacation"
|
|
assert rows[0].is_dubbeldag is True
|
|
|
|
async def test_list_by_workspace_is_date_ordered(
|
|
self, day_assertion_repo: SQLiteDayAssertionRepository, seeded_ids: SeededIds
|
|
) -> None:
|
|
await day_assertion_repo.create(_make(seeded_ids, date=date(2027, 2, 5)))
|
|
await day_assertion_repo.create(_make(seeded_ids, date=date(2027, 2, 1)))
|
|
|
|
rows = await day_assertion_repo.list_by_workspace(seeded_ids.workspace_id)
|
|
assert [r.date for r in rows] == [date(2027, 2, 1), date(2027, 2, 5)]
|
|
assert await day_assertion_repo.list_by_workspace(uuid4()) == []
|
|
|
|
async def test_unsupported_filter_is_rejected(
|
|
self, day_assertion_repo: SQLiteDayAssertionRepository
|
|
) -> None:
|
|
with pytest.raises(ValueError):
|
|
await day_assertion_repo.list(nonsense=1)
|
|
|
|
|
|
class TestDayAssertionService:
|
|
async def test_assert_day_is_idempotent_by_natural_key(
|
|
self, day_assertion_service: DayAssertionService, seeded_ids: SeededIds
|
|
) -> None:
|
|
first = await day_assertion_service.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
_DAY,
|
|
day_type="lagsta",
|
|
)
|
|
second = await day_assertion_service.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
_DAY,
|
|
day_type="vacation",
|
|
)
|
|
assert second.id == first.id
|
|
rows = await day_assertion_service.list_for_workspace(seeded_ids.workspace_id)
|
|
assert len(rows) == 1
|
|
assert rows[0].day_type == "vacation"
|
|
|
|
async def test_assert_none_is_the_exclusion_shape(
|
|
self, day_assertion_service: DayAssertionService, seeded_ids: SeededIds
|
|
) -> None:
|
|
created = await day_assertion_service.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
_DAY,
|
|
day_type=DAY_TYPE_NONE,
|
|
)
|
|
assert created.day_type == "none"
|
|
|
|
async def test_invalid_day_type_is_rejected(
|
|
self, day_assertion_service: DayAssertionService, seeded_ids: SeededIds
|
|
) -> None:
|
|
with pytest.raises(ValidationError):
|
|
await day_assertion_service.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
_DAY,
|
|
day_type="not-a-real-type",
|
|
)
|
|
|
|
async def test_clear_is_idempotent(
|
|
self, day_assertion_service: DayAssertionService, seeded_ids: SeededIds
|
|
) -> None:
|
|
await day_assertion_service.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
_DAY,
|
|
day_type="lagsta",
|
|
)
|
|
await day_assertion_service.clear(
|
|
seeded_ids.workspace_id, seeded_ids.child_id, seeded_ids.personal_data_uuid, _DAY
|
|
)
|
|
assert await day_assertion_service.list_for_workspace(seeded_ids.workspace_id) == []
|
|
|
|
# A no-op the second time.
|
|
await day_assertion_service.clear(
|
|
seeded_ids.workspace_id, seeded_ids.child_id, seeded_ids.personal_data_uuid, _DAY
|
|
)
|
|
|
|
async def test_get_is_workspace_scoped(
|
|
self, day_assertion_service: DayAssertionService, seeded_ids: SeededIds
|
|
) -> None:
|
|
created = await day_assertion_service.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
_DAY,
|
|
day_type="lagsta",
|
|
)
|
|
with pytest.raises(NotFoundError):
|
|
await day_assertion_service.get(created.id, workspace_id=uuid4())
|
|
assert (
|
|
await day_assertion_service.get(created.id, workspace_id=seeded_ids.workspace_id)
|
|
).id == created.id
|
|
|
|
|
|
class TestDayAssertionServiceMirroring:
|
|
"""Restructure step 3b: `assert_day`/`clear` mirror into
|
|
|
|
`planned_leave_day` immediately — the same "true the moment the call
|
|
returns" guarantee `PlannedDayExclusionService.exclude()` used to
|
|
provide, now generalised to every day_type including a concrete pin.
|
|
"""
|
|
|
|
async def test_assert_day_mirrors_a_concrete_pin_into_planned_leave_day(
|
|
self,
|
|
day_assertion_repo: SQLiteDayAssertionRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
service = DayAssertionService(day_assertion_repo, planned_leave_repo=planned_leave_repo)
|
|
|
|
await service.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
_DAY,
|
|
day_type="vacation",
|
|
)
|
|
|
|
rows = await planned_leave_repo.list_by_workspace_child(
|
|
seeded_ids.workspace_id, seeded_ids.child_id
|
|
)
|
|
assert len(rows) == 1
|
|
assert rows[0].date == _DAY
|
|
assert rows[0].day_type == "vacation"
|
|
|
|
async def test_reasserting_a_different_day_type_replaces_the_mirrored_row(
|
|
self,
|
|
day_assertion_repo: SQLiteDayAssertionRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
service = DayAssertionService(day_assertion_repo, planned_leave_repo=planned_leave_repo)
|
|
await service.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
_DAY,
|
|
day_type="lagsta",
|
|
)
|
|
await service.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
_DAY,
|
|
day_type="vacation",
|
|
)
|
|
|
|
rows = await planned_leave_repo.list_by_workspace_child(
|
|
seeded_ids.workspace_id, seeded_ids.child_id
|
|
)
|
|
assert len(rows) == 1
|
|
assert rows[0].day_type == "vacation"
|
|
|
|
async def test_asserting_none_deletes_the_mirrored_row(
|
|
self,
|
|
day_assertion_repo: SQLiteDayAssertionRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
service = DayAssertionService(day_assertion_repo, planned_leave_repo=planned_leave_repo)
|
|
await service.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
_DAY,
|
|
day_type="vacation",
|
|
)
|
|
|
|
await service.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
_DAY,
|
|
day_type=DAY_TYPE_NONE,
|
|
)
|
|
|
|
rows = await planned_leave_repo.list_by_workspace_child(
|
|
seeded_ids.workspace_id, seeded_ids.child_id
|
|
)
|
|
assert rows == []
|
|
|
|
async def test_clear_deletes_the_mirrored_row_too(
|
|
self,
|
|
day_assertion_repo: SQLiteDayAssertionRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
service = DayAssertionService(day_assertion_repo, planned_leave_repo=planned_leave_repo)
|
|
await service.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
_DAY,
|
|
day_type="vacation",
|
|
)
|
|
|
|
await service.clear(
|
|
seeded_ids.workspace_id, seeded_ids.child_id, seeded_ids.personal_data_uuid, _DAY
|
|
)
|
|
|
|
rows = await planned_leave_repo.list_by_workspace_child(
|
|
seeded_ids.workspace_id, seeded_ids.child_id
|
|
)
|
|
assert rows == []
|
|
# Idempotent — a second clear with nothing left is a no-op.
|
|
await service.clear(
|
|
seeded_ids.workspace_id, seeded_ids.child_id, seeded_ids.personal_data_uuid, _DAY
|
|
)
|
|
|
|
|
|
class TestDayAssertionServiceAtomicity:
|
|
"""Adversarial-review fix #1: `assert_day`/`clear` each do an assertion
|
|
|
|
write followed by a `planned_leave_day` mirror write — two statements
|
|
that must succeed or fail together. Before the fix, `self._repo.upsert`
|
|
(or `delete_by_natural_key`) committed immediately and the mirror step
|
|
ran as its own separate commit(s), so a crash mid-mirror left the
|
|
assertion table and `planned_leave_day` permanently diverged. These
|
|
tests inject a failure INTO the mirror step (not just the happy path)
|
|
and prove the assertion write itself was rolled back with it.
|
|
"""
|
|
|
|
async def test_failure_during_mirror_leaves_no_assertion_committed(
|
|
self,
|
|
day_assertion_service_with_uow: DayAssertionService,
|
|
day_assertion_repo: SQLiteDayAssertionRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
seeded_ids: SeededIds,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
async def boom(*_args: object, **_kwargs: object) -> None:
|
|
raise RuntimeError("simulated failure mid-mirror")
|
|
|
|
monkeypatch.setattr(planned_leave_repo, "create", boom)
|
|
|
|
with pytest.raises(RuntimeError):
|
|
await day_assertion_service_with_uow.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
_DAY,
|
|
day_type="vacation",
|
|
)
|
|
|
|
# Neither half of the write landed — the assertion table and
|
|
# `planned_leave_day` never diverged.
|
|
assert (
|
|
await day_assertion_repo.get_by_natural_key(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
_DAY,
|
|
)
|
|
is None
|
|
)
|
|
assert (
|
|
await planned_leave_repo.list_by_workspace_child(
|
|
seeded_ids.workspace_id, seeded_ids.child_id
|
|
)
|
|
== []
|
|
)
|
|
|
|
async def test_failure_during_clear_mirror_leaves_the_original_assertion_intact(
|
|
self,
|
|
day_assertion_service_with_uow: DayAssertionService,
|
|
day_assertion_repo: SQLiteDayAssertionRepository,
|
|
planned_leave_repo: SQLitePlannedLeaveRepository,
|
|
seeded_ids: SeededIds,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
created = await day_assertion_service_with_uow.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
_DAY,
|
|
day_type="vacation",
|
|
)
|
|
|
|
async def boom(*_args: object, **_kwargs: object) -> None:
|
|
raise RuntimeError("simulated failure mid-mirror")
|
|
|
|
monkeypatch.setattr(planned_leave_repo, "delete_many", boom)
|
|
|
|
with pytest.raises(RuntimeError):
|
|
await day_assertion_service_with_uow.clear(
|
|
seeded_ids.workspace_id, seeded_ids.child_id, seeded_ids.personal_data_uuid, _DAY
|
|
)
|
|
|
|
# The assertion delete was rolled back along with the failed mirror
|
|
# delete — the original assertion (and its mirrored row) survive
|
|
# untouched, rather than the assertion vanishing while the row it
|
|
# was meant to keep in step with stays behind.
|
|
after = await day_assertion_repo.get_by_natural_key(
|
|
seeded_ids.workspace_id, seeded_ids.child_id, seeded_ids.personal_data_uuid, _DAY
|
|
)
|
|
assert after is not None
|
|
assert after.id == created.id
|
|
rows = await planned_leave_repo.list_by_workspace_child(
|
|
seeded_ids.workspace_id, seeded_ids.child_id
|
|
)
|
|
assert len(rows) == 1
|
|
|
|
|
|
class TestDayAssertionServiceGuards:
|
|
"""The free-day guard (`app.services.day_guards.reject_illegal_free_day`)
|
|
|
|
is enforced here too now — the same rule `PlanningService.
|
|
add_planned_day`/`update` used to be the only enforcer of.
|
|
"""
|
|
|
|
_SATURDAY = date(2027, 2, 6)
|
|
|
|
async def test_vacation_on_a_free_day_is_rejected(
|
|
self,
|
|
day_assertion_repo: SQLiteDayAssertionRepository,
|
|
workspace_repo: SQLiteWorkspaceRepository,
|
|
day_override_repo: SQLiteDayOverrideRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
service = DayAssertionService(
|
|
day_assertion_repo, workspace_repo=workspace_repo, day_override_repo=day_override_repo
|
|
)
|
|
with pytest.raises(ValidationError):
|
|
await service.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
self._SATURDAY,
|
|
day_type="vacation",
|
|
)
|
|
|
|
async def test_sjukpenning_on_a_free_day_is_allowed_when_workspace_opts_in(
|
|
self,
|
|
day_assertion_repo: SQLiteDayAssertionRepository,
|
|
workspace_repo: SQLiteWorkspaceRepository,
|
|
day_override_repo: SQLiteDayOverrideRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
workspace = await workspace_repo.get_by_id(seeded_ids.workspace_id)
|
|
assert workspace is not None
|
|
workspace.take_pl_on_red_days = True
|
|
await workspace_repo.update(workspace.id, workspace)
|
|
|
|
service = DayAssertionService(
|
|
day_assertion_repo, workspace_repo=workspace_repo, day_override_repo=day_override_repo
|
|
)
|
|
created = await service.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
self._SATURDAY,
|
|
day_type="sjukpenning",
|
|
)
|
|
assert created.day_type == "sjukpenning"
|
|
|
|
async def test_exclusion_is_always_allowed_on_a_free_day(
|
|
self,
|
|
day_assertion_repo: SQLiteDayAssertionRepository,
|
|
workspace_repo: SQLiteWorkspaceRepository,
|
|
day_override_repo: SQLiteDayOverrideRepository,
|
|
seeded_ids: SeededIds,
|
|
) -> None:
|
|
"""`day_type='none'` is never a "plan leave here" instruction, so the
|
|
|
|
free-day guard (which only ever governs sjukpenning/lagsta/vacation)
|
|
must not block it.
|
|
"""
|
|
service = DayAssertionService(
|
|
day_assertion_repo, workspace_repo=workspace_repo, day_override_repo=day_override_repo
|
|
)
|
|
created = await service.assert_day(
|
|
seeded_ids.workspace_id,
|
|
seeded_ids.child_id,
|
|
seeded_ids.personal_data_uuid,
|
|
self._SATURDAY,
|
|
day_type=DAY_TYPE_NONE,
|
|
)
|
|
assert created.day_type == DAY_TYPE_NONE
|