"""Abstract repository interface for DayAssertion. Adds entity-specific queries on top of the generic CRUD contract. Concrete implementations live in app/repositories_impl//. Mirrors `PlannedDayExclusionRepository`'s shape (natural-key lookup + workspace list), since a `DayAssertion` supersedes that entity's role for the allocator (see `app/entities/day_assertion.py`). """ from abc import abstractmethod from datetime import date from uuid import UUID from app.entities.day_assertion import DayAssertion from app.repositories.base import Repository class DayAssertionRepository(Repository[DayAssertion]): @abstractmethod async def get_by_natural_key( self, workspace_id: UUID, child_id: UUID, parent_personal_data_uuid: UUID, day: date, ) -> DayAssertion | None: """Return the assertion for a given (workspace, child, parent, date) (UNIQUE), or None.""" @abstractmethod async def list_by_workspace(self, workspace_id: UUID) -> list[DayAssertion]: """Return every assertion for a workspace, ordered by date. This is what `AllocationInputLoader` reads wholesale once per `load()` call — see its docstring for how the result is folded into `AllocationInput`. """ @abstractmethod async def list_by_wish(self, wish_id: UUID) -> list[DayAssertion]: """Return every assertion attached to this wish (`wish_id` on the assertion itself — NOT derived by walking `planned_leave_day` rows). Adversarial-review fix (restructure step 3b): `LeaveWishService. _clear_assertions_for` used to only clear an assertion at each SURVIVING `planned_leave_day` row's natural key, so an assertion whose mirrored row had already gone missing (e.g. `DayAssertionService` crashing between its own assertion write and its mirror write, before that method's `uow` fix) was invisible to it and outlived the wish as a dangling `wish_id=NULL` ad-hoc assertion (the FK is `ON DELETE SET NULL`), permanently locking that date with no UI path to find it. Querying by `wish_id` directly closes that hole regardless of whether a mirrored row exists. """ @abstractmethod async def delete_by_natural_key( self, workspace_id: UUID, child_id: UUID, parent_personal_data_uuid: UUID, day: date, ) -> None: """Delete the assertion at this key, if any. A no-op when none exists. Idempotent by design (unlike `delete(id)`, which raises `NotFoundError`) — callers clearing a slot rarely know whether an assertion exists there, and shouldn't have to check first. """ @abstractmethod async def upsert(self, item: DayAssertion) -> DayAssertion: """Create-or-replace the assertion at `item`'s natural key. The single write primitive every dual-write call site (`PlanningService`, `PlannedDayExclusionService`) uses — a caller never needs to know whether a row already exists at this (workspace, child, parent, date) before writing the current fact. `item.id` is ignored on an update (the existing row's id is kept) and used as-is on a fresh insert. """