TorbatYar/backend/services/experience/app/validators/bundles.py
Mortezakoohjani 203671a7bf feat(experience): ship Experience Platform phases 11.0-11.10
Add the experience service with sites through analytics/AI hooks, migrations through 0011, and phase docs/manifests marking the track complete.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 11:43:10 +03:30

47 lines
1.7 KiB
Python

"""Bundle and licensing boundary validation — Phase 11.8."""
from typing import Any
from app.validators.foundation import ValidationError, validate_non_empty
from shared.exceptions import AppError
FORBIDDEN_OWNERSHIP_KEYS = frozenset(
{"plan_engine", "subscription_engine", "billing_ledger", "core_plan_id"}
)
def validate_metadata(value: Any) -> dict | None:
if value is None:
return None
if not isinstance(value, dict):
raise ValidationError("metadata_json باید object باشد", {"field": "metadata_json"})
banned = FORBIDDEN_OWNERSHIP_KEYS.intersection(value)
if banned:
raise ValidationError(
"مالکیت plan/subscription/billing متعلق به Core است",
{"field": "metadata_json", "forbidden_keys": sorted(banned)},
)
return value
def validate_entitlement_ref(value: str | None, *, required: bool = False) -> str | None:
if value is None:
if required:
raise ValidationError("core_entitlement_ref الزامی است")
return None
cleaned = validate_non_empty(value, "core_entitlement_ref")
if len(cleaned) > 255:
raise ValidationError("core_entitlement_ref بیش از حد طولانی است")
return cleaned
def ensure_transition(current, target, transitions: dict, label: str) -> None:
if current == target:
return
if target not in transitions.get(current, frozenset()):
raise AppError(
f"گذار وضعیت {label} مجاز نیست",
error_code=f"invalid_{label}_status_transition",
status_code=409,
details={"current": current.value, "target": target.value},
)