"""Appointment validators — Phase 13.1.""" from __future__ import annotations from datetime import datetime from shared.exceptions import AppError from app.models.types import AppointmentLifecycleAction, AppointmentStatus ALLOWED_TRANSITIONS: dict[AppointmentLifecycleAction, set[AppointmentStatus]] = { AppointmentLifecycleAction.BOOK: set(), AppointmentLifecycleAction.CONFIRM: {AppointmentStatus.DRAFT}, AppointmentLifecycleAction.CHECK_IN: {AppointmentStatus.CONFIRMED}, AppointmentLifecycleAction.COMPLETE: { AppointmentStatus.CONFIRMED, AppointmentStatus.CHECKED_IN, }, AppointmentLifecycleAction.CANCEL: { AppointmentStatus.DRAFT, AppointmentStatus.CONFIRMED, AppointmentStatus.CHECKED_IN, }, AppointmentLifecycleAction.RESCHEDULE: { AppointmentStatus.DRAFT, AppointmentStatus.CONFIRMED, }, AppointmentLifecycleAction.MARK_NO_SHOW: {AppointmentStatus.CONFIRMED}, } TARGET_STATUS: dict[AppointmentLifecycleAction, AppointmentStatus] = { AppointmentLifecycleAction.BOOK: AppointmentStatus.DRAFT, AppointmentLifecycleAction.CONFIRM: AppointmentStatus.CONFIRMED, AppointmentLifecycleAction.CHECK_IN: AppointmentStatus.CHECKED_IN, AppointmentLifecycleAction.COMPLETE: AppointmentStatus.COMPLETED, AppointmentLifecycleAction.CANCEL: AppointmentStatus.CANCELLED, AppointmentLifecycleAction.MARK_NO_SHOW: AppointmentStatus.NO_SHOW, } TERMINAL_STATUSES = frozenset( {AppointmentStatus.COMPLETED, AppointmentStatus.CANCELLED, AppointmentStatus.NO_SHOW} ) def ensure_appointment_lifecycle_transition( *, action: AppointmentLifecycleAction, current: AppointmentStatus ) -> None: if current in TERMINAL_STATUSES: raise AppError( "نوبت در وضعیت پایانی است", status_code=409, error_code="appointment_terminal", details={"status": current.value, "action": action.value}, ) allowed = ALLOWED_TRANSITIONS.get(action, set()) if action != AppointmentLifecycleAction.BOOK and current not in allowed: raise AppError( "انتقال وضعیت نوبت مجاز نیست", status_code=409, error_code="invalid_appointment_transition", details={ "from_status": current.value, "action": action.value, "allowed_from": sorted(s.value for s in allowed), }, ) def target_status_for(action: AppointmentLifecycleAction) -> AppointmentStatus: return TARGET_STATUS[action] def validate_time_window(*, starts_at: datetime, ends_at: datetime) -> None: if ends_at <= starts_at: raise AppError( "بازه زمانی نامعتبر است", status_code=422, error_code="invalid_time_window", ) def validate_schedule_times(*, start_time, end_time) -> None: if end_time <= start_time: raise AppError( "ساعت پایان باید بعد از ساعت شروع باشد", status_code=422, error_code="invalid_schedule_times", )