from __future__ import annotations import json from dataclasses import dataclass, field from decimal import Decimal from pathlib import Path from typing import Any, Literal PricingModelStatus = Literal["active", "candidate", "retired"] ChargeComponentKind = Literal[ "access", "setup", "usage", "support", "discount", "risk_adjustment", ] ParameterClass = Literal[ "fixed", "seller_controlled", "customer_tunable", "calculated", "constrained", "provider", ] _ALLOWED_COMPONENT_KINDS = { "access", "setup", "usage", "support", "discount", "risk_adjustment", } _ALLOWED_PARAMETER_CLASSES = { "fixed", "seller_controlled", "customer_tunable", "calculated", "constrained", "provider", } def _money(value: str | int | float | Decimal | None) -> Decimal | None: if value in (None, ""): return None return Decimal(str(value)) def _tuple_dict(value: dict[str, Any] | None) -> dict[str, Any]: return dict(value or {}) @dataclass(frozen=True) class ChargeComponent: id: str kind: ChargeComponentKind | str amount: Decimal | None = None cadence: str | None = None meter: str | None = None unit: str | None = None unit_price: Decimal | None = None included_units: Decimal | None = None label: str | None = None billing_treatment: str | None = None metadata: dict[str, Any] = field(default_factory=dict) @dataclass(frozen=True) class Commitment: id: str kind: str value: str unit: str | None = None description: str = "" @dataclass(frozen=True) class TunableParameter: key: str parameter_class: ParameterClass | str data_type: str description: str = "" default_value: str | None = None min_value: Decimal | None = None max_value: Decimal | None = None options: tuple[str, ...] = () @dataclass(frozen=True) class PricingModel: id: str name: str model_type: str lifecycle_phase: str currency: str access_fee_amount: Decimal access_fee_cadence: str status: PricingModelStatus description: str = "" included_usage: str | None = None overage_meter: str | None = None charge_components: tuple[ChargeComponent, ...] = () commitments: tuple[Commitment, ...] = () tunable_parameters: tuple[TunableParameter, ...] = () eligibility: tuple[str, ...] = () provider_hints: dict[str, Any] = field(default_factory=dict) metadata: dict[str, Any] = field(default_factory=dict) def _read_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) def _parse_charge_component(raw: dict[str, Any]) -> ChargeComponent: return ChargeComponent( id=raw["id"], kind=raw["kind"], amount=_money(raw.get("amount")), cadence=raw.get("cadence"), meter=raw.get("meter"), unit=raw.get("unit"), unit_price=_money(raw.get("unit_price")), included_units=_money(raw.get("included_units")), label=raw.get("label"), billing_treatment=raw.get("billing_treatment"), metadata=_tuple_dict(raw.get("metadata")), ) def _parse_commitment(raw: dict[str, Any]) -> Commitment: return Commitment( id=raw["id"], kind=raw["kind"], value=str(raw["value"]), unit=raw.get("unit"), description=raw.get("description", ""), ) def _parse_tunable_parameter(raw: dict[str, Any]) -> TunableParameter: return TunableParameter( key=raw["key"], parameter_class=raw["parameter_class"], data_type=raw["data_type"], description=raw.get("description", ""), default_value=str(raw["default_value"]) if raw.get("default_value") is not None else None, min_value=_money(raw.get("min_value")), max_value=_money(raw.get("max_value")), options=tuple(str(item) for item in raw.get("options", [])), ) def _legacy_charge_components(raw: dict[str, Any]) -> list[dict[str, Any]]: components: list[dict[str, Any]] = [ { "id": f"{raw['id']}-access", "kind": "access", "amount": raw["access_fee_amount"], "cadence": raw["access_fee_cadence"], "label": "Recurring access fee", "billing_treatment": "recurring", "metadata": {"included_usage": raw.get("included_usage")}, } ] if raw.get("model_type") == "hybrid_subscription_usage" or raw.get("overage_meter"): components.append( { "id": f"{raw['id']}-usage", "kind": "usage", "meter": raw.get("overage_meter") or "usage", "unit": raw.get("unit") or "usage_unit", "included_units": raw.get("included_units") or raw.get("included_tokens"), "unit_price": raw.get("unit_price") or raw.get("overage_unit_price"), "label": "Variable usage component", "billing_treatment": "metered", "metadata": {"included_usage": raw.get("included_usage")}, } ) return components def _access_component(components: tuple[ChargeComponent, ...]) -> ChargeComponent | None: return next((component for component in components if component.kind == "access"), None) def _usage_component(components: tuple[ChargeComponent, ...]) -> ChargeComponent | None: return next((component for component in components if component.kind == "usage"), None) def _parse_pricing_model(raw: dict[str, Any]) -> PricingModel: charge_components = tuple( _parse_charge_component(item) for item in (raw.get("charge_components") or _legacy_charge_components(raw)) ) access_component = _access_component(charge_components) usage_component = _usage_component(charge_components) access_fee_amount = _money(raw.get("access_fee_amount")) if access_fee_amount is None: access_fee_amount = access_component.amount if access_component else Decimal("0") access_fee_cadence = raw.get("access_fee_cadence") if access_fee_cadence is None: access_fee_cadence = access_component.cadence if access_component else "monthly" metadata = _tuple_dict(raw.get("metadata")) included_usage = raw.get("included_usage") or metadata.get("included_usage") if included_usage is None and access_component: included_usage = access_component.metadata.get("included_usage") if included_usage is None and usage_component: included_usage = usage_component.metadata.get("included_usage") overage_meter = raw.get("overage_meter") or (usage_component.meter if usage_component else None) return PricingModel( id=raw["id"], name=raw["name"], model_type=raw["model_type"], lifecycle_phase=raw["lifecycle_phase"], currency=raw["currency"], access_fee_amount=access_fee_amount or Decimal("0"), access_fee_cadence=access_fee_cadence or "monthly", status=raw["status"], description=raw.get("description", ""), included_usage=included_usage, overage_meter=overage_meter, charge_components=charge_components, commitments=tuple( _parse_commitment(item) for item in raw.get("commitments", []) ), tunable_parameters=tuple( _parse_tunable_parameter(item) for item in raw.get("tunable_parameters", []) ), eligibility=tuple(str(item) for item in raw.get("eligibility", [])), provider_hints=_tuple_dict(raw.get("provider_hints")), metadata=metadata, ) def load_pricing_models(path: str | Path) -> list[PricingModel]: raw = _read_json(Path(path)) models = [_parse_pricing_model(item) for item in raw["models"]] issues = validate_pricing_catalog(models) if issues: formatted = "; ".join( f"{model_id}: {', '.join(model_issues)}" for model_id, model_issues in sorted(issues.items()) ) raise ValueError(f"invalid pricing catalog: {formatted}") return models def validate_pricing_catalog(models: list[PricingModel]) -> dict[str, list[str]]: issues: dict[str, list[str]] = {} ids = [model.id for model in models] if len(ids) != len(set(ids)): issues.setdefault("__catalog__", []).append("duplicate model ids") for model in models: model_issues = validate_pricing_model(model) if model_issues: issues[model.id] = model_issues return issues def validate_pricing_model(model: PricingModel) -> list[str]: issues: list[str] = [] if model.status not in {"active", "candidate", "retired"}: issues.append(f"unsupported status '{model.status}'") if model.access_fee_amount < Decimal("0"): issues.append("access_fee_amount must be non-negative") if not model.charge_components: issues.append("at least one charge component is required") component_ids = [component.id for component in model.charge_components] if len(component_ids) != len(set(component_ids)): issues.append("charge component ids must be unique") access_components = [component for component in model.charge_components if component.kind == "access"] if len(access_components) != 1: issues.append("exactly one access charge component is required") for component in model.charge_components: if component.kind not in _ALLOWED_COMPONENT_KINDS: issues.append(f"unsupported charge component kind '{component.kind}'") if component.kind == "access": if component.amount is None: issues.append("access charge component must define amount") if component.cadence is None: issues.append("access charge component must define cadence") if component.kind == "usage" and component.meter is None: issues.append("usage charge component must define meter") usage_components = [component for component in model.charge_components if component.kind == "usage"] if model.model_type == "hybrid_subscription_usage" and not usage_components: issues.append("hybrid_subscription_usage requires a usage charge component") tunable_keys = [parameter.key for parameter in model.tunable_parameters] if len(tunable_keys) != len(set(tunable_keys)): issues.append("tunable parameter keys must be unique") for parameter in model.tunable_parameters: if parameter.parameter_class not in _ALLOWED_PARAMETER_CLASSES: issues.append(f"unsupported parameter_class '{parameter.parameter_class}'") if ( parameter.parameter_class == "customer_tunable" and not parameter.options and parameter.min_value is None and parameter.max_value is None ): issues.append( f"customer_tunable parameter '{parameter.key}' must define bounds or options" ) commitment_ids = [commitment.id for commitment in model.commitments] if len(commitment_ids) != len(set(commitment_ids)): issues.append("commitment ids must be unique") return issues