TorbatYar/backend/services/delivery/app/validators/dispatch.py
Mortezakoohjani 72077908f1 feat(delivery): deploy backend phases 10.2-10.8 through settlement
Ship fleet, availability, pricing, dispatch, routing, tracking, and settlement on delivery-service 0.10.8.0 with migrations and phase tests green.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 21:01:23 +03:30

85 lines
2.8 KiB
Python

"""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