diff --git a/backend/bot/services/subscription_service_impl/devices.py b/backend/bot/services/subscription_service_impl/devices.py index 0d82b52..df30706 100644 --- a/backend/bot/services/subscription_service_impl/devices.py +++ b/backend/bot/services/subscription_service_impl/devices.py @@ -87,9 +87,9 @@ class HwidDeviceMixin: full_price = float(package.price_for_period(period_months)) period_start = self._as_aware_utc(getattr(sub, "start_date", None)) period_end = self._as_aware_utc(getattr(sub, "end_date", None)) or valid_until - if not period_start or period_start >= period_end: - period_start = valid_from - period_end = valid_until + inferred_period_start = add_months(period_end, -period_months) + if not period_start or period_start >= period_end or period_start < inferred_period_start: + period_start = inferred_period_start basis_seconds = max(1.0, (period_end - period_start).total_seconds()) billable_start = max(now, valid_from) diff --git a/tests/test_hwid_device_topup.py b/tests/test_hwid_device_topup.py index c6f9d53..e1f67cf 100644 --- a/tests/test_hwid_device_topup.py +++ b/tests/test_hwid_device_topup.py @@ -222,6 +222,57 @@ class HwidDeviceTopupBehaviourTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(quote["price"], 50) self.assertAlmostEqual(quote["proration_ratio"], 0.5) + async def test_quote_uses_last_paid_duration_when_start_date_is_stale(self): + with tempfile.TemporaryDirectory() as tmpdir: + settings = _make_settings( + tmpdir, + _tariffs_config_payload( + hwid_device_packages={ + "rub": [ + { + "count": 1, + "price": 50, + "prices": {"3": 150}, + } + ], + "stars": [], + } + ), + ) + service = _make_service(settings) + sub = _make_sub() + sub.duration_months = 3 + sub.start_date = datetime(2098, 7, 1, tzinfo=timezone.utc) + sub.end_date = datetime(2099, 1, 1, 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(2098, 10, 1, tzinfo=timezone.utc), + ) + + self.assertIsNotNone(quote) + self.assertEqual(quote["price"], 150) + self.assertAlmostEqual(quote["proration_ratio"], 1.0) + 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: