fix: bind HWID top-ups to subscription periods

This commit is contained in:
3252a8
2026-05-25 19:33:50 +03:00
parent 59fa301344
commit c68cb97964
49 changed files with 2027 additions and 119 deletions
+219 -4
View File
@@ -9,8 +9,9 @@ recent fix). These tests pin that:
returns a meaningful payload;
* the panel call uses ``hwidDeviceLimit = base + extra``, not just ``base``;
* a panel failure returns ``None`` and DOES NOT write the audit row;
* the happy path persists ``extra_hwid_devices = old + purchased`` and
records the device purchase.
* the happy path records a validity window ending at the subscription end;
* renewal top-ups start after the current HWID entitlement and do not double
the active device limit before that date.
"""
import json
@@ -75,6 +76,8 @@ def _make_sub(*, hwid_device_limit=3, extra_hwid_devices=0):
panel_subscription_uuid="panel-sub",
tariff_key="standard",
end_date=datetime(2099, 1, 1, tzinfo=timezone.utc),
start_date=datetime(2098, 12, 1, tzinfo=timezone.utc),
duration_months=1,
hwid_device_limit=hwid_device_limit,
extra_hwid_devices=extra_hwid_devices,
)
@@ -135,8 +138,90 @@ class HwidDeviceTopupInputTests(unittest.IsolatedAsyncioTestCase):
)
self.assertIsNone(result)
async def test_rejects_traffic_tariff(self):
with tempfile.TemporaryDirectory() as tmpdir:
settings = _make_settings(
tmpdir,
_tariffs_config_payload(
billing_model="traffic",
traffic_packages={"rub": [{"gb": 100, "price": 100}], "stars": []},
),
)
service = _make_service(settings)
sub = _make_sub()
user = _make_user()
with (
patch(
"bot.services.subscription_service_impl.devices.user_dal.get_user_by_id",
AsyncMock(return_value=user),
),
patch(
"bot.services.subscription_service_impl.devices.subscription_dal.get_active_subscription_by_user_id",
AsyncMock(return_value=sub),
),
):
result = await service.activate_hwid_device_topup(
session=AsyncMock(),
user_id=42,
device_count=1,
payment_amount=50,
payment_db_id=1,
)
self.assertIsNone(result)
class HwidDeviceTopupBehaviourTests(unittest.IsolatedAsyncioTestCase):
async def test_quote_prorates_period_price_for_remaining_subscription_window(self):
with tempfile.TemporaryDirectory() as tmpdir:
settings = _make_settings(
tmpdir,
_tariffs_config_payload(
hwid_device_packages={
"rub": [
{
"count": 1,
"price": 100,
"prices": {"1": 100},
"min_price": 10,
}
],
"stars": [],
}
),
)
service = _make_service(settings)
sub = _make_sub()
sub.start_date = datetime(2099, 1, 1, tzinfo=timezone.utc)
sub.end_date = datetime(2099, 1, 31, tzinfo=timezone.utc)
user = _make_user()
with (
patch(
"bot.services.subscription_service_impl.devices.user_dal.get_user_by_id",
AsyncMock(return_value=user),
),
patch(
"bot.services.subscription_service_impl.devices.subscription_dal.get_active_subscription_by_user_id",
AsyncMock(return_value=sub),
),
patch(
"bot.services.subscription_service_impl.devices.tariff_dal.get_hwid_device_entitlement_summary",
AsyncMock(return_value={"active_devices": 0, "active_until": None}),
),
):
quote = await service.quote_hwid_device_topup(
session=AsyncMock(),
user_id=42,
device_count=1,
tariff_key="standard",
currency="rub",
now=datetime(2099, 1, 16, tzinfo=timezone.utc),
)
self.assertIsNotNone(quote)
self.assertEqual(quote["price"], 50)
self.assertAlmostEqual(quote["proration_ratio"], 0.5)
async def test_unlimited_subscriber_returns_noop_payload(self):
# hwid_device_limit == 0 means unlimited — top-up makes no sense and must skip.
with tempfile.TemporaryDirectory() as tmpdir:
@@ -170,7 +255,7 @@ class HwidDeviceTopupBehaviourTests(unittest.IsolatedAsyncioTestCase):
# Unlimited subscriber: no audit row, no panel touch.
create_purchase.assert_not_awaited()
async def test_panel_payload_uses_effective_limit_with_extras(self):
async def test_panel_payload_uses_effective_limit_with_active_entitlements(self):
with tempfile.TemporaryDirectory() as tmpdir:
settings = _make_settings(tmpdir, _tariffs_config_payload())
service = _make_service(settings)
@@ -196,6 +281,10 @@ class HwidDeviceTopupBehaviourTests(unittest.IsolatedAsyncioTestCase):
"bot.services.subscription_service_impl.devices.subscription_dal.update_subscription",
AsyncMock(return_value=updated_sub),
),
patch(
"bot.services.subscription_service_impl.devices.tariff_dal.get_hwid_device_entitlement_summary",
AsyncMock(return_value={"active_devices": 2, "active_until": sub.end_date}),
),
patch(
"bot.services.subscription_service_impl.payments.payment_dal.get_payment_by_db_id",
AsyncMock(return_value=SimpleNamespace()),
@@ -203,7 +292,7 @@ class HwidDeviceTopupBehaviourTests(unittest.IsolatedAsyncioTestCase):
patch(
"bot.services.subscription_service_impl.devices.tariff_dal.create_hwid_device_purchase",
AsyncMock(),
),
) as create_purchase,
):
result = await service.activate_hwid_device_topup(
session=AsyncMock(),
@@ -217,10 +306,132 @@ class HwidDeviceTopupBehaviourTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(result["hwid_device_limit"], 6)
self.assertEqual(result["extra_hwid_devices"], 3)
self.assertEqual(result["purchased_hwid_devices"], 1)
create_purchase.assert_awaited_once()
purchase_kwargs = create_purchase.await_args.kwargs
self.assertEqual(purchase_kwargs["valid_until"], sub.end_date)
self.assertLess(purchase_kwargs["valid_from"], sub.end_date)
# Panel must see the full effective limit, not just the base.
panel_payload = service.panel_service.update_user_details_on_panel.await_args.args[1]
self.assertEqual(panel_payload["hwidDeviceLimit"], 6)
async def test_activation_uses_frozen_payment_validity_window(self):
with tempfile.TemporaryDirectory() as tmpdir:
settings = _make_settings(tmpdir, _tariffs_config_payload())
service = _make_service(settings)
sub = _make_sub(hwid_device_limit=3, extra_hwid_devices=0)
sub.end_date = datetime(2099, 3, 1, tzinfo=timezone.utc)
user = _make_user()
frozen_until = datetime(2099, 2, 1, tzinfo=timezone.utc)
payment = SimpleNamespace(
hwid_valid_from=datetime(2099, 1, 1, tzinfo=timezone.utc),
hwid_valid_until=frozen_until,
hwid_pricing_period_months=1,
hwid_proration_ratio=1.0,
hwid_full_price=50,
)
updated_sub = SimpleNamespace(subscription_id=11, end_date=sub.end_date)
service.panel_service.update_user_details_on_panel = AsyncMock(
return_value={"ok": True}
)
with (
patch(
"bot.services.subscription_service_impl.devices.user_dal.get_user_by_id",
AsyncMock(return_value=user),
),
patch(
"bot.services.subscription_service_impl.devices.subscription_dal.get_active_subscription_by_user_id",
AsyncMock(return_value=sub),
),
patch(
"bot.services.subscription_service_impl.devices.subscription_dal.update_subscription",
AsyncMock(return_value=updated_sub),
),
patch(
"bot.services.subscription_service_impl.devices.tariff_dal.get_hwid_device_entitlement_summary",
AsyncMock(return_value={"active_devices": 0, "active_until": None}),
),
patch(
"bot.services.subscription_service_impl.payments.payment_dal.get_payment_by_db_id",
AsyncMock(return_value=payment),
),
patch(
"bot.services.subscription_service_impl.devices.tariff_dal.create_hwid_device_purchase",
AsyncMock(),
) as create_purchase,
):
result = await service.activate_hwid_device_topup(
session=AsyncMock(),
user_id=42,
device_count=1,
payment_amount=50,
payment_db_id=1,
)
self.assertEqual(result["hwid_devices_valid_until"], frozen_until)
purchase_kwargs = create_purchase.await_args.kwargs
self.assertEqual(purchase_kwargs["valid_until"], frozen_until)
async def test_renewal_topup_starts_after_existing_entitlement(self):
with tempfile.TemporaryDirectory() as tmpdir:
settings = _make_settings(tmpdir, _tariffs_config_payload())
service = _make_service(settings)
current_entitlement_end = datetime(2099, 1, 1, tzinfo=timezone.utc)
renewed_end = datetime(2099, 2, 1, tzinfo=timezone.utc)
sub = _make_sub(hwid_device_limit=3, extra_hwid_devices=2)
sub.end_date = renewed_end
user = _make_user()
updated_sub = SimpleNamespace(subscription_id=11, end_date=renewed_end)
service.panel_service.update_user_details_on_panel = AsyncMock(
return_value={"ok": True}
)
with (
patch(
"bot.services.subscription_service_impl.devices.user_dal.get_user_by_id",
AsyncMock(return_value=user),
),
patch(
"bot.services.subscription_service_impl.devices.subscription_dal.get_active_subscription_by_user_id",
AsyncMock(return_value=sub),
),
patch(
"bot.services.subscription_service_impl.devices.subscription_dal.update_subscription",
AsyncMock(return_value=updated_sub),
),
patch(
"bot.services.subscription_service_impl.devices.tariff_dal.get_hwid_device_entitlement_summary",
AsyncMock(
return_value={
"active_devices": 2,
"active_until": current_entitlement_end,
}
),
),
patch(
"bot.services.subscription_service_impl.payments.payment_dal.get_payment_by_db_id",
AsyncMock(return_value=SimpleNamespace()),
),
patch(
"bot.services.subscription_service_impl.devices.tariff_dal.create_hwid_device_purchase",
AsyncMock(),
) as create_purchase,
):
result = await service.activate_hwid_device_topup(
session=AsyncMock(),
user_id=42,
device_count=1,
payment_amount=50,
payment_db_id=1,
renewal=True,
)
self.assertEqual(result["extra_hwid_devices"], 2)
self.assertEqual(result["hwid_device_limit"], 5)
purchase_kwargs = create_purchase.await_args.kwargs
self.assertEqual(purchase_kwargs["valid_from"], current_entitlement_end)
self.assertEqual(purchase_kwargs["valid_until"], renewed_end)
panel_payload = service.panel_service.update_user_details_on_panel.await_args.args[1]
self.assertEqual(panel_payload["hwidDeviceLimit"], 5)
async def test_panel_failure_returns_none_and_skips_audit(self):
with tempfile.TemporaryDirectory() as tmpdir:
settings = _make_settings(tmpdir, _tariffs_config_payload())
@@ -242,6 +453,10 @@ class HwidDeviceTopupBehaviourTests(unittest.IsolatedAsyncioTestCase):
"bot.services.subscription_service_impl.devices.subscription_dal.update_subscription",
AsyncMock(return_value=updated_sub),
),
patch(
"bot.services.subscription_service_impl.devices.tariff_dal.get_hwid_device_entitlement_summary",
AsyncMock(return_value={"active_devices": 0, "active_until": None}),
),
patch(
"bot.services.subscription_service_impl.payments.payment_dal.get_payment_by_db_id",
AsyncMock(return_value=SimpleNamespace()),