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:
@@ -0,0 +1,87 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest import IsolatedAsyncioTestCase
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from bot.payment_providers.shared import webhooks
|
||||
from bot.payment_providers.shared.success import PaymentSuccessRequest, finalize_successful_payment
|
||||
|
||||
|
||||
class _PaymentWithLazyUser:
|
||||
payment_id = 12
|
||||
user_id = 42
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
raise RuntimeError("lazy relationship access is not allowed here")
|
||||
|
||||
|
||||
class _I18n:
|
||||
def gettext(self, _language, key, **_kwargs):
|
||||
return key
|
||||
|
||||
|
||||
class PaymentWebhookNotificationTests(IsolatedAsyncioTestCase):
|
||||
async def test_failed_payment_notification_loads_user_explicitly(self):
|
||||
user = SimpleNamespace(user_id=42, language_code="ru", email="u@example.test")
|
||||
bot = SimpleNamespace(send_message=AsyncMock())
|
||||
settings = SimpleNamespace(DEFAULT_LANGUAGE="en", SUBSCRIPTION_MINI_APP_URL="")
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
webhooks.user_dal,
|
||||
"get_user_by_id",
|
||||
AsyncMock(return_value=user),
|
||||
) as get_user,
|
||||
patch.object(
|
||||
webhooks,
|
||||
"send_user_notification_email",
|
||||
AsyncMock(),
|
||||
) as send_email,
|
||||
):
|
||||
await webhooks.notify_user_payment_failed(
|
||||
bot=bot,
|
||||
settings=settings,
|
||||
i18n=_I18n(),
|
||||
session=AsyncMock(),
|
||||
payment=_PaymentWithLazyUser(),
|
||||
)
|
||||
|
||||
get_user.assert_awaited_once()
|
||||
bot.send_message.assert_awaited_once_with(42, "payment_failed")
|
||||
send_email.assert_awaited_once()
|
||||
|
||||
async def test_finalize_failure_marks_payment_retryable(self):
|
||||
session = AsyncMock()
|
||||
payment = SimpleNamespace(payment_id=12, user_id=42, status="succeeded")
|
||||
subscription_service = SimpleNamespace(
|
||||
activate_subscription=AsyncMock(side_effect=RuntimeError("panel failed"))
|
||||
)
|
||||
|
||||
with patch(
|
||||
"bot.payment_providers.shared.success.payment_dal.update_payment_status_by_db_id",
|
||||
AsyncMock(return_value=payment),
|
||||
) as update_status:
|
||||
result = await finalize_successful_payment(
|
||||
PaymentSuccessRequest(
|
||||
bot=SimpleNamespace(),
|
||||
settings=SimpleNamespace(DEFAULT_LANGUAGE="en"),
|
||||
i18n=_I18n(),
|
||||
session=session,
|
||||
subscription_service=subscription_service,
|
||||
referral_service=SimpleNamespace(),
|
||||
payment=payment,
|
||||
user_id=42,
|
||||
amount=50,
|
||||
currency="RUB",
|
||||
sale_mode="hwid_devices@standard",
|
||||
months=1,
|
||||
traffic_amount=1,
|
||||
provider_subscription="platega",
|
||||
provider_notification="platega",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertIsNone(result)
|
||||
session.rollback.assert_awaited_once()
|
||||
update_status.assert_awaited_once_with(session, 12, "activation_failed")
|
||||
session.commit.assert_awaited_once()
|
||||
Reference in New Issue
Block a user