"""Dispatch validators — Phase 10.5.""" from __future__ import annotations from shared.exceptions import AppError from app.models.types import DispatchJobAction, DispatchJobStatus ALLOWED_TRANSITIONS: dict[DispatchJobAction, set[DispatchJobStatus]] = { DispatchJobAction.ASSIGN: {DispatchJobStatus.PENDING}, DispatchJobAction.START: {DispatchJobStatus.ASSIGNED}, DispatchJobAction.COMPLETE: { DispatchJobStatus.ASSIGNED, DispatchJobStatus.IN_PROGRESS, }, DispatchJobAction.CANCEL: { DispatchJobStatus.PENDING, DispatchJobStatus.ASSIGNED, DispatchJobStatus.IN_PROGRESS, }, DispatchJobAction.FAIL: { DispatchJobStatus.PENDING, DispatchJobStatus.ASSIGNED, DispatchJobStatus.IN_PROGRESS, }, } TARGET_STATUS: dict[DispatchJobAction, DispatchJobStatus] = { DispatchJobAction.ASSIGN: DispatchJobStatus.ASSIGNED, DispatchJobAction.START: DispatchJobStatus.IN_PROGRESS, DispatchJobAction.COMPLETE: DispatchJobStatus.COMPLETED, DispatchJobAction.CANCEL: DispatchJobStatus.CANCELLED, DispatchJobAction.FAIL: DispatchJobStatus.FAILED, } TERMINAL = frozenset( {DispatchJobStatus.COMPLETED, DispatchJobStatus.CANCELLED, DispatchJobStatus.FAILED} ) def ensure_dispatch_job_transition( *, action: DispatchJobAction, current: DispatchJobStatus ) -> None: if current in TERMINAL: raise AppError( "کار ارسال در وضعیت پایانی است", status_code=409, error_code="dispatch_job_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_dispatch_job_transition", details={ "from_status": current.value, "action": action.value, "allowed_from": sorted(s.value for s in allowed), }, ) def target_status_for(action: DispatchJobAction) -> DispatchJobStatus: return TARGET_STATUS[action] def validate_dispatch_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