"""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}, )