"""Driver lifecycle validators — Phase 10.1.""" from __future__ import annotations import re from datetime import date from shared.exceptions import AppError from app.models.types import DriverLifecycleAction, DriverStatus from app.validators.foundation import validate_non_empty PHONE_RE = re.compile(r"^\+?[0-9]{8,15}$") EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") ALLOWED_TRANSITIONS: dict[DriverLifecycleAction, set[DriverStatus]] = { DriverLifecycleAction.ACTIVATE: { DriverStatus.PENDING, DriverStatus.INACTIVE, DriverStatus.SUSPENDED, }, DriverLifecycleAction.SUSPEND: {DriverStatus.ACTIVE}, DriverLifecycleAction.RESUME: {DriverStatus.SUSPENDED}, DriverLifecycleAction.DEACTIVATE: { DriverStatus.ACTIVE, DriverStatus.SUSPENDED, }, DriverLifecycleAction.BLOCK: { DriverStatus.PENDING, DriverStatus.ACTIVE, DriverStatus.SUSPENDED, DriverStatus.INACTIVE, }, DriverLifecycleAction.UNBLOCK: {DriverStatus.BLOCKED}, DriverLifecycleAction.ARCHIVE: { DriverStatus.INACTIVE, DriverStatus.BLOCKED, }, } TERMINAL_STATUSES = frozenset({DriverStatus.ARCHIVED}) TARGET_STATUS: dict[DriverLifecycleAction, DriverStatus] = { DriverLifecycleAction.ACTIVATE: DriverStatus.ACTIVE, DriverLifecycleAction.SUSPEND: DriverStatus.SUSPENDED, DriverLifecycleAction.RESUME: DriverStatus.ACTIVE, DriverLifecycleAction.DEACTIVATE: DriverStatus.INACTIVE, DriverLifecycleAction.BLOCK: DriverStatus.BLOCKED, DriverLifecycleAction.UNBLOCK: DriverStatus.INACTIVE, DriverLifecycleAction.ARCHIVE: DriverStatus.ARCHIVED, } def ensure_driver_lifecycle_transition( *, action: DriverLifecycleAction, current: DriverStatus ) -> None: if current in TERMINAL_STATUSES: raise AppError( "راننده در وضعیت پایانی است و قابل تغییر نیست", status_code=409, error_code="driver_terminal", details={"status": current.value, "action": action.value}, ) allowed = ALLOWED_TRANSITIONS.get(action, set()) if current not in allowed: raise AppError( "انتقال وضعیت راننده مجاز نیست", status_code=409, error_code="invalid_driver_lifecycle_transition", details={ "from_status": current.value, "action": action.value, "allowed_from": sorted(s.value for s in allowed), }, ) def target_status_for(action: DriverLifecycleAction) -> DriverStatus: return TARGET_STATUS[action] def validate_reason(reason: str | None, *, required: bool = False) -> str | None: if reason is None or not str(reason).strip(): if required: raise AppError( "دلیل الزامی است", status_code=422, error_code="reason_required", ) return None text = str(reason).strip() if len(text) > 500: raise AppError( "دلیل بیش از حد طولانی است", status_code=422, error_code="reason_too_long", ) return text def validate_mobile(mobile: str | None) -> str | None: if mobile is None or not str(mobile).strip(): return None value = str(mobile).strip() if not PHONE_RE.match(value): raise AppError( "شماره موبایل نامعتبر است", status_code=422, error_code="invalid_mobile", details={"field": "mobile"}, ) return value def validate_email(email: str | None) -> str | None: if email is None or not str(email).strip(): return None value = str(email).strip() if not EMAIL_RE.match(value): raise AppError( "ایمیل نامعتبر است", status_code=422, error_code="invalid_email", details={"field": "email"}, ) return value def validate_credential_dates( *, issued_at: date | None, expires_at: date | None ) -> None: if issued_at and expires_at and expires_at < issued_at: raise AppError( "تاریخ انقضا نمی‌تواند قبل از تاریخ صدور باشد", status_code=422, error_code="invalid_credential_dates", ) def validate_display_name(name: str) -> str: return validate_non_empty(name, "display_name")