feat: grant transition plan to existing users

This commit is contained in:
QiuSW
2026-07-22 16:56:13 +08:00
parent 09d13ff64a
commit 82c338cbe7
10 changed files with 374 additions and 4 deletions
+169
View File
@@ -1,10 +1,13 @@
from datetime import timedelta
from decimal import Decimal
from concurrent.futures import ThreadPoolExecutor
from io import StringIO
from types import SimpleNamespace
from unittest.mock import patch
from django.contrib import admin
from django.core.management import call_command
from django.core.management.base import CommandError
from django.db import close_old_connections
from django.test import SimpleTestCase, TestCase, TransactionTestCase, override_settings
from django.urls import reverse
@@ -40,6 +43,7 @@ from apps.licensing.services import (
evaluate_device_authorization,
evaluate_subscription_access,
get_subscription_mode,
grant_plan_to_existing_users,
grant_software_entitlement,
record_device_heartbeat,
release_license_seat,
@@ -51,6 +55,171 @@ from apps.licensing.services import (
from apps.users.models import ApiKey, User, UserWallet
class ExistingUserPlanGrantCommandTests(TestCase):
def setUp(self):
self.plan = SoftwarePlan.objects.create(
product_code=ClientDevice.ProductCode.CMSHOPEE,
name="测试",
duration_days=30,
price=Decimal("99.00"),
device_limit=1,
grace_days=2,
)
self.user_one = User.objects.create_user(
username="existing-one",
email="existing-one@example.com",
password="password",
)
self.user_two = User.objects.create_user(
username="existing-two",
email="existing-two@example.com",
password="password",
)
self.staff_user = User.objects.create_user(
username="staff-user",
email="staff-user@example.com",
password="password",
is_staff=True,
)
self.inactive_user = User.objects.create_user(
username="inactive-user",
email="inactive-user@example.com",
password="password",
is_active=False,
)
def command_options(self, **overrides):
options = {
"plan_id": self.plan.pk,
"reason": "T-633 存量用户测试套餐过渡",
}
options.update(overrides)
return options
def test_dry_run_is_read_only_and_reports_scope(self):
stdout = StringIO()
call_command("grant_existing_users_plan", stdout=stdout, **self.command_options())
self.assertIn("mode=DRY-RUN", stdout.getvalue())
self.assertIn("eligible=2", stdout.getvalue())
self.assertIn("skipped_existing=0", stdout.getvalue())
self.assertIn("grant_count=2", stdout.getvalue())
self.assertFalse(SoftwareEntitlement.objects.exists())
self.assertFalse(LicenseSeat.objects.exists())
self.assertFalse(LicenseEvent.objects.exists())
def test_execute_grants_only_active_nonstaff_users_with_audit_events(self):
before = timezone.now()
call_command(
"grant_existing_users_plan",
execute=True,
expected_grant_count=2,
**self.command_options(),
)
entitlements = SoftwareEntitlement.objects.order_by("user_id")
self.assertEqual(entitlements.count(), 2)
self.assertSetEqual(
set(entitlements.values_list("user_id", flat=True)),
{self.user_one.pk, self.user_two.pk},
)
entitlement = entitlements.first()
self.assertEqual(entitlement.source_plan, self.plan)
self.assertEqual(entitlement.plan_name, "测试")
self.assertEqual(entitlement.plan_duration_days, 30)
self.assertEqual(entitlement.plan_grace_days, 2)
self.assertGreaterEqual(entitlement.starts_at, before)
self.assertEqual(
entitlement.expires_at,
entitlement.starts_at + timedelta(days=30),
)
self.assertEqual(
entitlement.grace_expires_at,
entitlement.expires_at + timedelta(days=2),
)
self.assertEqual(LicenseSeat.objects.count(), 2)
self.assertEqual(
LicenseEvent.objects.filter(
action=LicenseEvent.Action.GRANTED,
reason="T-633 存量用户测试套餐过渡",
).count(),
2,
)
def test_existing_usable_product_entitlement_is_skipped_and_rerun_is_idempotent(self):
formal_plan = SoftwarePlan.objects.create(
product_code=ClientDevice.ProductCode.CMSHOPEE,
name="正式会员",
duration_days=365,
price=Decimal("999.00"),
device_limit=1,
grace_days=7,
)
grant_software_entitlement(
user=self.user_one,
plan=formal_plan,
reason="已有正式权益",
)
call_command(
"grant_existing_users_plan",
execute=True,
expected_grant_count=1,
**self.command_options(),
)
result = grant_plan_to_existing_users(
plan=self.plan,
reason="重复预演",
)
self.assertEqual(SoftwareEntitlement.objects.count(), 2)
self.assertEqual(result.eligible_count, 2)
self.assertEqual(result.skipped_existing_count, 2)
self.assertEqual(result.grant_count, 0)
self.assertFalse(result.executed)
def test_execute_requires_matching_preview_count(self):
with self.assertRaisesRegex(CommandError, "当前实际应授予 2 人"):
call_command(
"grant_existing_users_plan",
execute=True,
expected_grant_count=1,
**self.command_options(),
)
self.assertFalse(SoftwareEntitlement.objects.exists())
self.assertFalse(LicenseEvent.objects.exists())
def test_failure_rolls_back_entire_batch(self):
original_grant = grant_software_entitlement
call_count = 0
def fail_second_grant(**kwargs):
nonlocal call_count
call_count += 1
if call_count == 2:
raise RuntimeError("simulated grant failure")
return original_grant(**kwargs)
with patch(
"apps.licensing.services.grant_software_entitlement",
side_effect=fail_second_grant,
):
with self.assertRaisesRegex(RuntimeError, "simulated grant failure"):
call_command(
"grant_existing_users_plan",
execute=True,
expected_grant_count=2,
**self.command_options(),
)
self.assertFalse(SoftwareEntitlement.objects.exists())
self.assertFalse(LicenseSeat.objects.exists())
self.assertFalse(LicenseEvent.objects.exists())
class SubscriptionModeUnitTests(SimpleTestCase):
@override_settings(
CMSHOPEE_SUBSCRIPTION_MODE="",