From d39e7812035329f9771d5f579b1500d0a675ee6e Mon Sep 17 00:00:00 2001 From: kavore <161734431+kavore@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:09:04 +0300 Subject: [PATCH] feat(payments): implement server-side price resolution for subscription payments Add a new function `resolve_fiat_offer_price_for_user` to determine the correct offer price for users based on their subscription choices and any applicable discounts. Update payment handlers for CryptoPay, FreeKassa, Platega, SeverPay, and YooKassa to utilize this function, ensuring callback price validation and improving error handling for price mismatches. This enhances security by preventing callback payload tampering and ensures accurate pricing for users. --- bot/handlers/user/payment.py | 81 ++++++++- .../user/subscription/payments_crypto.py | 41 ++++- .../user/subscription/payments_freekassa.py | 43 ++++- .../user/subscription/payments_platega.py | 41 ++++- .../user/subscription/payments_severpay.py | 41 ++++- .../subscription/payments_subscription.py | 30 ++++ .../user/subscription/payments_yookassa.py | 160 +++++++++++++++++- 7 files changed, 417 insertions(+), 20 deletions(-) diff --git a/bot/handlers/user/payment.py b/bot/handlers/user/payment.py index 53ebdd7..ef25452 100644 --- a/bot/handlers/user/payment.py +++ b/bot/handlers/user/payment.py @@ -627,6 +627,51 @@ async def yookassa_webhook_route(request: web.Request): async with async_session_factory() as session: try: + # Defense-in-depth: for state mutations beyond succeeded, verify against provider API. + if notification_object.event in { + YOOKASSA_EVENT_PAYMENT_CANCELED, + YOOKASSA_EVENT_PAYMENT_WAITING_FOR_CAPTURE, + }: + if not yookassa_service or not yookassa_service.configured: + logging.critical( + "YooKassa webhook rejected: verification service is not configured for event %s (payment_id=%s)", + notification_object.event, + payment_dict_for_processing.get("id"), + ) + return web.Response(status=503, text="yookassa_verification_required") + + provider_payment_info = await yookassa_service.get_payment_info( + payment_dict_for_processing.get("id") + ) + if not provider_payment_info: + logging.error( + "YooKassa webhook verification failed: payment %s not found via provider API", + payment_dict_for_processing.get("id"), + ) + return web.Response(status=503, text="yookassa_verification_failed") + + provider_status = str(provider_payment_info.get("status") or "") + provider_paid = bool(provider_payment_info.get("paid")) + provider_metadata_raw = provider_payment_info.get("metadata") or {} + provider_metadata = provider_metadata_raw if isinstance(provider_metadata_raw, dict) else {} + + payment_dict_for_processing["status"] = provider_status or payment_dict_for_processing.get("status") + payment_dict_for_processing["paid"] = provider_paid + payment_dict_for_processing["metadata"] = dict(provider_metadata) + + provider_amount_value = provider_payment_info.get("amount_value") + provider_amount_currency = provider_payment_info.get("amount_currency") + if provider_amount_value is not None and provider_amount_currency: + payment_dict_for_processing["amount"] = { + "value": str(provider_amount_value), + "currency": str(provider_amount_currency), + } + + provider_pm = provider_payment_info.get("payment_method") + if isinstance(provider_pm, dict) and provider_pm.get("id"): + # Use provider payment_method as authoritative. + payment_dict_for_processing["payment_method"] = provider_pm + if notification_object.event == YOOKASSA_EVENT_PAYMENT_SUCCEEDED: if not yookassa_service or not yookassa_service.configured: logging.critical( @@ -669,6 +714,13 @@ async def yookassa_webhook_route(request: web.Request): await session.rollback() return web.Response(status=503, text="yookassa_invalid_succeeded_payload") elif notification_object.event == YOOKASSA_EVENT_PAYMENT_CANCELED: + if payment_dict_for_processing.get("status") not in {"canceled", "cancelled"}: + logging.error( + "YooKassa webhook rejected: canceled event status mismatch for payment %s (status=%s)", + payment_dict_for_processing.get("id"), + payment_dict_for_processing.get("status"), + ) + return web.Response(status=503, text="yookassa_invalid_canceled_payload") await process_cancelled_payment( session, bot, payment_dict_for_processing, i18n_instance, settings) @@ -677,6 +729,13 @@ async def yookassa_webhook_route(request: web.Request): # Bind-only flow: save method and cancel auth if metadata has bind_only metadata = payment_dict_for_processing.get("metadata", {}) or {} if settings.yookassa_autopayments_active and metadata.get("bind_only") == "1": + if payment_dict_for_processing.get("status") != "waiting_for_capture": + logging.error( + "YooKassa webhook rejected: waiting_for_capture event status mismatch for payment %s (status=%s)", + payment_dict_for_processing.get("id"), + payment_dict_for_processing.get("status"), + ) + return web.Response(status=503, text="yookassa_invalid_waiting_payload") try: user_id_str = metadata.get("user_id") if user_id_str and user_id_str.isdigit(): @@ -685,23 +744,27 @@ async def yookassa_webhook_route(request: web.Request): if isinstance(payment_method, dict) and payment_method.get("id"): pm_type = payment_method.get("type") title = payment_method.get("title") + + # Support both webhook shape (nested card) and provider shape (card_last4) + last4_val = None card = payment_method.get("card") or {} - account_number = payment_method.get("account_number") or payment_method.get("account") + if isinstance(card, dict) and card.get("last4"): + last4_val = card.get("last4") + if not last4_val: + last4_val = payment_method.get("card_last4") + display_network = None display_last4 = None if (pm_type or "").lower() in {"bank_card", "bank-card", "card"}: - display_network = card.get("card_type") or title or "Card" - display_last4 = card.get("last4") + display_network = title or "Card" + display_last4 = last4_val elif (pm_type or "").lower() in {"yoo_money", "yoomoney", "yoo-money", "wallet"}: - # Normalize wallet display name to avoid leaking full account from title display_network = "YooMoney" - if isinstance(account_number, str) and len(account_number) >= 4: - display_last4 = account_number[-4:] - else: - display_last4 = None + display_last4 = last4_val else: display_network = title or (pm_type.upper() if pm_type else "Payment method") - display_last4 = None + display_last4 = last4_val + await user_billing_dal.upsert_yk_payment_method( session, user_id=user_id, diff --git a/bot/handlers/user/subscription/payments_crypto.py b/bot/handlers/user/subscription/payments_crypto.py index 450ff61..1a29d53 100644 --- a/bot/handlers/user/subscription/payments_crypto.py +++ b/bot/handlers/user/subscription/payments_crypto.py @@ -12,6 +12,8 @@ from config.settings import Settings router = Router(name="user_subscription_payments_crypto_router") +from bot.handlers.user.subscription.payments_subscription import resolve_fiat_offer_price_for_user + @router.callback_query(F.data.startswith("pay_crypto:")) async def pay_crypto_callback_handler( callback: types.CallbackQuery, @@ -43,7 +45,7 @@ async def pay_crypto_callback_handler( _, data_payload = callback.data.split(":", 1) parts = data_payload.split(":") months = float(parts[0]) - price_amount = float(parts[1]) + callback_price_amount = float(parts[1]) sale_mode = parts[2] if len(parts) > 2 else "subscription" except (ValueError, IndexError): try: @@ -53,6 +55,43 @@ async def pay_crypto_callback_handler( return user_id = callback.from_user.id + resolved_price_amount = await resolve_fiat_offer_price_for_user( + session=session, + settings=settings, + user_id=user_id, + months=months, + sale_mode=sale_mode, + promo_code_service=promo_code_service, + ) + if resolved_price_amount is None: + logging.warning( + "CryptoPay: no server-side price for user %s, value=%s, mode=%s", + user_id, + months, + sale_mode, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_crypto.py: %s", exc) + return + + if abs(resolved_price_amount - callback_price_amount) > 0.01: + logging.warning( + "CryptoPay: callback price mismatch for user %s, value=%s, mode=%s, callback=%.2f, resolved=%.2f", + user_id, + months, + sale_mode, + callback_price_amount, + resolved_price_amount, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_crypto.py: %s", exc) + return + + price_amount = resolved_price_amount human_value = str(int(months)) if float(months).is_integer() else f"{months:g}" payment_description = ( get_text("payment_description_traffic", traffic_gb=human_value) diff --git a/bot/handlers/user/subscription/payments_freekassa.py b/bot/handlers/user/subscription/payments_freekassa.py index 40994dd..2204cc4 100644 --- a/bot/handlers/user/subscription/payments_freekassa.py +++ b/bot/handlers/user/subscription/payments_freekassa.py @@ -14,6 +14,8 @@ from db.dal import payment_dal router = Router(name="user_subscription_payments_freekassa_router") +from bot.handlers.user.subscription.payments_subscription import resolve_fiat_offer_price_for_user + @router.callback_query(F.data.startswith("pay_fk:")) async def pay_fk_callback_handler( callback: types.CallbackQuery, @@ -50,7 +52,7 @@ async def pay_fk_callback_handler( _, data_payload = callback.data.split(":", 1) parts = data_payload.split(":") months = float(parts[0]) - price_rub = float(parts[1]) + callback_price_rub = float(parts[1]) sale_mode = parts[2] if len(parts) > 2 else "subscription" except (ValueError, IndexError): logging.error(f"Invalid pay_fk data in callback: {callback.data}") @@ -61,6 +63,43 @@ async def pay_fk_callback_handler( return user_id = callback.from_user.id + resolved_price_rub = await resolve_fiat_offer_price_for_user( + session=session, + settings=settings, + user_id=user_id, + months=months, + sale_mode=sale_mode, + promo_code_service=promo_code_service, + ) + if resolved_price_rub is None: + logging.warning( + "FreeKassa: no server-side price for user %s, value=%s, mode=%s", + user_id, + months, + sale_mode, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_freekassa.py: %s", exc) + return + + if abs(resolved_price_rub - callback_price_rub) > 0.01: + logging.warning( + "FreeKassa: callback price mismatch for user %s, value=%s, mode=%s, callback=%.2f, resolved=%.2f", + user_id, + months, + sale_mode, + callback_price_rub, + resolved_price_rub, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_freekassa.py: %s", exc) + return + + price_rub = resolved_price_rub human_value = str(int(months)) if float(months).is_integer() else f"{months:g}" payment_description = ( get_text("payment_description_traffic", traffic_gb=human_value) @@ -69,8 +108,6 @@ async def pay_fk_callback_handler( ) currency_code = getattr(freekassa_service, "default_currency", None) or "RUB" - # Price is already discounted at payments_subscription.py stage - # Service will handle discount metadata if needed payment_record_payload = { "user_id": user_id, "amount": price_rub, diff --git a/bot/handlers/user/subscription/payments_platega.py b/bot/handlers/user/subscription/payments_platega.py index 1b2eafb..627991b 100644 --- a/bot/handlers/user/subscription/payments_platega.py +++ b/bot/handlers/user/subscription/payments_platega.py @@ -14,6 +14,8 @@ from db.dal import payment_dal router = Router(name="user_subscription_payments_platega_router") +from bot.handlers.user.subscription.payments_subscription import resolve_fiat_offer_price_for_user + @router.callback_query(F.data.startswith("pay_platega:")) async def pay_platega_callback_handler( callback: types.CallbackQuery, @@ -50,7 +52,7 @@ async def pay_platega_callback_handler( _, data_payload = callback.data.split(":", 1) parts = data_payload.split(":") months = float(parts[0]) - price_rub = float(parts[1]) + callback_price_rub = float(parts[1]) sale_mode = parts[2] if len(parts) > 2 else "subscription" except (ValueError, IndexError): logging.error(f"Invalid pay_platega data in callback: {callback.data}") @@ -61,6 +63,43 @@ async def pay_platega_callback_handler( return user_id = callback.from_user.id + resolved_price_rub = await resolve_fiat_offer_price_for_user( + session=session, + settings=settings, + user_id=user_id, + months=months, + sale_mode=sale_mode, + promo_code_service=promo_code_service, + ) + if resolved_price_rub is None: + logging.warning( + "Platega: no server-side price for user %s, value=%s, mode=%s", + user_id, + months, + sale_mode, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_platega.py: %s", exc) + return + + if abs(resolved_price_rub - callback_price_rub) > 0.01: + logging.warning( + "Platega: callback price mismatch for user %s, value=%s, mode=%s, callback=%.2f, resolved=%.2f", + user_id, + months, + sale_mode, + callback_price_rub, + resolved_price_rub, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_platega.py: %s", exc) + return + + price_rub = resolved_price_rub human_value = str(int(months)) if float(months).is_integer() else f"{months:g}" payment_description = ( get_text("payment_description_traffic", traffic_gb=human_value) diff --git a/bot/handlers/user/subscription/payments_severpay.py b/bot/handlers/user/subscription/payments_severpay.py index 0f6758f..e5d7e0e 100644 --- a/bot/handlers/user/subscription/payments_severpay.py +++ b/bot/handlers/user/subscription/payments_severpay.py @@ -13,6 +13,8 @@ from db.dal import payment_dal router = Router(name="user_subscription_payments_severpay_router") +from bot.handlers.user.subscription.payments_subscription import resolve_fiat_offer_price_for_user + @router.callback_query(F.data.startswith("pay_severpay:")) async def pay_severpay_callback_handler( callback: types.CallbackQuery, @@ -49,7 +51,7 @@ async def pay_severpay_callback_handler( _, data_payload = callback.data.split(":", 1) parts = data_payload.split(":") months = float(parts[0]) - price_rub = float(parts[1]) + callback_price_rub = float(parts[1]) sale_mode = parts[2] if len(parts) > 2 else "subscription" except (ValueError, IndexError): logging.error(f"Invalid pay_severpay data in callback: {callback.data}") @@ -60,6 +62,43 @@ async def pay_severpay_callback_handler( return user_id = callback.from_user.id + resolved_price_rub = await resolve_fiat_offer_price_for_user( + session=session, + settings=settings, + user_id=user_id, + months=months, + sale_mode=sale_mode, + promo_code_service=promo_code_service, + ) + if resolved_price_rub is None: + logging.warning( + "SeverPay: no server-side price for user %s, value=%s, mode=%s", + user_id, + months, + sale_mode, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_severpay.py: %s", exc) + return + + if abs(resolved_price_rub - callback_price_rub) > 0.01: + logging.warning( + "SeverPay: callback price mismatch for user %s, value=%s, mode=%s, callback=%.2f, resolved=%.2f", + user_id, + months, + sale_mode, + callback_price_rub, + resolved_price_rub, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_severpay.py: %s", exc) + return + + price_rub = resolved_price_rub human_value = str(int(months)) if float(months).is_integer() else f"{months:g}" payment_description = ( get_text("payment_description_traffic", traffic_gb=human_value) diff --git a/bot/handlers/user/subscription/payments_subscription.py b/bot/handlers/user/subscription/payments_subscription.py index b84dc9f..547c4bb 100644 --- a/bot/handlers/user/subscription/payments_subscription.py +++ b/bot/handlers/user/subscription/payments_subscription.py @@ -12,6 +12,36 @@ from config.settings import Settings router = Router(name="user_subscription_payments_selection_router") +async def resolve_fiat_offer_price_for_user( + session: AsyncSession, + settings: Settings, + user_id: int, + months: float, + sale_mode: str, + promo_code_service=None, +) -> Optional[float]: + """Resolve offer price server-side to prevent callback payload tampering.""" + price_source = ( + getattr(settings, "traffic_packages", {}) or {} + if sale_mode == "traffic" + else (settings.subscription_options or {}) + ) + base_price = price_source.get(months) + if base_price is None: + return None + + resolved_price = float(base_price) + if promo_code_service: + active_discount_info = await promo_code_service.get_user_active_discount(session, user_id) + if active_discount_info: + discount_pct, _ = active_discount_info + resolved_price, _ = promo_code_service.calculate_discounted_price( + resolved_price, + discount_pct, + ) + return resolved_price + + @router.callback_query(F.data.startswith("subscribe_period:")) async def select_subscription_period_callback_handler( callback: types.CallbackQuery, diff --git a/bot/handlers/user/subscription/payments_yookassa.py b/bot/handlers/user/subscription/payments_yookassa.py index 39f99dc..593f1cb 100644 --- a/bot/handlers/user/subscription/payments_yookassa.py +++ b/bot/handlers/user/subscription/payments_yookassa.py @@ -18,6 +18,8 @@ from db.dal import payment_dal, user_billing_dal, active_discount_dal router = Router(name="user_subscription_payments_yookassa_router") +from bot.handlers.user.subscription.payments_subscription import resolve_fiat_offer_price_for_user + def _format_value(val: float) -> str: return str(int(val)) if float(val).is_integer() else f"{val:g}" @@ -404,8 +406,46 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_yookassa.py: %s", exc) return - months, price_rub, sale_mode = parsed + months, callback_price_rub, sale_mode = parsed user_id = callback.from_user.id + + resolved_price_rub = await resolve_fiat_offer_price_for_user( + session=session, + settings=settings, + user_id=user_id, + months=months, + sale_mode=sale_mode, + promo_code_service=promo_code_service, + ) + if resolved_price_rub is None: + logging.warning( + "YooKassa: no server-side price for user %s, value=%s, mode=%s", + user_id, + months, + sale_mode, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_yookassa.py: %s", exc) + return + + if abs(resolved_price_rub - callback_price_rub) > 0.01: + logging.warning( + "YooKassa: callback price mismatch for user %s, value=%s, mode=%s, callback=%.2f, resolved=%.2f", + user_id, + months, + sale_mode, + callback_price_rub, + resolved_price_rub, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_yookassa.py: %s", exc) + return + + price_rub = resolved_price_rub currency_code_for_yk = "RUB" autopay_enabled = bool(settings.yookassa_autopayments_active and sale_mode != "traffic" and not settings.traffic_sale_mode) autopay_require_binding = bool( @@ -523,8 +563,45 @@ async def pay_yk_new_card_handler(callback: types.CallbackQuery, settings: Setti logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_yookassa.py: %s", exc) return - months, price_rub, sale_mode = parsed + months, callback_price_rub, sale_mode = parsed user_id = callback.from_user.id + resolved_price_rub = await resolve_fiat_offer_price_for_user( + session=session, + settings=settings, + user_id=user_id, + months=months, + sale_mode=sale_mode, + promo_code_service=promo_code_service, + ) + if resolved_price_rub is None: + logging.warning( + "YooKassa: no server-side price for new-card flow, user=%s, value=%s, mode=%s", + user_id, + months, + sale_mode, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_yookassa.py: %s", exc) + return + + if abs(resolved_price_rub - callback_price_rub) > 0.01: + logging.warning( + "YooKassa: callback price mismatch in new-card flow, user=%s, value=%s, mode=%s, callback=%.2f, resolved=%.2f", + user_id, + months, + sale_mode, + callback_price_rub, + resolved_price_rub, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_yookassa.py: %s", exc) + return + + price_rub = resolved_price_rub currency_code_for_yk = "RUB" autopay_enabled = bool(settings.yookassa_autopayments_active and sale_mode != "traffic" and not settings.traffic_sale_mode) autopay_require_binding = bool( @@ -555,7 +632,7 @@ async def pay_yk_new_card_handler(callback: types.CallbackQuery, settings: Setti @router.callback_query(F.data.startswith("pay_yk_saved_list:")) -async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, yookassa_service: YooKassaService, session: AsyncSession): +async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, yookassa_service: YooKassaService, session: AsyncSession, promo_code_service=None): current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key @@ -588,7 +665,7 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set try: months = float(parts[0]) - price_rub = float(parts[1]) + callback_price_rub = float(parts[1]) page = int(parts[2]) if len(parts) > 2 else 0 sale_mode = parts[3] if len(parts) > 3 else "subscription" except (ValueError, IndexError): @@ -608,6 +685,43 @@ async def pay_yk_saved_list_handler(callback: types.CallbackQuery, settings: Set return user_id = callback.from_user.id + resolved_price_rub = await resolve_fiat_offer_price_for_user( + session=session, + settings=settings, + user_id=user_id, + months=months, + sale_mode=sale_mode, + promo_code_service=promo_code_service, + ) + if resolved_price_rub is None: + logging.warning( + "YooKassa: no server-side price for saved-list flow, user=%s, value=%s, mode=%s", + user_id, + months, + sale_mode, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_yookassa.py: %s", exc) + return + + if abs(resolved_price_rub - callback_price_rub) > 0.01: + logging.warning( + "YooKassa: callback price mismatch in saved-list flow, user=%s, value=%s, mode=%s, callback=%.2f, resolved=%.2f", + user_id, + months, + sale_mode, + callback_price_rub, + resolved_price_rub, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_yookassa.py: %s", exc) + return + + price_rub = resolved_price_rub try: saved_methods = await user_billing_dal.list_user_payment_methods( session, user_id, provider="yookassa" @@ -744,7 +858,7 @@ async def pay_yk_use_saved_handler(callback: types.CallbackQuery, settings: Sett try: months = float(parts[0]) - price_rub = float(parts[1]) + callback_price_rub = float(parts[1]) sale_mode = parts[3] if len(parts) > 3 else "subscription" except (ValueError, IndexError): logging.error(f"pay_yk_use_saved months/price parsing error: {callback.data}") @@ -764,6 +878,41 @@ async def pay_yk_use_saved_handler(callback: types.CallbackQuery, settings: Sett method_identifier = parts[2] user_id = callback.from_user.id + resolved_price_rub = await resolve_fiat_offer_price_for_user( + session=session, + settings=settings, + user_id=user_id, + months=months, + sale_mode=sale_mode, + promo_code_service=promo_code_service, + ) + if resolved_price_rub is None: + logging.warning( + "YooKassa: no server-side price for use-saved flow, user=%s, value=%s, mode=%s", + user_id, + months, + sale_mode, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_yookassa.py: %s", exc) + return + + if abs(resolved_price_rub - callback_price_rub) > 0.01: + logging.warning( + "YooKassa: callback price mismatch in use-saved flow, user=%s, value=%s, mode=%s, callback=%.2f, resolved=%.2f", + user_id, + months, + sale_mode, + callback_price_rub, + resolved_price_rub, + ) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception as exc: + logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_yookassa.py: %s", exc) + return try: saved_methods = await user_billing_dal.list_user_payment_methods( @@ -791,6 +940,7 @@ async def pay_yk_use_saved_handler(callback: types.CallbackQuery, settings: Sett logging.debug("Suppressed exception in bot/handlers/user/subscription/payments_yookassa.py: %s", exc) return + price_rub = resolved_price_rub currency_code_for_yk = "RUB" await _initiate_yk_payment(