import logging from aiogram import Router, F, types from typing import Optional from sqlalchemy.ext.asyncio import AsyncSession from config.settings import Settings from bot.keyboards.inline.user_keyboards import get_payment_method_keyboard, get_payment_url_keyboard from bot.services.yookassa_service import YooKassaService from bot.services.freekassa_service import FreeKassaService from bot.services.crypto_pay_service import CryptoPayService from bot.services.stars_service import StarsService from bot.middlewares.i18n import JsonI18n from db.dal import payment_dal, user_billing_dal router = Router(name="user_subscription_payments_router") @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): 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 try: months = int(callback.data.split(":")[-1]) except (ValueError, IndexError): logging.error(f"Invalid subscription period in callback_data: {callback.data}") try: await callback.answer(get_text("error_try_again"), show_alert=True) except Exception: pass return price_rub = settings.subscription_options.get(months) if price_rub is None: logging.error( f"Price not found for {months} months subscription period in settings.subscription_options." ) try: await callback.answer(get_text("error_try_again"), show_alert=True) except Exception: pass return currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL text_content = get_text("choose_payment_method") tribute_url = settings.tribute_payment_links.get(months) stars_price = settings.stars_subscription_options.get(months) reply_markup = get_payment_method_keyboard( months, price_rub, tribute_url, stars_price, currency_symbol_val, current_lang, i18n, settings, ) try: await callback.message.edit_text(text_content, reply_markup=reply_markup) except Exception as e_edit: logging.warning( f"Edit message for payment method selection failed: {e_edit}. Sending new one." ) await callback.message.answer(text_content, reply_markup=reply_markup) try: await callback.answer() except Exception: pass @router.callback_query(F.data.startswith("pay_yk:")) async def pay_yk_callback_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 is not configured or unavailable.") target_msg_edit = callback.message await target_msg_edit.edit_text(get_text("payment_service_unavailable")) try: await callback.answer(get_text("payment_service_unavailable_alert"), show_alert=True) except Exception: pass return try: _, data_payload = callback.data.split(":", 1) months_str, price_str = data_payload.split(":") months = int(months_str) price_rub = float(price_str) except (ValueError, IndexError): logging.error(f"Invalid pay_yk data in callback: {callback.data}") try: await callback.answer(get_text("error_try_again"), show_alert=True) except Exception: pass return user_id = callback.from_user.id payment_description = get_text("payment_description_subscription", months=months) currency_code_for_yk = "RUB" 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, ) 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 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_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, ) await callback.message.edit_text(get_text("error_payment_gateway_link_failed")) try: await callback.answer(get_text("error_try_again"), show_alert=True) except Exception: pass return 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), disable_web_page_preview=False, ) else: 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}" ) await callback.message.edit_text(get_text("error_payment_gateway")) try: await callback.answer() except Exception: pass @router.callback_query(F.data.startswith("pay_fk:")) async def pay_fk_callback_handler( callback: types.CallbackQuery, settings: Settings, i18n_data: dict, freekassa_service: FreeKassaService, 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 freekassa_service or not freekassa_service.configured: logging.error("FreeKassa service is not configured or unavailable.") 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) months_str, price_str = data_payload.split(":") months = int(months_str) price_rub = float(price_str) except (ValueError, IndexError): logging.error(f"Invalid pay_fk data in callback: {callback.data}") try: await callback.answer(get_text("error_try_again"), show_alert=True) except Exception: pass return user_id = callback.from_user.id payment_description = get_text("payment_description_subscription", months=months) currency_code = freekassa_service.currency or settings.DEFAULT_CURRENCY_SYMBOL or "RUB" payment_record_payload = { "user_id": user_id, "amount": price_rub, "currency": currency_code, "status": "pending_freekassa", "description": payment_description, "subscription_duration_months": months, "provider": "freekassa", } try: payment_record = await payment_dal.create_payment_record(session, payment_record_payload) await session.commit() except Exception as e_db_create: await session.rollback() logging.error( f"FreeKassa: failed to create payment record for user {user_id}: {e_db_create}", exc_info=True, ) try: await callback.message.edit_text(get_text("error_creating_payment_record")) except Exception: pass try: await callback.answer(get_text("error_try_again"), show_alert=True) except Exception: pass return payment_link = None try: payment_link = freekassa_service.build_payment_link( payment_db_id=payment_record.payment_id, user_id=user_id, months=months, amount=price_rub, ) except Exception as e_link: logging.error(f"FreeKassa: failed to build payment link for payment {payment_record.payment_id}: {e_link}", exc_info=True) if payment_link: try: await callback.message.edit_text( get_text(key="payment_link_message", months=months), reply_markup=get_payment_url_keyboard(payment_link, current_lang, i18n), disable_web_page_preview=False, ) except Exception as e_edit: logging.warning(f"FreeKassa: edit message failed ({e_edit}), sending new message.") try: await callback.message.answer( get_text(key="payment_link_message", months=months), reply_markup=get_payment_url_keyboard(payment_link, current_lang, i18n), disable_web_page_preview=False, ) except Exception: pass try: await callback.answer() except Exception: pass return try: await payment_dal.update_payment_status_by_db_id( session, payment_record.payment_id, "failed_creation", ) await session.commit() except Exception as e_status: await session.rollback() logging.error(f"FreeKassa: failed to mark payment {payment_record.payment_id} as failed_creation: {e_status}", exc_info=True) try: await callback.message.edit_text(get_text("error_payment_gateway")) except Exception: pass try: await callback.answer(get_text("error_payment_gateway"), show_alert=True) except Exception: pass @router.callback_query(F.data.startswith("pay_crypto:")) async def pay_crypto_callback_handler( callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession, cryptopay_service: CryptoPayService, ): 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 cryptopay_service or not getattr(cryptopay_service, "configured", False): try: await callback.answer(get_text("payment_service_unavailable_alert"), show_alert=True) except Exception: pass return try: _, data_payload = callback.data.split(":", 1) months_str, price_str = data_payload.split(":") months = int(months_str) price_amount = float(price_str) except (ValueError, IndexError): try: await callback.answer(get_text("error_try_again"), show_alert=True) except Exception: pass return user_id = callback.from_user.id payment_description = get_text("payment_description_subscription", months=months) invoice_url = await cryptopay_service.create_invoice( session=session, user_id=user_id, months=months, amount=price_amount, description=payment_description, ) if invoice_url: try: await callback.message.edit_text( get_text(key="payment_link_message", months=months), reply_markup=get_payment_url_keyboard(invoice_url, current_lang, i18n), disable_web_page_preview=False, ) except Exception: try: await callback.message.answer( get_text(key="payment_link_message", months=months), reply_markup=get_payment_url_keyboard(invoice_url, current_lang, i18n), disable_web_page_preview=False, ) except Exception: pass try: await callback.answer() except Exception: pass return try: await callback.answer(get_text("error_payment_gateway"), show_alert=True) except Exception: pass @router.callback_query(F.data.startswith("pay_stars:")) async def pay_stars_callback_handler( callback: types.CallbackQuery, settings: Settings, i18n_data: dict, session: AsyncSession, stars_service: StarsService, ): 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 settings.STARS_ENABLED: try: await callback.answer(get_text("payment_service_unavailable_alert"), show_alert=True) except Exception: pass return try: _, data_payload = callback.data.split(":", 1) months_str, stars_price_str = data_payload.split(":") months = int(months_str) stars_price = int(stars_price_str) except (ValueError, IndexError): try: await callback.answer(get_text("error_try_again"), show_alert=True) except Exception: pass return user_id = callback.from_user.id payment_description = get_text("payment_description_subscription", months=months) payment_db_id = await stars_service.create_invoice( session=session, user_id=user_id, months=months, stars_price=stars_price, description=payment_description, ) if payment_db_id: try: await callback.answer() except Exception: pass return try: await callback.answer(get_text("error_payment_gateway"), show_alert=True) except Exception: pass @router.pre_checkout_query() async def handle_pre_checkout_query(query: types.PreCheckoutQuery): try: await query.answer(ok=True) except Exception: # Nothing else to do here; Telegram will show an error if not answered pass @router.message(F.successful_payment) async def handle_successful_stars_payment( message: types.Message, settings: Settings, i18n_data: dict, session: AsyncSession, stars_service: StarsService, ): payload = (message.successful_payment.invoice_payload if message and message.successful_payment else "") try: payment_db_id_str, months_str = (payload or "").split(":", 1) payment_db_id = int(payment_db_id_str) months = int(months_str) except Exception: return stars_amount = int(message.successful_payment.total_amount) if message.successful_payment else 0 await stars_service.process_successful_payment( session=session, message=message, payment_db_id=payment_db_id, months=months, stars_amount=stars_amount, i18n_data=i18n_data, )