feat: add staged subscription rollout mode
This commit is contained in:
+117
-13
@@ -63,7 +63,12 @@ from apps.billing.services import RechargePayment
|
||||
from apps.moderation.models import SensitiveWord
|
||||
from apps.moderation.providers.keyword import reset_keyword_matcher_cache
|
||||
from apps.portal.models import DownloadRelease
|
||||
from apps.licensing.models import ClientDevice, SoftwareOrder, SoftwarePlan
|
||||
from apps.licensing.models import (
|
||||
ClientDevice,
|
||||
SoftwareEntitlement,
|
||||
SoftwareOrder,
|
||||
SoftwarePlan,
|
||||
)
|
||||
from apps.licensing.services import (
|
||||
create_software_order,
|
||||
grant_software_entitlement,
|
||||
@@ -1437,6 +1442,82 @@ class GenerateApiTests(TestCase):
|
||||
starts_at=starts_at,
|
||||
)
|
||||
|
||||
@override_settings(
|
||||
CMSHOPEE_SUBSCRIPTION_MODE="open",
|
||||
PUBLIC_BASE_URL="https://cm.example.test",
|
||||
)
|
||||
def test_cmshopee_open_mode_returns_compatible_access_without_writing_entitlement(self):
|
||||
response = self.client.get(
|
||||
"/api/v1/cmshopee/subscription/status",
|
||||
**self.auth_header(),
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertEqual(data["product_code"], "cmshopee")
|
||||
self.assertEqual(data["status"], "active")
|
||||
self.assertTrue(data["allowed"])
|
||||
self.assertIsNone(data["code"])
|
||||
self.assertEqual(data["account"]["username"], self.user.username)
|
||||
self.assertEqual(data["plan"]["code"], "development-open")
|
||||
self.assertEqual(data["plan"]["display_name"], "开发测试长期会员")
|
||||
self.assertEqual(data["expires_at"], data["plan"]["expires_at"])
|
||||
self.assertIsNone(data["grace_expires_at"])
|
||||
self.assertEqual(data["manage_url"], "https://cm.example.test/subscription")
|
||||
self.assertEqual(data["access_source"], "open_mode")
|
||||
self.assertEqual(data["entitlement_status"], "required")
|
||||
self.assertTrue(data["notice_id"])
|
||||
self.assertFalse(SoftwareEntitlement.objects.filter(user=self.user).exists())
|
||||
|
||||
@override_settings(CMSHOPEE_SUBSCRIPTION_MODE="shadow")
|
||||
def test_cmshopee_shadow_mode_allows_missing_entitlement_and_reports_real_status(self):
|
||||
response = self.client.get(
|
||||
"/api/v1/cmshopee/subscription/status",
|
||||
**self.auth_header(),
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertEqual(data["status"], "active")
|
||||
self.assertTrue(data["allowed"])
|
||||
self.assertEqual(data["access_source"], "shadow_fallback")
|
||||
self.assertEqual(data["entitlement_status"], "required")
|
||||
self.assertEqual(data["plan"]["code"], "shadow-fallback")
|
||||
|
||||
@override_settings(CMSHOPEE_SUBSCRIPTION_MODE="open")
|
||||
def test_cmshopee_open_mode_submit_does_not_create_entitlement(self):
|
||||
response = self.post_with_provider(
|
||||
"/api/v1/cmshopee/generate/title",
|
||||
{"prompt": "开发测试标题", "model": self.title_alias},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(self.provider.text_calls), 1)
|
||||
self.assertFalse(SoftwareEntitlement.objects.filter(user=self.user).exists())
|
||||
|
||||
@override_settings(CMSHOPEE_SUBSCRIPTION_MODE="shadow")
|
||||
def test_cmshopee_shadow_submit_logs_real_entitlement_status(self):
|
||||
with self.assertLogs("cmhub.licensing.authorization", level="INFO") as captured:
|
||||
response = self.post_with_provider(
|
||||
"/api/v1/cmshopee/generate/title",
|
||||
{"prompt": "影子模式标题", "model": self.title_alias},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
events = [
|
||||
getattr(record, "cmshopee_authorization", None)
|
||||
for record in captured.records
|
||||
if getattr(record, "cmshopee_authorization", None)
|
||||
]
|
||||
self.assertEqual(len(events), 1)
|
||||
self.assertEqual(events[0]["subscription_mode"], "shadow")
|
||||
self.assertEqual(events[0]["access_source"], "shadow_fallback")
|
||||
self.assertEqual(events[0]["entitlement_status"], "required")
|
||||
self.assertTrue(events[0]["would_reject"])
|
||||
self.assertEqual(events[0]["would_reject_code"], "subscription_required")
|
||||
self.assertTrue(events[0]["final_allowed"])
|
||||
|
||||
@override_settings(CMSHOPEE_SUBSCRIPTION_MODE="enforce")
|
||||
def test_cmshopee_subscription_status_is_required_without_entitlement(self):
|
||||
response = self.client.get(
|
||||
"/api/v1/cmshopee/subscription/status",
|
||||
@@ -1444,17 +1525,29 @@ class GenerateApiTests(TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(
|
||||
response.json(),
|
||||
{
|
||||
"product_code": "cmshopee",
|
||||
"status": "required",
|
||||
"allowed": False,
|
||||
"code": "subscription_required",
|
||||
"plan": None,
|
||||
},
|
||||
data = response.json()
|
||||
self.assertEqual(data["status"], "required")
|
||||
self.assertFalse(data["allowed"])
|
||||
self.assertEqual(data["code"], "subscription_required")
|
||||
self.assertIsNone(data["plan"])
|
||||
self.assertEqual(data["entitlement_status"], "required")
|
||||
self.assertEqual(data["access_source"], "entitlement")
|
||||
|
||||
@override_settings(
|
||||
CMSHOPEE_SUBSCRIPTION_MODE="",
|
||||
CMSHOPEE_SUBSCRIPTION_ENFORCEMENT=True,
|
||||
)
|
||||
def test_cmshopee_legacy_enforcement_setting_remains_supported(self):
|
||||
response = self.client.get(
|
||||
"/api/v1/cmshopee/subscription/status",
|
||||
**self.auth_header(),
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(response.json()["allowed"])
|
||||
self.assertEqual(response.json()["code"], "subscription_required")
|
||||
|
||||
@override_settings(CMSHOPEE_SUBSCRIPTION_MODE="enforce")
|
||||
def test_cmshopee_subscription_status_is_active_without_device_session(self):
|
||||
entitlement = self.create_cmshopee_entitlement()
|
||||
|
||||
@@ -1468,8 +1561,18 @@ class GenerateApiTests(TestCase):
|
||||
self.assertEqual(data["status"], "active")
|
||||
self.assertTrue(data["allowed"])
|
||||
self.assertIsNone(data["code"])
|
||||
self.assertEqual(data["plan"]["display_name"], entitlement.plan_name)
|
||||
self.assertEqual(data["plan"]["name"], entitlement.plan_name)
|
||||
self.assertEqual(data["plan"]["code"], f"plan-{entitlement.source_plan_id}")
|
||||
self.assertEqual(data["expires_at"], entitlement.expires_at.isoformat())
|
||||
self.assertEqual(
|
||||
data["grace_expires_at"],
|
||||
entitlement.grace_expires_at.isoformat(),
|
||||
)
|
||||
self.assertEqual(data["access_source"], "entitlement")
|
||||
self.assertEqual(data["entitlement_status"], "active")
|
||||
|
||||
@override_settings(CMSHOPEE_SUBSCRIPTION_MODE="enforce")
|
||||
def test_cmshopee_subscription_status_is_expired_after_grace_period(self):
|
||||
self.create_cmshopee_entitlement(
|
||||
starts_at=timezone.now() - timedelta(days=40),
|
||||
@@ -1485,8 +1588,9 @@ class GenerateApiTests(TestCase):
|
||||
self.assertEqual(data["status"], "expired")
|
||||
self.assertFalse(data["allowed"])
|
||||
self.assertEqual(data["code"], "subscription_expired")
|
||||
self.assertEqual(data["entitlement_status"], "expired")
|
||||
|
||||
@override_settings(CMSHOPEE_SUBSCRIPTION_ENFORCEMENT=True)
|
||||
@override_settings(CMSHOPEE_SUBSCRIPTION_MODE="enforce")
|
||||
def test_cmshopee_enforcement_rejects_without_subscription(self):
|
||||
response = self.post_with_provider(
|
||||
"/api/v1/cmshopee/generate/title",
|
||||
@@ -1498,7 +1602,7 @@ class GenerateApiTests(TestCase):
|
||||
self.assertEqual(self.provider.text_calls, [])
|
||||
self.assert_generation_not_charged()
|
||||
|
||||
@override_settings(CMSHOPEE_SUBSCRIPTION_ENFORCEMENT=True)
|
||||
@override_settings(CMSHOPEE_SUBSCRIPTION_MODE="enforce")
|
||||
def test_cmshopee_account_subscription_allows_multiple_device_contexts(self):
|
||||
self.create_cmshopee_entitlement()
|
||||
|
||||
@@ -1516,7 +1620,7 @@ class GenerateApiTests(TestCase):
|
||||
self.assertEqual(second_response.status_code, 200)
|
||||
self.assertEqual(len(self.provider.text_calls), 2)
|
||||
|
||||
@override_settings(CMSHOPEE_SUBSCRIPTION_ENFORCEMENT=True)
|
||||
@override_settings(CMSHOPEE_SUBSCRIPTION_MODE="enforce")
|
||||
def test_generic_generation_remains_available_without_subscription(self):
|
||||
response = self.post_with_provider(
|
||||
"/api/v1/generate/title",
|
||||
|
||||
+15
-11
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
@@ -79,7 +78,7 @@ from apps.licensing.services import (
|
||||
create_migration_request,
|
||||
expire_software_order,
|
||||
query_and_apply_software_payment,
|
||||
evaluate_account_authorization,
|
||||
evaluate_subscription_access,
|
||||
software_subscription_status,
|
||||
record_device_heartbeat,
|
||||
register_device,
|
||||
@@ -391,7 +390,7 @@ class GenerateImageTaskDetailView(ExternalApiView):
|
||||
return Response(task_detail_response(task), status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class CmshopeeShadowAuthorizationMixin:
|
||||
class CmshopeeSubscriptionAuthorizationMixin:
|
||||
product_code = "cmshopee"
|
||||
|
||||
def optional_client_device(self, request):
|
||||
@@ -403,7 +402,7 @@ class CmshopeeShadowAuthorizationMixin:
|
||||
request.headers.get("X-Device-Session", "").strip()
|
||||
)
|
||||
device = None
|
||||
decision = evaluate_account_authorization(
|
||||
decision = evaluate_subscription_access(
|
||||
user=request.user,
|
||||
product_code=self.product_code,
|
||||
)
|
||||
@@ -412,9 +411,14 @@ class CmshopeeShadowAuthorizationMixin:
|
||||
"product_code": self.product_code,
|
||||
"user_id": request.user.id,
|
||||
"client_device_id": getattr(device, "id", None),
|
||||
"would_reject": decision.would_reject,
|
||||
"would_reject_code": decision.code,
|
||||
"enforcement": settings.CMSHOPEE_SUBSCRIPTION_ENFORCEMENT,
|
||||
"would_reject": bool(decision.entitlement_code),
|
||||
"would_reject_code": decision.entitlement_code,
|
||||
"final_allowed": decision.allowed,
|
||||
"final_code": decision.code,
|
||||
"subscription_mode": decision.mode,
|
||||
"access_source": decision.access_source,
|
||||
"entitlement_status": decision.entitlement_status,
|
||||
"enforcement": decision.mode == "enforce",
|
||||
}
|
||||
authorization_logger.info(
|
||||
"%s %s",
|
||||
@@ -422,7 +426,7 @@ class CmshopeeShadowAuthorizationMixin:
|
||||
event,
|
||||
extra={"cmshopee_authorization": event},
|
||||
)
|
||||
if settings.CMSHOPEE_SUBSCRIPTION_ENFORCEMENT and decision.would_reject:
|
||||
if decision.would_reject:
|
||||
message = (
|
||||
"当前账号没有有效的软件订阅"
|
||||
if decision.code == "subscription_required"
|
||||
@@ -433,16 +437,16 @@ class CmshopeeShadowAuthorizationMixin:
|
||||
return device
|
||||
|
||||
|
||||
class CmshopeeGenerateTitleView(CmshopeeShadowAuthorizationMixin, GenerateTitleView):
|
||||
class CmshopeeGenerateTitleView(CmshopeeSubscriptionAuthorizationMixin, GenerateTitleView):
|
||||
pass
|
||||
|
||||
|
||||
class CmshopeeAnalyzeImagesView(CmshopeeShadowAuthorizationMixin, AnalyzeImagesView):
|
||||
class CmshopeeAnalyzeImagesView(CmshopeeSubscriptionAuthorizationMixin, AnalyzeImagesView):
|
||||
pass
|
||||
|
||||
|
||||
class CmshopeeGenerateImageTaskSubmitView(
|
||||
CmshopeeShadowAuthorizationMixin,
|
||||
CmshopeeSubscriptionAuthorizationMixin,
|
||||
GenerateImageTaskSubmitView,
|
||||
):
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user