feat: add account subscription authorization

This commit is contained in:
QiuSW
2026-07-21 16:38:52 +08:00
parent 64a3dfec1d
commit d9230b9688
11 changed files with 245 additions and 15 deletions
+83
View File
@@ -86,6 +86,18 @@ class AuthorizationDecision:
return not self.allowed
@dataclass(frozen=True)
class SubscriptionAuthorizationDecision:
product_code: str
allowed: bool
code: str
entitlement: SoftwareEntitlement | None = None
@property
def would_reject(self) -> bool:
return not self.allowed
@dataclass(frozen=True)
class DeviceRegistrationResult:
device: ClientDevice
@@ -911,3 +923,74 @@ def evaluate_device_authorization(*, user, product_code: str, device=None, raw_c
if not credential.is_active_at(now) or not credential.entitlement.is_usable_at(now):
return AuthorizationDecision(product_code, False, "license_expired", credential.id)
return AuthorizationDecision(product_code, True, "", credential.id)
def evaluate_account_authorization(*, user, product_code: str, now=None):
"""Authorize a product by account entitlement, without device or credential checks."""
now = now or timezone.now()
usable_entitlement = (
SoftwareEntitlement.objects.filter(
user=user,
product_code=product_code,
status=SoftwareEntitlement.Status.ACTIVE,
starts_at__lte=now,
grace_expires_at__gt=now,
)
.order_by("-expires_at", "-id")
.first()
)
if usable_entitlement is not None:
return SubscriptionAuthorizationDecision(
product_code=product_code,
allowed=True,
code="",
entitlement=usable_entitlement,
)
historical_entitlement = (
SoftwareEntitlement.objects.filter(
user=user,
product_code=product_code,
)
.exclude(status=SoftwareEntitlement.Status.REVOKED)
.order_by("-grace_expires_at", "-id")
.first()
)
if historical_entitlement is not None:
return SubscriptionAuthorizationDecision(
product_code=product_code,
allowed=False,
code="subscription_expired",
entitlement=historical_entitlement,
)
return SubscriptionAuthorizationDecision(
product_code=product_code,
allowed=False,
code="subscription_required",
)
def software_subscription_status(*, user, product_code: str, now=None) -> dict:
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": {
"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,
}