fix: separate HWID device renewal flows

Keep one-off device top-ups scoped to the active subscription term and move device renewal into subscription checkout.

Carry HWID renewal metadata through provider callbacks and webhooks, including YooKassa saved-card flows.

Add admin extension controls, docs, demo data, and regression coverage.
This commit is contained in:
3252a8
2026-06-03 23:51:46 +03:00
parent a06884d816
commit fbb89793cb
48 changed files with 2410 additions and 224 deletions
+55 -1
View File
@@ -20,9 +20,10 @@ return.
"""
import unittest
from datetime import datetime, timezone
from types import SimpleNamespace
from typing import Any, Dict, List, Optional
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
from bot.services.subscription_service_impl.renewal import RenewalMixin
@@ -276,6 +277,59 @@ class ChargeRenewalHappyPathTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(yk.calls[0]["metadata"]["subscription_months"], "1")
self.assertEqual(yk.calls[0]["amount"], 99.0)
async def test_includes_hwid_device_renewal_in_saved_method_charge(self):
yk = _FakeYooKassaService(response={"id": "auto-pay-8", "status": "pending"})
mixin = _make_mixin(yk=yk, price_for_months=399.0)
valid_from = datetime(2099, 2, 1, tzinfo=timezone.utc)
valid_until = datetime(2099, 3, 1, tzinfo=timezone.utc)
mixin.quote_hwid_device_renewal_for_subscription = AsyncMock(
return_value={
"device_count": 2,
"price": 50.0,
"full_price": 50.0,
"valid_from": valid_from,
"valid_until": valid_until,
"pricing_period_months": 1,
"proration_ratio": 1.0,
}
)
with patch(
"db.dal.user_billing_dal.get_user_default_payment_method",
_stub_default_pm,
):
ok = await mixin.charge_subscription_renewal(
session=None,
sub=_FakeSub(
auto_renew_enabled=True,
provider="yookassa",
user_id=77,
subscription_id=555,
tariff_key="standard",
duration_months=1,
),
)
self.assertTrue(ok)
self.assertEqual(len(yk.calls), 1)
call = yk.calls[0]
self.assertEqual(call["amount"], 449.0)
meta = call["metadata"]
self.assertEqual(meta["sale_mode"], "subscription@standard")
self.assertEqual(meta["hwid_devices"], "2")
self.assertEqual(meta["hwid_valid_from"], valid_from.isoformat())
self.assertEqual(meta["hwid_valid_until"], valid_until.isoformat())
self.assertEqual(meta["hwid_pricing_period_months"], "1")
self.assertEqual(meta["hwid_proration_ratio"], "1.0")
self.assertEqual(meta["hwid_full_price"], "50.0")
mixin.quote_hwid_device_renewal_for_subscription.assert_awaited_once_with(
None,
user_id=77,
target_tariff_key="standard",
months=1,
currency="rub",
)
if __name__ == "__main__": # pragma: no cover
unittest.main()