feat: grant transition plan to existing users
This commit is contained in:
@@ -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="",
|
||||
|
||||
Reference in New Issue
Block a user