229 lines
11 KiB
Python
229 lines
11 KiB
Python
"""
|
|
đŠ Subscription Tier Pydantic Schemas
|
|
|
|
A subscription_tiers JSONB rules mezĆjĂ©nek erĆsen tipizĂĄlt Pydantic modelljei.
|
|
A rules struktĂșra a seed_packages.py-ban definiĂĄlt formĂĄtumot követi:
|
|
|
|
{
|
|
"type": "private" | "corporate",
|
|
"pricing_zones": {
|
|
"HU": {"monthly_price": 3000, "yearly_price": 32500, "currency": "HUF"},
|
|
"DEFAULT": {"monthly_price": 12.99, "yearly_price": 129.99, "currency": "EUR"}
|
|
},
|
|
"allowances": {"max_vehicles": int, "max_garages": int, "monthly_free_credits": int},
|
|
"entitlements": ["SRV_..."],
|
|
"affiliate": {"commission_rate_percent": int, "referral_bonus_credits": int}
|
|
}
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
|
|
|
# ââ BelsĆ (embedded) modellek a rules JSONB struktĂșrĂĄhoz ââââââââââââââââââââââ
|
|
|
|
|
|
class PricingZoneModel(BaseModel):
|
|
"""Egyetlen ĂĄrazĂĄsi zĂłna adatai (orszĂĄg-specifikus ĂĄr)."""
|
|
monthly_price: float = Field(..., ge=0, description="Havi elĆfizetĂ©si dĂj")
|
|
yearly_price: float = Field(..., ge=0, description="Ăves elĆfizetĂ©si dĂj")
|
|
currency: str = Field(default="EUR", min_length=3, max_length=3, description="Pénznem ISO 4217 kódja")
|
|
credit_price: Optional[int] = Field(default=None, ge=0, description="Kreditår (havi kredit költség a csomaghoz)")
|
|
|
|
|
|
class PricingModel(BaseModel):
|
|
"""ĂrazĂĄsi informĂĄciĂłk egy elĆfizetĂ©si csomaghoz (legacy, egyzĂłnĂĄs)."""
|
|
monthly_price: float = Field(..., ge=0, description="Havi elĆfizetĂ©si dĂj (EUR)")
|
|
yearly_price: float = Field(..., ge=0, description="Ăves elĆfizetĂ©si dĂj (EUR)")
|
|
currency: str = Field(default="EUR", min_length=3, max_length=3, description="Pénznem ISO 4217 kódja")
|
|
credit_price: Optional[int] = Field(default=None, ge=0, description="Kreditår (havi kredit költség a csomaghoz)")
|
|
|
|
|
|
class AllowancesModel(BaseModel):
|
|
"""Korlåtok és keretek a csomagban."""
|
|
max_vehicles: int = Field(default=0, ge=0, description="Maximålis jårmƱszåm")
|
|
max_garages: int = Field(default=0, ge=0, description="MaximĂĄlis garĂĄzsok szĂĄma")
|
|
max_users: int = Field(default=0, ge=0, description="MaximĂĄlis dolgozĂłk/felhasznĂĄlĂłk szĂĄma (0 = nincs korlĂĄtozva)")
|
|
monthly_free_credits: int = Field(default=0, ge=0, description="Havi ingyenes kreditek szĂĄma")
|
|
|
|
|
|
class DurationModel(BaseModel):
|
|
"""ElĆfizetĂ©s idĆtartamĂĄnak beĂĄllĂtĂĄsai (duration_days stacking)."""
|
|
days: int = Field(default=30, ge=1, description="ElĆfizetĂ©s idĆtartama napokban (pl. 30 = havi, 365 = Ă©ves)")
|
|
allow_stacking: bool = Field(default=True, description="IdĆhalmozĂĄs engedĂ©lyezĂ©se (stacking): ha True, a meglĂ©vĆ valid_until-hoz hozzĂĄadĂłdik")
|
|
|
|
|
|
class AdPolicyModel(BaseModel):
|
|
"""Hirdetési szabålyok a csomaghoz."""
|
|
show_ads: bool = Field(default=True, description="Megjelenjenek-e hirdetések a felhasznålónak")
|
|
ad_free_grace_days: int = Field(default=0, ge=0, description="HĂĄny nap tĂŒrelmi idĆ a reklĂĄmok eltƱnĂ©sĂ©ig fizetĂ©s utĂĄn")
|
|
max_daily_impressions: Optional[int] = Field(default=None, ge=0, description="MaximĂĄlis napi hirdetĂ©smegjelenĂtĂ©s (None = korlĂĄtlan)")
|
|
|
|
|
|
class LifecycleModel(BaseModel):
|
|
"""Ăletciklus ĂĄllapotok egy elĆfizetĂ©si csomaghoz."""
|
|
is_public: bool = Field(default=True, description="Publikus-e a csomag (lĂĄtszik-e a felhasznĂĄlĂłknak)")
|
|
available_until: Optional[datetime] = Field(default=None, description="Meddig Ă©rhetĆ el a csomag (None = nincs lejĂĄrat)")
|
|
|
|
|
|
class AffiliateModel(BaseModel):
|
|
"""Partnerprogram (affiliate) beĂĄllĂtĂĄsok."""
|
|
commission_rate_percent: int = Field(..., ge=0, le=100, description="Jutalék mértéke szåzalékban")
|
|
referral_bonus_credits: int = Field(..., ge=0, description="AjĂĄnlĂĄsi bĂłnusz kreditek szĂĄma")
|
|
|
|
|
|
class MarketingModel(BaseModel):
|
|
"""Marketing informĂĄciĂłk egy elĆfizetĂ©si csomaghoz (kĂĄrtya megjelenĂtĂ©shez)."""
|
|
subtitle: Optional[str] = Field(default=None, description="Rövid leĂrĂł szöveg a kĂĄrtyĂĄn (pl. 'Kisebb flotta esetĂ©n.')")
|
|
badge: Optional[str] = Field(default=None, description="Jelvény szöveg (pl. 'LegnépszerƱbb', 'Pro')")
|
|
highlight_color: Optional[str] = Field(default=None, description="KiemelĆ szĂn hex kĂłdja (pl. '#70BC84')")
|
|
|
|
|
|
class SubscriptionRulesModel(BaseModel):
|
|
"""
|
|
A subscription_tiers.rules JSONB oszlopĂĄnak erĆsen tipizĂĄlt modellje.
|
|
|
|
Fields:
|
|
type: "private" (magånszemély) vagy "corporate" (vållalati)
|
|
display_name: Emberi olvasĂĄsra szĂĄnt megjelenĂtĂ©si nĂ©v (pl. "PrivĂĄt Ingyenes")
|
|
pricing: ĂrazĂĄsi adatok (legacy, egyzĂłnĂĄs) - deprecated, hasznĂĄld a pricing_zones-t
|
|
pricing_zones: Több régiós årazåsi adatok (orszågkód -> PricingZoneModel)
|
|
allowances: Korlåtok (max jårmƱ, garåzs, kredit)
|
|
entitlements: JogosultsĂĄgok listĂĄja (pl. SRV_DATA_EXPORT)
|
|
affiliate: Partnerprogram beĂĄllĂtĂĄsok
|
|
lifecycle: Ăletciklus ĂĄllapotok (opcionĂĄlis, pl. is_public)
|
|
duration: ElĆfizetĂ©s idĆtartamĂĄnak beĂĄllĂtĂĄsai (duration_days, stacking)
|
|
ad_policy: Hirdetési szabålyok (show_ads, ad_free_grace_days)
|
|
marketing: Marketing adatok a kĂĄrtya megjelenĂtĂ©shez (subtitle, badge, highlight_color)
|
|
"""
|
|
type: Optional[str] = Field(default=None, pattern=r"^(private|corporate|addon)$", description="Csomag tĂpusa (private/corporate/addon)")
|
|
display_name: Optional[str] = Field(
|
|
default=None,
|
|
description="Emberi olvasĂĄsra szĂĄnt megjelenĂtĂ©si nĂ©v (pl. 'PrivĂĄt Ingyenes')",
|
|
)
|
|
pricing: Optional[PricingModel] = Field(
|
|
default=None,
|
|
description="ĂrazĂĄsi adatok (legacy, egyzĂłnĂĄs). Deprecated: hasznĂĄld a pricing_zones-t.",
|
|
)
|
|
pricing_zones: Optional[Dict[str, PricingZoneModel]] = Field(
|
|
default=None,
|
|
description="Több régiós årazås. Kulcs: orszågkód (pl. 'HU', 'DE', 'DEFAULT').",
|
|
)
|
|
allowances: Optional[AllowancesModel] = Field(
|
|
default=None,
|
|
description="Korlåtok (max jårmƱ, garåzs, kredit). Opcionålis a hiånyos seed adatok miatt."
|
|
)
|
|
entitlements: List[str] = Field(default_factory=list, description="JogosultsĂĄgok (entitlement) listĂĄja")
|
|
affiliate: Optional[AffiliateModel] = Field(
|
|
default=None,
|
|
description="Partnerprogram beĂĄllĂtĂĄsok. OpcionĂĄlis a hiĂĄnyos seed adatok miatt."
|
|
)
|
|
lifecycle: Optional[LifecycleModel] = Field(
|
|
default=None,
|
|
description="Ăletciklus ĂĄllapotok (is_public, available_until)"
|
|
)
|
|
duration: Optional[DurationModel] = Field(
|
|
default=None,
|
|
description="ElĆfizetĂ©s idĆtartamĂĄnak beĂĄllĂtĂĄsai (duration_days, allow_stacking)"
|
|
)
|
|
ad_policy: Optional[AdPolicyModel] = Field(
|
|
default=None,
|
|
description="Hirdetési szabålyok (show_ads, ad_free_grace_days, max_daily_impressions)"
|
|
)
|
|
marketing: Optional[MarketingModel] = Field(
|
|
default=None,
|
|
description="Marketing adatok a kĂĄrtya megjelenĂtĂ©shez (subtitle, badge, highlight_color)"
|
|
)
|
|
|
|
@field_validator("type")
|
|
@classmethod
|
|
def validate_type(cls, v: Optional[str]) -> Optional[str]:
|
|
if v is None:
|
|
return v
|
|
if v.lower() not in ("private", "corporate", "addon"):
|
|
raise ValueError("A 'type' mezĆ csak 'private', 'corporate' vagy 'addon' lehet.")
|
|
return v.lower()
|
|
|
|
|
|
# ââ API Response / Request modellek âââââââââââââââââââââââââââââââââââââââââââ
|
|
|
|
|
|
class SubscriptionTierResponse(BaseModel):
|
|
"""Teljes SubscriptionTier rekord visszaadĂĄsa (GET /, GET /{id})."""
|
|
id: int
|
|
name: str
|
|
rules: SubscriptionRulesModel
|
|
is_custom: bool
|
|
tier_level: int = Field(default=0, description="Entitlement hierarchy level: 0=free, 1=premium, 2=enterprise")
|
|
feature_capabilities: dict = Field(default_factory=dict, description="Feature capability flags JSONB")
|
|
is_default_fallback: bool = Field(default=False, description="Singleton: maximum one tier can be the default fallback")
|
|
trial_days_on_signup: int = Field(default=0, ge=0, description="Singleton: maximum one tier can have trial_days_on_signup > 0")
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class PublicSubscriptionTierResponse(BaseModel):
|
|
"""
|
|
Publikus elĆfizetĂ©si csomag vĂĄlasz a frontend szĂĄmĂĄra.
|
|
Tartalmazza a feloldott (resolved) årazåst a felhasznåló régiója alapjån.
|
|
"""
|
|
id: int
|
|
name: str
|
|
rules: SubscriptionRulesModel
|
|
is_custom: bool
|
|
resolved_pricing: Optional[PricingZoneModel] = Field(
|
|
default=None,
|
|
description="A felhasznåló régiójåra feloldott årazås (egyetlen PricingZoneModel).",
|
|
)
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class SubscriptionTierCreate(BaseModel):
|
|
"""Ăj elĆfizetĂ©si csomag lĂ©trehozĂĄsa (POST /)."""
|
|
name: str = Field(..., min_length=2, max_length=100, description="Csomag neve (pl. private_pro_v2)")
|
|
type: Optional[str] = Field(default="base", description="Csomag tĂpusa: 'base' (alap) vagy 'addon' (kiegĂ©szĂtĆ)")
|
|
rules: SubscriptionRulesModel
|
|
is_custom: bool = Field(default=False, description="Egyedi (custom) csomag-e")
|
|
tier_level: int = Field(default=0, ge=0, description="Entitlement hierarchy level: 0=free, 1=premium, 2=enterprise")
|
|
feature_capabilities: dict = Field(default_factory=dict, description="Feature capability flags JSONB")
|
|
is_default_fallback: bool = Field(default=False, description="Singleton: maximum one tier can be the default fallback")
|
|
trial_days_on_signup: int = Field(default=0, ge=0, description="Singleton: maximum one tier can have trial_days_on_signup > 0")
|
|
|
|
@field_validator("name")
|
|
@classmethod
|
|
def validate_name(cls, v: str) -> str:
|
|
if not v.strip():
|
|
raise ValueError("A csomag neve nem lehet ĂŒres.")
|
|
return v.strip().lower()
|
|
|
|
|
|
class SubscriptionTierUpdate(BaseModel):
|
|
"""MeglĂ©vĆ elĆfizetĂ©si csomag rĂ©szleges frissĂtĂ©se (PATCH /{tier_id})."""
|
|
name: Optional[str] = Field(default=None, min_length=2, max_length=100, description="Ăj csomagnĂ©v")
|
|
type: Optional[str] = Field(default=None, description="Csomag tĂpusa: 'base' vagy 'addon'")
|
|
rules: Optional[SubscriptionRulesModel] = Field(default=None, description="FrissĂtett rules JSONB")
|
|
is_custom: Optional[bool] = Field(default=None, description="Egyedi csomag jelölés")
|
|
tier_level: Optional[int] = Field(default=None, ge=0, description="Entitlement hierarchy level")
|
|
feature_capabilities: Optional[dict] = Field(default=None, description="Feature capability flags JSONB")
|
|
is_default_fallback: Optional[bool] = Field(default=None, description="Singleton: maximum one tier can be the default fallback")
|
|
trial_days_on_signup: Optional[int] = Field(default=None, ge=0, description="Singleton: maximum one tier can have trial_days_on_signup > 0")
|
|
|
|
@field_validator("name")
|
|
@classmethod
|
|
def validate_name(cls, v: Optional[str]) -> Optional[str]:
|
|
if v is not None and not v.strip():
|
|
raise ValueError("A csomag neve nem lehet ĂŒres.")
|
|
return v.strip().lower() if v else v
|
|
|
|
|
|
class SubscriptionTierListResponse(BaseModel):
|
|
"""Csomagok listĂĄzĂĄsĂĄnak vĂĄlaszformĂĄtuma (GET /)."""
|
|
total: int
|
|
tiers: List[SubscriptionTierResponse]
|