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>
85 lines
2.8 KiB
Python
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
|