Register all active platform services and base features in core DB so admin can manage them, add delivery/hospitality/sports-center backend phases, update apps catalog and production deploy tooling. Co-authored-by: Cursor <cursoragent@cursor.com>
141 lines
4.6 KiB
Python
141 lines
4.6 KiB
Python
"""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")
|