"""Shared write-time guard: a free day can never be `vacation`. Extracted out of `PlanningService._reject_illegal_free_day` (restructure step 3b) so `DayAssertionService` — which now owns the hand-pin write path too, see its module docstring — enforces the exact same rule instead of growing its own copy. A free day (Saturday, Sunday, or Swedish red day, subject to a workspace's `DayOverride`s — see `allocation_service.is_free_day`, the single source of truth for "is this day free") can never be persisted as `vacation`, and may be `sjukpenning`/`lagsta` only when the workspace opted in via `Workspace.take_pl_on_red_days` (default False). """ from datetime import date from app.entities.workspace import Workspace from app.services.allocation_service import is_free_day from app.utils.errors import ValidationError from app.utils.holidays import get_swedish_holidays def reject_illegal_free_day( workspace: Workspace | None, day_overrides: dict[date, bool], d: date, day_type: str, ) -> None: """Raise `ValidationError` iff `day_type` may not be planned on `d`. No-op for any `day_type` other than `sjukpenning`/`lagsta`/`vacation` (e.g. `'none'`, the exclusion-shaped assertion — excluding a day is always allowed, free or not) and a no-op entirely when `d` is not a free day in the first place. """ if day_type not in ("vacation", "sjukpenning", "lagsta"): return holidays_map = get_swedish_holidays(d.year) if not is_free_day(d, holidays_map, day_overrides): return if day_type == "vacation": raise ValidationError( "Vacation cannot be planned on a free day (weekend or red day)", message_key="planning.rejection.vacationOnFreeDay", data={"date": d.isoformat()}, ) take_pl_on_red_days = workspace.take_pl_on_red_days if workspace is not None else False if not take_pl_on_red_days: raise ValidationError( "Parental leave cannot be planned on a free day unless the workspace " "setting take_pl_on_red_days is enabled", message_key="planning.rejection.parentalLeaveOnFreeDayDisabled", data={"date": d.isoformat()}, )