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.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user