diff --git a/backend/bot/app/web/webapp/billing.py b/backend/bot/app/web/webapp/billing.py index 1662d5b..2bbf27b 100644 --- a/backend/bot/app/web/webapp/billing.py +++ b/backend/bot/app/web/webapp/billing.py @@ -87,6 +87,7 @@ async def create_payment_route(request: web.Request) -> web.Response: return validation_error method = str(payment_payload.method or "").strip().lower() settings: Settings = request.app["settings"] + subscription_service: SubscriptionService = request.app["subscription_service"] cached = _get_cached_webapp_settings(request) tariffs_config = settings.tariffs_config traffic_mode = bool(settings.traffic_sale_mode) diff --git a/backend/bot/payment_providers/yookassa.py b/backend/bot/payment_providers/yookassa.py index 99793a3..2d8fddd 100644 --- a/backend/bot/payment_providers/yookassa.py +++ b/backend/bot/payment_providers/yookassa.py @@ -144,10 +144,12 @@ class YooKassaService: bot_username_for_default_return: Optional[str] = None, settings_obj: Optional[Settings] = None, config: Optional[YooKassaConfig] = None, + subscription_service: Optional[SubscriptionService] = None, ): self.settings = settings_obj self.config = config or YooKassaConfig() + self.subscription_service = subscription_service self._bot_username_for_default_return = bot_username_for_default_return self._configured_return_url_override = configured_return_url self._sdk_configured_for = ( @@ -2680,6 +2682,7 @@ def create_service(ctx: ServiceFactoryContext) -> YooKassaService: bot_username_for_default_return=ctx.bot_username_for_default_return, settings_obj=ctx.settings, config=config, + subscription_service=ctx.subscription_service, ) diff --git a/tests/test_build_services_wiring.py b/tests/test_build_services_wiring.py index fd9ba8a..80bebeb 100644 --- a/tests/test_build_services_wiring.py +++ b/tests/test_build_services_wiring.py @@ -104,6 +104,7 @@ class BuildServicesWiringTests(unittest.TestCase): self.assertIsInstance(yookassa, YooKassaService) # Identity check: the wired attribute must be the *same* instance. self.assertIs(getattr(subscription, "yookassa_service", None), yookassa) + self.assertIs(getattr(yookassa, "subscription_service", None), subscription) def test_panel_webhook_service_can_reach_subscription_service(self): """The 24h pre-expiry handler in panel_webhook_service.handle_event diff --git a/tests/test_webapp_device_topup_options.py b/tests/test_webapp_device_topup_options.py index 3eba071..6f97fe3 100644 --- a/tests/test_webapp_device_topup_options.py +++ b/tests/test_webapp_device_topup_options.py @@ -94,3 +94,105 @@ class WebAppDeviceTopupOptionsTests(IsolatedAsyncioTestCase): self.assertEqual(payload["extra_hwid_devices_valid_until_text"], "02.01.2099 03:04") self.assertEqual(payload["plans"][0]["valid_from"], valid_from.isoformat()) self.assertEqual(payload["plans"][0]["valid_until"], active_until.isoformat()) + + async def test_create_payment_route_quotes_hwid_with_app_subscription_service(self): + tariff = SimpleNamespace( + key="standard", + billing_model="period", + enabled_periods=[1], + hwid_device_packages=SimpleNamespace( + rub=[SimpleNamespace(count=1)], + stars=[], + ), + ) + settings = SimpleNamespace( + traffic_sale_mode=False, + tariffs_config=SimpleNamespace(require=lambda key: tariff), + DEFAULT_LANGUAGE="en", + DEFAULT_CURRENCY_SYMBOL="RUB", + ADMIN_IDS=[], + ) + quote = { + "price": 25, + "valid_from": datetime(2099, 1, 1, tzinfo=timezone.utc), + "valid_until": datetime(2099, 4, 1, tzinfo=timezone.utc), + "pricing_period_months": 3, + "proration_ratio": 1.0, + "full_price": 25, + } + subscription_service = SimpleNamespace( + quote_hwid_device_topup=AsyncMock(return_value=quote) + ) + request = SimpleNamespace( + app={ + "settings": settings, + "async_session_factory": _SessionFactory(), + "subscription_service": subscription_service, + } + ) + db_user = SimpleNamespace( + is_banned=False, + panel_user_uuid="panel-user", + language_code="en", + telegram_id=42, + ) + sub = SimpleNamespace(tariff_key="standard") + + async def _fake_create_payment(**kwargs): + return billing_module.web.json_response( + { + "ok": True, + "price": kwargs["price"], + "hwid_valid_until": kwargs["hwid_quote"]["valid_until"].isoformat(), + } + ) + + with ( + patch.object(billing_module, "_require_user_id", return_value=42), + patch.object( + billing_module, + "_enforce_webapp_rate_limit", + AsyncMock(return_value=None), + ), + patch.object( + billing_module, + "_read_json", + AsyncMock( + return_value={ + "method": "yookassa", + "months": 1, + "device_count": 1, + "tariff_key": "standard", + "sale_mode": "hwid_devices", + } + ), + ), + patch.object( + billing_module, + "_get_cached_webapp_settings", + return_value={"subscription_options": {}, "stars_subscription_options": {}}, + ), + patch.object( + billing_module.user_dal, + "get_user_by_id", + AsyncMock(return_value=db_user), + ), + patch.object( + billing_module.subscription_dal, + "get_active_subscription_by_user_id", + AsyncMock(return_value=sub), + ), + patch.object( + billing_module, + "_create_subscription_payment", + AsyncMock(side_effect=_fake_create_payment), + ) as create_payment, + ): + response = await billing_module.create_payment_route(request) + + self.assertEqual(response.status, 200) + payload = json.loads(response.text) + self.assertTrue(payload["ok"]) + self.assertEqual(payload["price"], 25) + subscription_service.quote_hwid_device_topup.assert_awaited_once() + create_payment.assert_awaited_once()