pay from saved card

This commit is contained in:
machka pasla
2025-10-16 22:04:04 +03:00
parent 394e8dcc6c
commit 51ffbbfa1d
4 changed files with 773 additions and 140 deletions
+666 -138
View File
@@ -2,11 +2,17 @@ import logging
from datetime import datetime from datetime import datetime
from aiogram import Router, F, types from aiogram import Router, F, types
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from typing import Optional from typing import Optional, List, Tuple
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config.settings import Settings from config.settings import Settings
from bot.keyboards.inline.user_keyboards import get_payment_method_keyboard, get_payment_url_keyboard from bot.keyboards.inline.user_keyboards import (
get_payment_method_keyboard,
get_payment_url_keyboard,
get_yk_autopay_choice_keyboard,
get_yk_saved_cards_keyboard,
get_back_to_main_menu_markup,
)
from bot.services.yookassa_service import YooKassaService from bot.services.yookassa_service import YooKassaService
from bot.services.freekassa_service import FreeKassaService from bot.services.freekassa_service import FreeKassaService
from bot.services.crypto_pay_service import CryptoPayService from bot.services.crypto_pay_service import CryptoPayService
@@ -17,6 +23,283 @@ from db.dal import payment_dal, user_billing_dal
router = Router(name="user_subscription_payments_router") router = Router(name="user_subscription_payments_router")
def _parse_months_and_price(payload: str) -> Optional[Tuple[int, float]]:
try:
months_str, price_str = payload.split(":")
return int(months_str), float(price_str)
except (ValueError, IndexError):
return None
def _format_saved_payment_method_title(get_text, network: Optional[str], last4: Optional[str], is_default: bool) -> str:
def _is_yoomoney_network(name: Optional[str]) -> bool:
s = (name or "").lower()
return "yoomoney" in s or "yoo money" in s or "yoo-money" in s
def _extract_last4(text: str) -> Optional[str]:
digits = "".join(ch for ch in text if ch.isdigit())
return digits[-4:] if len(digits) >= 4 else None
if _is_yoomoney_network(network):
inferred_last4 = last4 or (_extract_last4(network or "") or "****")
title = get_text("payment_method_wallet_title", last4=inferred_last4)
elif last4:
network_name = network or get_text("payment_network_card")
title = get_text("payment_method_card_title", network=network_name, last4=last4)
else:
network_name = network or get_text("payment_network_generic")
title = get_text("payment_method_generic_title", network=network_name)
return f"{title}" if is_default else title
async def _initiate_yk_payment(
callback: types.CallbackQuery,
*,
settings: Settings,
session: AsyncSession,
yookassa_service: YooKassaService,
i18n: Optional[JsonI18n],
current_lang: str,
get_text,
user_id: int,
months: int,
price_rub: float,
currency_code_for_yk: str,
save_payment_method: bool,
back_callback: str,
payment_method_id: Optional[str] = None,
selected_method_internal_id: Optional[int] = None,
) -> bool:
"""Create payment record and initiate YooKassa payment (new card or saved card)."""
if not callback.message:
return False
payment_description = get_text("payment_description_subscription", months=months)
payment_record_data = {
"user_id": user_id,
"amount": price_rub,
"currency": currency_code_for_yk,
"status": "pending_yookassa",
"description": payment_description,
"subscription_duration_months": months,
}
db_payment_record = None
try:
db_payment_record = await payment_dal.create_payment_record(session, payment_record_data)
await session.commit()
logging.info(
f"Payment record {db_payment_record.payment_id} created for user {user_id} with status 'pending_yookassa'."
)
except Exception as e_db_payment:
await session.rollback()
logging.error(
f"Failed to create payment record in DB for user {user_id}: {e_db_payment}",
exc_info=True,
)
try:
await callback.message.edit_text(get_text("error_creating_payment_record"))
except Exception:
pass
return False
if not db_payment_record:
try:
await callback.message.edit_text(get_text("error_creating_payment_record"))
except Exception:
pass
return False
yookassa_metadata = {
"user_id": str(user_id),
"subscription_months": str(months),
"payment_db_id": str(db_payment_record.payment_id),
}
if payment_method_id:
yookassa_metadata["used_saved_payment_method_id"] = payment_method_id
receipt_email_for_yk = settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL
payment_response_yk = await yookassa_service.create_payment(
amount=price_rub,
currency=currency_code_for_yk,
description=payment_description,
metadata=yookassa_metadata,
receipt_email=receipt_email_for_yk,
save_payment_method=save_payment_method,
payment_method_id=payment_method_id,
)
if payment_response_yk and payment_response_yk.get("confirmation_url"):
pm = payment_response_yk.get("payment_method")
try:
if pm and pm.get("id"):
pm_type = pm.get("type")
title = pm.get("title")
card = pm.get("card") or {}
account_number = pm.get("account_number") or pm.get("account")
if isinstance(card, dict) and (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")
elif (pm_type or "").lower() in {"yoo_money", "yoomoney", "yoo-money", "wallet"}:
display_network = "YooMoney"
display_last4 = (
account_number[-4:]
if isinstance(account_number, str) and len(account_number) >= 4
else None
)
else:
display_network = title or (pm_type.upper() if pm_type else "Payment method")
display_last4 = None
await user_billing_dal.upsert_yk_payment_method(
session,
user_id=user_id,
payment_method_id=pm["id"],
card_last4=display_last4,
card_network=display_network,
)
try:
await user_billing_dal.upsert_user_payment_method(
session,
user_id=user_id,
provider_payment_method_id=pm["id"],
provider="yookassa",
card_last4=display_last4,
card_network=display_network,
set_default=save_payment_method,
)
except Exception:
pass
await session.commit()
except Exception:
await session.rollback()
logging.exception("Failed to save YooKassa payment method preliminarily")
try:
await payment_dal.update_payment_status_by_db_id(
session,
payment_db_id=db_payment_record.payment_id,
new_status=payment_response_yk.get("status", "pending"),
yk_payment_id=payment_response_yk.get("id"),
)
if selected_method_internal_id is not None:
try:
await user_billing_dal.set_user_default_payment_method(
session, user_id, selected_method_internal_id
)
except Exception:
logging.exception("Failed to set default payment method after initiating payment")
await session.commit()
except Exception as e_db_update_ykid:
await session.rollback()
logging.error(
f"Failed to update payment record {db_payment_record.payment_id} with YK ID: {e_db_update_ykid}",
exc_info=True,
)
try:
await callback.message.edit_text(get_text("error_payment_gateway_link_failed"))
except Exception:
pass
return False
try:
await callback.message.edit_text(
get_text(key="payment_link_message", months=months),
reply_markup=get_payment_url_keyboard(
payment_response_yk["confirmation_url"],
current_lang,
i18n,
back_callback=back_callback,
back_text_key="back_to_payment_methods_button",
),
disable_web_page_preview=False,
)
except Exception as e_edit:
logging.warning(
f"Edit message for payment link failed: {e_edit}. Sending new one."
)
try:
await callback.message.answer(
get_text(key="payment_link_message", months=months),
reply_markup=get_payment_url_keyboard(
payment_response_yk["confirmation_url"],
current_lang,
i18n,
back_callback=back_callback,
back_text_key="back_to_payment_methods_button",
),
disable_web_page_preview=False,
)
except Exception:
pass
return True
if payment_response_yk and payment_method_id:
status_to_store = payment_response_yk.get("status", "pending")
try:
await payment_dal.update_payment_status_by_db_id(
session,
payment_db_id=db_payment_record.payment_id,
new_status=status_to_store,
yk_payment_id=payment_response_yk.get("id"),
)
if selected_method_internal_id is not None:
try:
await user_billing_dal.set_user_default_payment_method(
session, user_id, selected_method_internal_id
)
except Exception:
logging.exception("Failed to set default payment method after saved-card payment start")
await session.commit()
except Exception as e_db_update_saved:
await session.rollback()
logging.error(
f"Failed to update saved-card payment record {db_payment_record.payment_id}: {e_db_update_saved}",
exc_info=True,
)
try:
await callback.message.edit_text(get_text("error_payment_gateway"))
except Exception:
pass
return False
message_text = get_text("yookassa_autopay_charge_initiated")
try:
await callback.message.edit_text(
message_text,
reply_markup=get_back_to_main_menu_markup(current_lang, i18n),
)
except Exception as e_edit:
logging.warning(f"Failed to notify about saved-card charge start: {e_edit}")
try:
await callback.message.answer(
message_text,
reply_markup=get_back_to_main_menu_markup(current_lang, i18n),
)
except Exception:
pass
return True
try:
await payment_dal.update_payment_status_by_db_id(
session, db_payment_record.payment_id, "failed_creation"
)
await session.commit()
except Exception as e_db_fail_create:
await session.rollback()
logging.error(
f"Additionally failed to update payment record to 'failed_creation': {e_db_fail_create}",
exc_info=True,
)
logging.error(
f"Failed to create payment in YooKassa for user {user_id}, payment_db_id {db_payment_record.payment_id}. Response: {payment_response_yk}"
)
try:
await callback.message.edit_text(get_text("error_payment_gateway"))
except Exception:
pass
return False
@router.callback_query(F.data.startswith("subscribe_period:")) @router.callback_query(F.data.startswith("subscribe_period:"))
async def select_subscription_period_callback_handler(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession): async def select_subscription_period_callback_handler(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
@@ -104,10 +387,7 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
try: try:
_, data_payload = callback.data.split(":", 1) _, data_payload = callback.data.split(":", 1)
months_str, price_str = data_payload.split(":") except ValueError:
months = int(months_str)
price_rub = float(price_str)
except (ValueError, IndexError):
logging.error(f"Invalid pay_yk data in callback: {callback.data}") logging.error(f"Invalid pay_yk data in callback: {callback.data}")
try: try:
await callback.answer(get_text("error_try_again"), show_alert=True) await callback.answer(get_text("error_try_again"), show_alert=True)
@@ -115,155 +395,403 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
pass pass
return return
parsed = _parse_months_and_price(data_payload)
if not parsed:
logging.error(f"Invalid pay_yk payload structure: {callback.data}")
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
except Exception:
pass
return
months, price_rub = parsed
user_id = callback.from_user.id user_id = callback.from_user.id
payment_description = get_text("payment_description_subscription", months=months)
currency_code_for_yk = "RUB" currency_code_for_yk = "RUB"
autopay_enabled = bool(getattr(settings, 'YOOKASSA_AUTOPAYMENTS_ENABLED', False))
payment_record_data = { saved_methods: List = []
"user_id": user_id, if autopay_enabled:
"amount": price_rub,
"currency": currency_code_for_yk,
"status": "pending_yookassa",
"description": payment_description,
"subscription_duration_months": months,
}
db_payment_record = None
try:
db_payment_record = await payment_dal.create_payment_record(session, payment_record_data)
await session.commit()
logging.info(
f"Payment record {db_payment_record.payment_id} created for user {user_id} with status 'pending_yookassa'."
)
except Exception as e_db_payment:
await session.rollback()
logging.error(
f"Failed to create payment record in DB for user {user_id}: {e_db_payment}",
exc_info=True,
)
await callback.message.edit_text(get_text("error_creating_payment_record"))
try: try:
await callback.answer(get_text("error_try_again"), show_alert=True) saved_methods = await user_billing_dal.list_user_payment_methods(
except Exception: session, user_id, provider="yookassa"
pass
return
if not db_payment_record:
await callback.message.edit_text(get_text("error_creating_payment_record"))
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
except Exception:
pass
return
yookassa_metadata = {
"user_id": str(user_id),
"subscription_months": str(months),
"payment_db_id": str(db_payment_record.payment_id),
}
receipt_email_for_yk = settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL
payment_response_yk = await yookassa_service.create_payment(
amount=price_rub,
currency=currency_code_for_yk,
description=payment_description,
metadata=yookassa_metadata,
receipt_email=receipt_email_for_yk,
# Save method only when autopayments are enabled
save_payment_method=bool(getattr(settings, 'YOOKASSA_AUTOPAYMENTS_ENABLED', False)),
)
if payment_response_yk and payment_response_yk.get("confirmation_url"):
pm = payment_response_yk.get("payment_method")
try:
if pm and pm.get("id"):
pm_type = pm.get("type")
title = pm.get("title")
card = pm.get("card") or {}
account_number = pm.get("account_number") or pm.get("account")
if isinstance(card, dict) and (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")
elif (pm_type or "").lower() in {"yoo_money", "yoomoney", "yoo-money", "wallet"}:
display_network = "YooMoney"
display_last4 = (
account_number[-4:]
if isinstance(account_number, str) and len(account_number) >= 4
else None
)
else:
display_network = title or (pm_type.upper() if pm_type else "Payment method")
display_last4 = None
await user_billing_dal.upsert_yk_payment_method(
session,
user_id=user_id,
payment_method_id=pm["id"],
card_last4=display_last4,
card_network=display_network,
)
try:
await user_billing_dal.upsert_user_payment_method(
session,
user_id=user_id,
provider_payment_method_id=pm["id"],
provider="yookassa",
card_last4=display_last4,
card_network=display_network,
set_default=True,
)
except Exception:
pass
await session.commit()
except Exception:
await session.rollback()
logging.exception("Failed to save YooKassa payment method preliminarily")
try:
await payment_dal.update_payment_status_by_db_id(
session,
payment_db_id=db_payment_record.payment_id,
new_status=payment_response_yk.get("status", "pending"),
yk_payment_id=payment_response_yk.get("id"),
) )
await session.commit() except Exception as e_list:
except Exception as e_db_update_ykid: logging.exception(f"Failed to load saved payment methods for user {user_id}: {e_list}")
await session.rollback() saved_methods = []
logging.error(
f"Failed to update payment record {db_payment_record.payment_id} with YK ID: {e_db_update_ykid}", if autopay_enabled and saved_methods:
exc_info=True, try:
await callback.message.edit_text(
get_text("yookassa_autopay_flow_prompt"),
reply_markup=get_yk_autopay_choice_keyboard(
months,
price_rub,
current_lang,
i18n,
has_saved_cards=True,
),
) )
await callback.message.edit_text(get_text("error_payment_gateway_link_failed")) except Exception as e_edit:
logging.warning(f"Failed to show autopay choice: {e_edit}. Sending new message.")
try: try:
await callback.answer(get_text("error_try_again"), show_alert=True) await callback.message.answer(
get_text("yookassa_autopay_flow_prompt"),
reply_markup=get_yk_autopay_choice_keyboard(
months,
price_rub,
current_lang,
i18n,
has_saved_cards=True,
),
)
except Exception: except Exception:
pass pass
return try:
await callback.answer()
except Exception:
pass
return
await _initiate_yk_payment(
callback,
settings=settings,
session=session,
yookassa_service=yookassa_service,
i18n=i18n,
current_lang=current_lang,
get_text=get_text,
user_id=user_id,
months=months,
price_rub=price_rub,
currency_code_for_yk=currency_code_for_yk,
save_payment_method=autopay_enabled,
back_callback=f"subscribe_period:{months}",
)
try:
await callback.answer()
except Exception:
pass
@router.callback_query(F.data.startswith("pay_yk_new:"))
async def pay_yk_new_card_handler(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, yookassa_service: YooKassaService, session: AsyncSession):
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
if not i18n or not callback.message:
try:
await callback.answer(get_text("error_occurred_try_again"), show_alert=True)
except Exception:
pass
return
if not yookassa_service or not yookassa_service.configured:
logging.error("YooKassa service unavailable for pay_yk_new.")
try:
await callback.answer(get_text("payment_service_unavailable_alert"), show_alert=True)
except Exception:
pass
try:
await callback.message.edit_text(get_text("payment_service_unavailable"))
except Exception:
pass
return
try:
_, data_payload = callback.data.split(":", 1)
except ValueError:
logging.error(f"Invalid pay_yk_new data in callback: {callback.data}")
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
except Exception:
pass
return
parsed = _parse_months_and_price(data_payload)
if not parsed:
logging.error(f"Invalid pay_yk_new payload structure: {callback.data}")
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
except Exception:
pass
return
months, price_rub = parsed
user_id = callback.from_user.id
currency_code_for_yk = "RUB"
autopay_enabled = bool(getattr(settings, 'YOOKASSA_AUTOPAYMENTS_ENABLED', False))
await _initiate_yk_payment(
callback,
settings=settings,
session=session,
yookassa_service=yookassa_service,
i18n=i18n,
current_lang=current_lang,
get_text=get_text,
user_id=user_id,
months=months,
price_rub=price_rub,
currency_code_for_yk=currency_code_for_yk,
save_payment_method=autopay_enabled,
back_callback=f"subscribe_period:{months}",
)
try:
await callback.answer()
except Exception:
pass
@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):
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
if not i18n or not callback.message:
try:
await callback.answer(get_text("error_occurred_try_again"), show_alert=True)
except Exception:
pass
return
autopay_enabled = bool(getattr(settings, 'YOOKASSA_AUTOPAYMENTS_ENABLED', False))
if not autopay_enabled:
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
except Exception:
pass
return
try:
_, data_payload = callback.data.split(":", 1)
except ValueError:
logging.error(f"Invalid pay_yk_saved_list data: {callback.data}")
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
except Exception:
pass
return
parts = data_payload.split(":")
if len(parts) < 2:
logging.error(f"pay_yk_saved_list payload missing components: {callback.data}")
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
except Exception:
pass
return
try:
months = int(parts[0])
price_rub = float(parts[1])
page = int(parts[2]) if len(parts) > 2 else 0
except (ValueError, IndexError):
logging.error(f"pay_yk_saved_list payload parsing error: {callback.data}")
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
except Exception:
pass
return
user_id = callback.from_user.id
try:
saved_methods = await user_billing_dal.list_user_payment_methods(
session, user_id, provider="yookassa"
)
except Exception as e_list:
logging.exception(f"Failed to list saved payment methods for user {user_id}: {e_list}")
saved_methods = []
if not saved_methods:
try:
await callback.message.edit_text(
get_text("yookassa_autopay_no_saved_cards"),
reply_markup=get_yk_autopay_choice_keyboard(
months,
price_rub,
current_lang,
i18n,
has_saved_cards=False,
),
)
except Exception as e_edit:
logging.warning(f"Failed to display no-saved-card notice: {e_edit}")
try:
await callback.message.answer(
get_text("yookassa_autopay_no_saved_cards"),
reply_markup=get_yk_autopay_choice_keyboard(
months,
price_rub,
current_lang,
i18n,
has_saved_cards=False,
),
)
except Exception:
pass
try:
await callback.answer()
except Exception:
pass
return
cards: List[Tuple[str, str]] = []
for method in saved_methods:
title = _format_saved_payment_method_title(
get_text, method.card_network, method.card_last4, method.is_default
)
cards.append((str(method.method_id), title))
per_page = 5
max_page = max(0, (len(cards) - 1) // per_page)
page = max(0, min(page, max_page))
try:
await callback.message.edit_text( await callback.message.edit_text(
get_text(key="payment_link_message", months=months), get_text("yookassa_autopay_choose_saved_card"),
reply_markup=get_payment_url_keyboard( reply_markup=get_yk_saved_cards_keyboard(
payment_response_yk["confirmation_url"], cards,
months,
price_rub,
current_lang, current_lang,
i18n, i18n,
back_callback=f"subscribe_period:{months}", page=page,
back_text_key="back_to_payment_methods_button",
), ),
disable_web_page_preview=False,
) )
else: except Exception as e_edit:
logging.warning(f"Failed to display saved card list: {e_edit}")
try: try:
await payment_dal.update_payment_status_by_db_id(session, db_payment_record.payment_id, "failed_creation") await callback.message.answer(
await session.commit() get_text("yookassa_autopay_choose_saved_card"),
except Exception as e_db_fail_create: reply_markup=get_yk_saved_cards_keyboard(
await session.rollback() cards,
logging.error( months,
f"Additionally failed to update payment record to 'failed_creation': {e_db_fail_create}", price_rub,
exc_info=True, current_lang,
i18n,
page=page,
),
) )
logging.error( except Exception:
f"Failed to create payment in YooKassa for user {user_id}, payment_db_id {db_payment_record.payment_id}. Response: {payment_response_yk}" pass
) try:
await callback.message.edit_text(get_text("error_payment_gateway")) await callback.answer()
except Exception:
pass
@router.callback_query(F.data.startswith("pay_yk_use_saved:"))
async def pay_yk_use_saved_handler(callback: types.CallbackQuery, settings: Settings, i18n_data: dict, yookassa_service: YooKassaService, session: AsyncSession):
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
if not i18n or not callback.message:
try:
await callback.answer(get_text("error_occurred_try_again"), show_alert=True)
except Exception:
pass
return
autopay_enabled = bool(getattr(settings, 'YOOKASSA_AUTOPAYMENTS_ENABLED', False))
if not autopay_enabled:
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
except Exception:
pass
return
if not yookassa_service or not yookassa_service.configured:
logging.error("YooKassa service unavailable for pay_yk_use_saved.")
try:
await callback.answer(get_text("payment_service_unavailable_alert"), show_alert=True)
except Exception:
pass
try:
await callback.message.edit_text(get_text("payment_service_unavailable"))
except Exception:
pass
return
try:
_, data_payload = callback.data.split(":", 1)
except ValueError:
logging.error(f"Invalid pay_yk_use_saved data: {callback.data}")
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
except Exception:
pass
return
parts = data_payload.split(":")
if len(parts) < 3:
logging.error(f"pay_yk_use_saved payload missing components: {callback.data}")
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
except Exception:
pass
return
try:
months = int(parts[0])
price_rub = float(parts[1])
except (ValueError, IndexError):
logging.error(f"pay_yk_use_saved months/price parsing error: {callback.data}")
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
except Exception:
pass
return
method_identifier = parts[2]
user_id = callback.from_user.id
try:
saved_methods = await user_billing_dal.list_user_payment_methods(
session, user_id, provider="yookassa"
)
except Exception as e_list:
logging.exception(f"Failed to list saved payment methods for user {user_id}: {e_list}")
saved_methods = []
selected_method = None
for method in saved_methods:
if method_identifier.isdigit():
if method.method_id == int(method_identifier):
selected_method = method
break
if method.provider_payment_method_id == method_identifier:
selected_method = method
break
if not selected_method:
logging.warning(f"Selected payment method not found for user {user_id}: {method_identifier}")
try:
await callback.answer(get_text("error_try_again"), show_alert=True)
except Exception:
pass
return
currency_code_for_yk = "RUB"
await _initiate_yk_payment(
callback,
settings=settings,
session=session,
yookassa_service=yookassa_service,
i18n=i18n,
current_lang=current_lang,
get_text=get_text,
user_id=user_id,
months=months,
price_rub=price_rub,
currency_code_for_yk=currency_code_for_yk,
save_payment_method=False,
back_callback=f"pay_yk_saved_list:{months}:{price_rub}",
payment_method_id=selected_method.provider_payment_method_id,
selected_method_internal_id=selected_method.method_id,
)
try: try:
await callback.answer() await callback.answer()
except Exception: except Exception:
+91
View File
@@ -156,6 +156,97 @@ def get_payment_url_keyboard(payment_url: str,
return builder.as_markup() return builder.as_markup()
def get_yk_autopay_choice_keyboard(
months: int,
price: float,
lang: str,
i18n_instance,
has_saved_cards: bool = True,
) -> InlineKeyboardMarkup:
"""Keyboard for choosing between saved card charge or new card payment when auto-renew is enabled."""
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder()
price_str = str(price)
if has_saved_cards:
builder.row(
InlineKeyboardButton(
text=_(key="yookassa_autopay_pay_saved_card_button"),
callback_data=f"pay_yk_saved_list:{months}:{price_str}",
)
)
builder.row(
InlineKeyboardButton(
text=_(key="yookassa_autopay_pay_new_card_button"),
callback_data=f"pay_yk_new:{months}:{price_str}",
)
)
builder.row(
InlineKeyboardButton(
text=_(key="back_to_payment_methods_button"),
callback_data=f"subscribe_period:{months}",
)
)
return builder.as_markup()
def get_yk_saved_cards_keyboard(
cards: List[Tuple[str, str]],
months: int,
price: float,
lang: str,
i18n_instance,
page: int = 0,
) -> InlineKeyboardMarkup:
"""Paginated keyboard for selecting a saved YooKassa card."""
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder()
per_page = 5
total = len(cards)
start = page * per_page
end = min(total, start + per_page)
price_str = str(price)
for method_id, title in cards[start:end]:
builder.row(
InlineKeyboardButton(
text=title,
callback_data=f"pay_yk_use_saved:{months}:{price_str}:{method_id}",
)
)
nav_buttons: List[InlineKeyboardButton] = []
if start > 0:
nav_buttons.append(
InlineKeyboardButton(
text="⬅️",
callback_data=f"pay_yk_saved_list:{months}:{price_str}:{page-1}",
)
)
if end < total:
nav_buttons.append(
InlineKeyboardButton(
text="➡️",
callback_data=f"pay_yk_saved_list:{months}:{price_str}:{page+1}",
)
)
if nav_buttons:
builder.row(*nav_buttons)
builder.row(
InlineKeyboardButton(
text=_(key="yookassa_autopay_pay_new_card_button"),
callback_data=f"pay_yk_new:{months}:{price_str}",
)
)
builder.row(
InlineKeyboardButton(
text=_(key="back_to_autopay_method_choice_button"),
callback_data=f"pay_yk:{months}:{price_str}",
)
)
return builder.as_markup()
def get_referral_link_keyboard(lang: str, def get_referral_link_keyboard(lang: str,
i18n_instance) -> InlineKeyboardMarkup: i18n_instance) -> InlineKeyboardMarkup:
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
+8 -1
View File
@@ -24,6 +24,13 @@
"choose_payment_method": "Choose payment method:", "choose_payment_method": "Choose payment method:",
"pay_button": "💳 Pay", "pay_button": "💳 Pay",
"pay_with_yookassa_button": "💳 YooKassa", "pay_with_yookassa_button": "💳 YooKassa",
"yookassa_autopay_flow_prompt": "Auto-renew is enabled. Choose how you'd like to pay:",
"yookassa_autopay_pay_saved_card_button": "💳 Pay with saved card",
"yookassa_autopay_pay_new_card_button": " Pay with new card",
"yookassa_autopay_choose_saved_card": "Choose a saved card to charge:",
"yookassa_autopay_no_saved_cards": "No saved cards found. Pay with a new card or link one in Payment Methods.",
"back_to_autopay_method_choice_button": "⬅️ Back to payment choice",
"yookassa_autopay_charge_initiated": "Charge request sent to the selected card. We'll notify you once the payment completes.",
"pay_with_sbp_button": "📱 SBP", "pay_with_sbp_button": "📱 SBP",
"back_to_payment_methods_button": "⬅️ Back to payment methods", "back_to_payment_methods_button": "⬅️ Back to payment methods",
"pay_with_cryptopay_button": "💎 CryptoBot", "pay_with_cryptopay_button": "💎 CryptoBot",
@@ -432,4 +439,4 @@
"admin_ads_deleted_success": "Campaign deleted.", "admin_ads_deleted_success": "Campaign deleted.",
"admin_ads_not_found": "Campaign not found.", "admin_ads_not_found": "Campaign not found.",
"free_kassa_order_full": "Order #{order_id} from {date}\n\n" "free_kassa_order_full": "Order #{order_id} from {date}\n\n"
} }
+8 -1
View File
@@ -24,6 +24,13 @@
"choose_payment_method": "Выберите способ оплаты:", "choose_payment_method": "Выберите способ оплаты:",
"pay_button": "💳 Оплатить", "pay_button": "💳 Оплатить",
"pay_with_yookassa_button": "💳 ЮKassa", "pay_with_yookassa_button": "💳 ЮKassa",
"yookassa_autopay_flow_prompt": "Автопродление включено. Выберите, как оплатить подписку:",
"yookassa_autopay_pay_saved_card_button": "💳 Оплата привязанной картой",
"yookassa_autopay_pay_new_card_button": "➕ Оплата новой картой",
"yookassa_autopay_choose_saved_card": "Выберите привязанную карту для списания:",
"yookassa_autopay_no_saved_cards": "Сохранённых карт нет. Оплатите новой картой или привяжите карту в разделе «Способы оплаты».",
"back_to_autopay_method_choice_button": "⬅️ Назад к выбору способа оплаты",
"yookassa_autopay_charge_initiated": "Запрос на списание с выбранной карты отправлен. Сообщим, как только платёж завершится.",
"pay_with_sbp_button": "📱 СБП", "pay_with_sbp_button": "📱 СБП",
"back_to_payment_methods_button": "⬅️ Назад к выбору оплаты", "back_to_payment_methods_button": "⬅️ Назад к выбору оплаты",
"pay_with_cryptopay_button": "💎 CryptoBot", "pay_with_cryptopay_button": "💎 CryptoBot",
@@ -432,4 +439,4 @@
"admin_ads_deleted_success": "Кампания удалена.", "admin_ads_deleted_success": "Кампания удалена.",
"admin_ads_not_found": "Кампания не найдена.", "admin_ads_not_found": "Кампания не найдена.",
"free_kassa_order_full": "Заказ №{order_id} от {date}\n\n" "free_kassa_order_full": "Заказ №{order_id} от {date}\n\n"
} }