166 lines
6.7 KiB
Python
166 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field, model_validator
|
|
|
|
|
|
ProductType = Literal["subscription", "team_subscription", "credit_addon"]
|
|
BillingCycle = Literal["monthly", "quarterly", "yearly"]
|
|
CreditLevelType = Literal["promotional", "general"]
|
|
|
|
|
|
class CreditProductBase(BaseModel):
|
|
product_code: str = Field(..., min_length=1, max_length=64)
|
|
product_type: ProductType
|
|
name: str = Field(..., min_length=1, max_length=96)
|
|
description: str | None = Field(default=None, max_length=512)
|
|
features: list[str] = Field(default_factory=list)
|
|
|
|
tier_code: str | None = Field(default=None, max_length=32)
|
|
tier_rank: int | None = Field(default=None, ge=1, le=999)
|
|
billing_cycle: BillingCycle | None = None
|
|
monthly_grant_credits: float | None = Field(default=None, gt=0, le=999999999.99)
|
|
first_purchase_price: float | None = Field(default=None, ge=0, le=999999999.99)
|
|
regular_price: float | None = Field(default=None, ge=0, le=999999999.99)
|
|
activity_price: float | None = Field(default=None, ge=0, le=999999999.99)
|
|
activity_start_at: datetime | None = None
|
|
activity_end_at: datetime | None = None
|
|
renewal_enabled: bool = True
|
|
|
|
price: float = Field(default=0, ge=0, le=999999999.99)
|
|
grant_credits: float | None = Field(default=None, gt=0, le=999999999.99)
|
|
validity_months: int | None = Field(default=None, ge=1, le=36)
|
|
credit_level: CreditLevelType = "general"
|
|
currency: str = Field(default="CNY", min_length=1, max_length=8)
|
|
is_active: bool = True
|
|
sort_order: int = Field(default=0, ge=-999999, le=999999)
|
|
|
|
@model_validator(mode="after")
|
|
def validate_product_fields(self):
|
|
if self.product_type in {"subscription", "team_subscription"}:
|
|
required = {
|
|
"tier_code": self.tier_code,
|
|
"tier_rank": self.tier_rank,
|
|
"billing_cycle": self.billing_cycle,
|
|
"monthly_grant_credits": self.monthly_grant_credits,
|
|
"first_purchase_price": self.first_purchase_price,
|
|
"regular_price": self.regular_price,
|
|
}
|
|
missing = [name for name, value in required.items() if value is None]
|
|
if missing:
|
|
raise ValueError(f"订阅套餐缺少字段: {', '.join(missing)}")
|
|
if self.grant_credits is not None or self.validity_months is not None:
|
|
raise ValueError("订阅套餐不能配置积分增值包字段")
|
|
if self.activity_price is not None:
|
|
if not self.activity_start_at or not self.activity_end_at:
|
|
raise ValueError("配置活动价时必须同时配置活动开始和结束时间")
|
|
if self.activity_end_at <= self.activity_start_at:
|
|
raise ValueError("活动结束时间必须晚于开始时间")
|
|
else:
|
|
if self.grant_credits is None:
|
|
raise ValueError("积分增值包必须配置积分数量")
|
|
if self.validity_months is None:
|
|
self.validity_months = 1
|
|
if any(
|
|
value is not None
|
|
for value in (
|
|
self.tier_code,
|
|
self.tier_rank,
|
|
self.billing_cycle,
|
|
self.monthly_grant_credits,
|
|
self.first_purchase_price,
|
|
self.regular_price,
|
|
self.activity_price,
|
|
self.activity_start_at,
|
|
self.activity_end_at,
|
|
)
|
|
):
|
|
raise ValueError("积分增值包不能配置订阅套餐字段")
|
|
return self
|
|
|
|
|
|
class CreditProductCreate(CreditProductBase):
|
|
pass
|
|
|
|
|
|
class CreditProductUpdate(BaseModel):
|
|
# product_code / product_type / tier_code / tier_rank / billing_cycle 创建后永久不可修改。
|
|
name: str | None = Field(default=None, min_length=1, max_length=96)
|
|
description: str | None = Field(default=None, max_length=512)
|
|
features: list[str] | None = None
|
|
monthly_grant_credits: float | None = Field(default=None, gt=0, le=999999999.99)
|
|
first_purchase_price: float | None = Field(default=None, ge=0, le=999999999.99)
|
|
regular_price: float | None = Field(default=None, ge=0, le=999999999.99)
|
|
activity_price: float | None = Field(default=None, ge=0, le=999999999.99)
|
|
activity_start_at: datetime | None = None
|
|
activity_end_at: datetime | None = None
|
|
renewal_enabled: bool | None = None
|
|
price: float | None = Field(default=None, ge=0, le=999999999.99)
|
|
grant_credits: float | None = Field(default=None, gt=0, le=999999999.99)
|
|
validity_months: int | None = Field(default=None, ge=1, le=36)
|
|
credit_level: CreditLevelType | None = None
|
|
currency: str | None = Field(default=None, min_length=1, max_length=8)
|
|
sort_order: int | None = Field(default=None, ge=-999999, le=999999)
|
|
|
|
|
|
class CreditProductRenewalUpdate(BaseModel):
|
|
renewal_enabled: bool
|
|
|
|
|
|
class CreditProductStatusUpdate(BaseModel):
|
|
is_active: bool
|
|
|
|
|
|
class CreditProductOut(BaseModel):
|
|
id: str
|
|
product_code: str
|
|
product_type: str
|
|
product_type_label: str = ""
|
|
name: str
|
|
description: str | None = None
|
|
features: list[str] = Field(default_factory=list)
|
|
tier_code: str | None = None
|
|
tier_label: str | None = None
|
|
tier_rank: int | None = None
|
|
billing_cycle: str | None = None
|
|
billing_cycle_label: str | None = None
|
|
monthly_grant_credits: float = 0
|
|
grant_count: int = 1
|
|
first_purchase_price: float = 0
|
|
regular_price: float = 0
|
|
activity_price: float | None = None
|
|
activity_start_at: datetime | None = None
|
|
activity_end_at: datetime | None = None
|
|
renewal_enabled: bool = True
|
|
grant_credits: float = 0
|
|
validity_months: int | None = None
|
|
price: float
|
|
current_price: float
|
|
price_type: str | None = None
|
|
price_type_label: str | None = None
|
|
credit_level: str
|
|
credit_level_label: str = ""
|
|
currency: str
|
|
is_active: bool
|
|
is_deleted: bool = False
|
|
deleted_at: datetime | None = None
|
|
status_label: str = ""
|
|
sort_order: int
|
|
can_purchase: bool | None = None
|
|
unavailable_reason: str | None = None
|
|
|
|
|
|
class CreditProductCatalogOut(BaseModel):
|
|
subscription_products: list[CreditProductOut]
|
|
team_subscription_products: list[CreditProductOut]
|
|
credit_addons: list[CreditProductOut]
|
|
personal_first_purchase_available: bool
|
|
team_first_purchase_available: bool
|
|
active_personal_subscriptions: list[dict] = Field(default_factory=list)
|
|
personal_entitlement: dict | None = None
|
|
team_entitlement: dict | None = None
|
|
team_purchase_available: bool = False
|
|
team_purchase_unavailable_reason: str | None = None
|