feat: add staged subscription rollout mode
This commit is contained in:
+139
-17
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta, timezone as datetime_timezone
|
||||
from decimal import Decimal
|
||||
from decimal import InvalidOperation
|
||||
|
||||
@@ -98,6 +98,22 @@ class SubscriptionAuthorizationDecision:
|
||||
return not self.allowed
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SubscriptionAccessDecision:
|
||||
product_code: str
|
||||
mode: str
|
||||
allowed: bool
|
||||
code: str
|
||||
access_source: str
|
||||
entitlement_status: str
|
||||
entitlement_code: str
|
||||
entitlement: SoftwareEntitlement | None = None
|
||||
|
||||
@property
|
||||
def would_reject(self) -> bool:
|
||||
return not self.allowed
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DeviceRegistrationResult:
|
||||
device: ClientDevice
|
||||
@@ -970,27 +986,133 @@ def evaluate_account_authorization(*, user, product_code: str, now=None):
|
||||
)
|
||||
|
||||
|
||||
def software_subscription_status(*, user, product_code: str, now=None) -> dict:
|
||||
decision = evaluate_account_authorization(
|
||||
def _real_entitlement_status(decision, now) -> str:
|
||||
if decision.allowed:
|
||||
if decision.entitlement and decision.entitlement.expires_at <= now:
|
||||
return "grace"
|
||||
return "active"
|
||||
if decision.code == "subscription_expired":
|
||||
return "expired"
|
||||
return "required"
|
||||
|
||||
|
||||
def get_subscription_mode() -> str:
|
||||
mode = str(getattr(settings, "CMSHOPEE_SUBSCRIPTION_MODE", "") or "").strip().lower()
|
||||
if mode in {"open", "shadow", "enforce"}:
|
||||
return mode
|
||||
return (
|
||||
"enforce"
|
||||
if getattr(settings, "CMSHOPEE_SUBSCRIPTION_ENFORCEMENT", False)
|
||||
else "open"
|
||||
)
|
||||
|
||||
|
||||
def evaluate_subscription_access(*, user, product_code: str, now=None, mode=None):
|
||||
now = now or timezone.now()
|
||||
mode = mode or get_subscription_mode()
|
||||
real_decision = evaluate_account_authorization(
|
||||
user=user,
|
||||
product_code=product_code,
|
||||
now=now,
|
||||
)
|
||||
entitlement = decision.entitlement
|
||||
return {
|
||||
"product_code": product_code,
|
||||
"status": (
|
||||
"active"
|
||||
if decision.allowed
|
||||
else ("expired" if decision.code == "subscription_expired" else "required")
|
||||
),
|
||||
"allowed": decision.allowed,
|
||||
"code": decision.code or None,
|
||||
"plan": {
|
||||
entitlement_status = _real_entitlement_status(real_decision, now)
|
||||
|
||||
if mode == "open":
|
||||
return SubscriptionAccessDecision(
|
||||
product_code=product_code,
|
||||
mode=mode,
|
||||
allowed=True,
|
||||
code="",
|
||||
access_source="open_mode",
|
||||
entitlement_status=entitlement_status,
|
||||
entitlement_code=real_decision.code,
|
||||
)
|
||||
if mode == "shadow" and not real_decision.allowed:
|
||||
return SubscriptionAccessDecision(
|
||||
product_code=product_code,
|
||||
mode=mode,
|
||||
allowed=True,
|
||||
code="",
|
||||
access_source="shadow_fallback",
|
||||
entitlement_status=entitlement_status,
|
||||
entitlement_code=real_decision.code,
|
||||
)
|
||||
return SubscriptionAccessDecision(
|
||||
product_code=product_code,
|
||||
mode=mode,
|
||||
allowed=real_decision.allowed,
|
||||
code=real_decision.code,
|
||||
access_source="entitlement",
|
||||
entitlement_status=entitlement_status,
|
||||
entitlement_code=real_decision.code,
|
||||
entitlement=real_decision.entitlement,
|
||||
)
|
||||
|
||||
|
||||
def _subscription_manage_url() -> str:
|
||||
base_url = str(getattr(settings, "PUBLIC_BASE_URL", "") or "").rstrip("/")
|
||||
return f"{base_url}/subscription" if base_url else "/subscription"
|
||||
|
||||
|
||||
def _effective_plan(access):
|
||||
entitlement = access.entitlement
|
||||
if entitlement is not None:
|
||||
plan_code = (
|
||||
f"plan-{entitlement.source_plan_id}"
|
||||
if entitlement.source_plan_id
|
||||
else f"entitlement-{entitlement.id}"
|
||||
)
|
||||
return {
|
||||
"code": plan_code,
|
||||
"display_name": entitlement.plan_name,
|
||||
# T-630 compatibility fields; new clients use the top-level dates.
|
||||
"name": entitlement.plan_name,
|
||||
"expires_at": entitlement.expires_at.isoformat(),
|
||||
"grace_expires_at": entitlement.grace_expires_at.isoformat(),
|
||||
}
|
||||
if entitlement is not None
|
||||
else None,
|
||||
}, entitlement.expires_at, entitlement.grace_expires_at
|
||||
if access.access_source in {"open_mode", "shadow_fallback"}:
|
||||
expires_at = datetime(2099, 12, 31, 23, 59, 59, tzinfo=datetime_timezone.utc)
|
||||
plan_code = "development-open" if access.access_source == "open_mode" else "shadow-fallback"
|
||||
display_name = "开发测试长期会员" if access.access_source == "open_mode" else "订阅过渡会员"
|
||||
return {
|
||||
"code": plan_code,
|
||||
"display_name": display_name,
|
||||
"name": display_name,
|
||||
"expires_at": expires_at.isoformat(),
|
||||
"grace_expires_at": None,
|
||||
}, expires_at, None
|
||||
return None, None, None
|
||||
|
||||
|
||||
def software_subscription_status(*, user, product_code: str, now=None, mode=None) -> dict:
|
||||
access = evaluate_subscription_access(
|
||||
user=user,
|
||||
product_code=product_code,
|
||||
now=now,
|
||||
mode=mode,
|
||||
)
|
||||
plan, expires_at, grace_expires_at = _effective_plan(access)
|
||||
username = user.get_username()
|
||||
return {
|
||||
"product_code": product_code,
|
||||
"status": (
|
||||
access.entitlement_status
|
||||
if access.access_source == "entitlement"
|
||||
else "active"
|
||||
),
|
||||
"allowed": access.allowed,
|
||||
"code": access.code or None,
|
||||
"account": {
|
||||
"display_name": user.get_full_name() or username,
|
||||
"username": username,
|
||||
},
|
||||
"plan": plan,
|
||||
"expires_at": expires_at.isoformat() if expires_at else None,
|
||||
"grace_expires_at": grace_expires_at.isoformat() if grace_expires_at else None,
|
||||
"manage_url": _subscription_manage_url(),
|
||||
"notice_id": (
|
||||
f"{product_code}-subscription-{access.mode}-{access.entitlement_status}-v1"
|
||||
),
|
||||
"access_source": access.access_source,
|
||||
"entitlement_status": access.entitlement_status,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user