diff --git a/.env.example b/.env.example index 873d2f1e..0c040c5e 100644 --- a/.env.example +++ b/.env.example @@ -277,6 +277,10 @@ PAL24_PAYMENT_DESCRIPTION="Пополнение баланса" PAL24_MIN_AMOUNT_KOPEKS=10000 PAL24_MAX_AMOUNT_KOPEKS=100000000 PAL24_REQUEST_TIMEOUT=30 +# Отображать кнопку СБП в PayPalych (true - отображать, false - скрывать) +PAL24_SBP_BUTTON_VISIBLE=true +# Отображать кнопку оплаты картой в PayPalych (true - отображать, false - скрывать) +PAL24_CARD_BUTTON_VISIBLE=true # ===== ИНТЕРФЕЙС И UX ===== diff --git a/app/config.py b/app/config.py index 3714a3e7..71065597 100644 --- a/app/config.py +++ b/app/config.py @@ -228,6 +228,8 @@ class Settings(BaseSettings): PAL24_REQUEST_TIMEOUT: int = 30 PAL24_SBP_BUTTON_TEXT: Optional[str] = None PAL24_CARD_BUTTON_TEXT: Optional[str] = None + PAL24_SBP_BUTTON_VISIBLE: bool = True + PAL24_CARD_BUTTON_VISIBLE: bool = True MAIN_MENU_MODE: str = "default" CONNECT_BUTTON_MODE: str = "guide" @@ -460,6 +462,12 @@ class Settings(BaseSettings): value = (self.PAL24_CARD_BUTTON_TEXT or "").strip() return value or fallback + def is_pal24_sbp_button_visible(self) -> bool: + return self.PAL24_SBP_BUTTON_VISIBLE + + def is_pal24_card_button_visible(self) -> bool: + return self.PAL24_CARD_BUTTON_VISIBLE + def get_remnawave_user_delete_mode(self) -> str: """Возвращает режим удаления пользователей: 'delete' или 'disable'""" mode = self.REMNAWAVE_USER_DELETE_MODE.lower().strip() diff --git a/app/handlers/balance.py b/app/handlers/balance.py index e28815cf..42012834 100644 --- a/app/handlers/balance.py +++ b/app/handlers/balance.py @@ -403,12 +403,23 @@ async def start_pal24_payment( await callback.answer("❌ Оплата через PayPalych временно недоступна", show_alert=True) return + # Формируем текст сообщения в зависимости от доступных способов оплаты + if settings.is_pal24_sbp_button_visible() and settings.is_pal24_card_button_visible(): + payment_methods_text = "СБП и банковской картой" + elif settings.is_pal24_sbp_button_visible(): + payment_methods_text = "СБП" + elif settings.is_pal24_card_button_visible(): + payment_methods_text = "банковской картой" + else: + # Если обе кнопки отключены, используем общий текст + payment_methods_text = "доступными способами" + message_text = texts.t( "PAL24_TOPUP_PROMPT", ( - "🏦 Оплата через PayPalych (СБП)\n\n" + f"🏦 Оплата через PayPalych ({payment_methods_text})\n\n" "Введите сумму для пополнения от 100 до 1 000 000 ₽.\n" - "Оплата проходит через систему быстрых платежей PayPalych." + f"Оплата проходит через PayPalych ({payment_methods_text})." ), ) @@ -1091,7 +1102,7 @@ async def process_pal24_payment_amount( ) sbp_button_text = settings.get_pal24_sbp_button_text(default_sbp_text) - if sbp_url: + if sbp_url and settings.is_pal24_sbp_button_visible(): pay_buttons.append( [ types.InlineKeyboardButton( @@ -1114,7 +1125,7 @@ async def process_pal24_payment_amount( ) card_button_text = settings.get_pal24_card_button_text(default_card_text) - if card_url and card_url != sbp_url: + if card_url and card_url != sbp_url and settings.is_pal24_card_button_visible(): pay_buttons.append( [ types.InlineKeyboardButton( @@ -1131,7 +1142,7 @@ async def process_pal24_payment_amount( ) step_counter += 1 - if not pay_buttons and fallback_url: + if not pay_buttons and fallback_url and settings.is_pal24_sbp_button_visible(): pay_buttons.append( [ types.InlineKeyboardButton( diff --git a/app/handlers/balance.py.backup2 b/app/handlers/balance.py.backup2 new file mode 100644 index 00000000..1bbcab0f --- /dev/null +++ b/app/handlers/balance.py.backup2 @@ -0,0 +1,1927 @@ +import html +import logging +from aiogram import Dispatcher, types, F +from aiogram.fsm.context import FSMContext +from sqlalchemy.ext.asyncio import AsyncSession + +from app.config import settings +from app.states import BalanceStates +from app.database.crud.user import add_user_balance +from app.database.crud.transaction import ( + get_user_transactions, get_user_transactions_count, + create_transaction +) +from app.database.models import User, TransactionType, PaymentMethod +from app.keyboards.inline import ( + get_balance_keyboard, get_payment_methods_keyboard, + get_back_keyboard, get_pagination_keyboard +) +from app.localization.texts import get_texts +from app.services.payment_service import PaymentService +from app.utils.pagination import paginate_list +from app.utils.decorators import error_handler + +logger = logging.getLogger(__name__) + +TRANSACTIONS_PER_PAGE = 10 + + +def get_quick_amount_buttons(language: str) -> list: + if not settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED or settings.DISABLE_TOPUP_BUTTONS: + return [] + + buttons = [] + periods = settings.get_available_subscription_periods() + + periods = periods[:6] + + for period in periods: + price_attr = f"PRICE_{period}_DAYS" + if hasattr(settings, price_attr): + price_kopeks = getattr(settings, price_attr) + price_rubles = price_kopeks // 100 + + callback_data = f"quick_amount_{price_kopeks}" + + buttons.append( + types.InlineKeyboardButton( + text=f"{price_rubles} ₽ ({period} дней)", + callback_data=callback_data + ) + ) + + keyboard_rows = [] + for i in range(0, len(buttons), 2): + keyboard_rows.append(buttons[i:i + 2]) + + return keyboard_rows + + +@error_handler + + +@error_handler +async def show_balance_menu( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession +): + texts = get_texts(db_user.language) + + balance_text = texts.BALANCE_INFO.format( + balance=texts.format_price(db_user.balance_kopeks) + ) + + await callback.message.edit_text( + balance_text, + reply_markup=get_balance_keyboard(db_user.language) + ) + await callback.answer() + + +@error_handler +async def show_balance_history( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + page: int = 1 +): + texts = get_texts(db_user.language) + + offset = (page - 1) * TRANSACTIONS_PER_PAGE + + raw_transactions = await get_user_transactions( + db, db_user.id, + limit=TRANSACTIONS_PER_PAGE * 3, + offset=offset + ) + + seen_transactions = set() + unique_transactions = [] + + for transaction in raw_transactions: + rounded_time = transaction.created_at.replace(second=0, microsecond=0) + transaction_key = ( + transaction.amount_kopeks, + transaction.description, + rounded_time + ) + + if transaction_key not in seen_transactions: + seen_transactions.add(transaction_key) + unique_transactions.append(transaction) + + if len(unique_transactions) >= TRANSACTIONS_PER_PAGE: + break + + all_transactions = await get_user_transactions(db, db_user.id, limit=1000) + seen_all = set() + total_unique = 0 + + for transaction in all_transactions: + rounded_time = transaction.created_at.replace(second=0, microsecond=0) + transaction_key = ( + transaction.amount_kopeks, + transaction.description, + rounded_time + ) + if transaction_key not in seen_all: + seen_all.add(transaction_key) + total_unique += 1 + + if not unique_transactions: + await callback.message.edit_text( + "📊 История операций пуста", + reply_markup=get_back_keyboard(db_user.language) + ) + await callback.answer() + return + + text = "📊 История операций\n\n" + + for transaction in unique_transactions: + emoji = "💰" if transaction.type == TransactionType.DEPOSIT.value else "💸" + amount_text = f"+{texts.format_price(transaction.amount_kopeks)}" if transaction.type == TransactionType.DEPOSIT.value else f"-{texts.format_price(transaction.amount_kopeks)}" + + text += f"{emoji} {amount_text}\n" + text += f"📝 {transaction.description}\n" + text += f"📅 {transaction.created_at.strftime('%d.%m.%Y %H:%M')}\n\n" + + keyboard = [] + total_pages = (total_unique + TRANSACTIONS_PER_PAGE - 1) // TRANSACTIONS_PER_PAGE + + if total_pages > 1: + pagination_row = get_pagination_keyboard( + page, total_pages, "balance_history", db_user.language + ) + keyboard.extend(pagination_row) + + keyboard.append([ + types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_balance") + ]) + + await callback.message.edit_text( + text, + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard), + parse_mode="HTML" + ) + await callback.answer() + + +@error_handler +async def handle_balance_history_pagination( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession +): + page = int(callback.data.split('_')[-1]) + await show_balance_history(callback, db_user, db, page) + + +@error_handler +async def show_payment_methods( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + from app.utils.payment_utils import get_payment_methods_text + + texts = get_texts(db_user.language) + payment_text = get_payment_methods_text(db_user.language) + + await callback.message.edit_text( + payment_text, + reply_markup=get_payment_methods_keyboard(0, db_user.language), + parse_mode="HTML" + ) + await callback.answer() + + +@error_handler +async def handle_payment_methods_unavailable( + callback: types.CallbackQuery, + db_user: User +): + texts = get_texts(db_user.language) + + await callback.answer( + texts.t( + "PAYMENT_METHODS_UNAVAILABLE_ALERT", + "⚠️ В данный момент автоматические способы оплаты временно недоступны. Для пополнения баланса обратитесь в техподдержку.", + ), + show_alert=True + ) + + +@error_handler +async def start_stars_payment( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.TELEGRAM_STARS_ENABLED: + await callback.answer("❌ Пополнение через Stars временно недоступно", show_alert=True) + return + + # Формируем текст сообщения в зависимости от настройки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + message_text = ( + f"⭐ Пополнение через Telegram Stars\n\n" + f"Выберите сумму пополнения или введите вручную:" + ) + else: + message_text = texts.TOP_UP_AMOUNT + + # Создаем клавиатуру + keyboard = get_back_keyboard(db_user.language) + + # Если включен быстрый выбор суммы и не отключены кнопки, добавляем кнопки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + quick_amount_buttons = get_quick_amount_buttons(db_user.language) + if quick_amount_buttons: + # Вставляем кнопки быстрого выбора перед кнопкой "Назад" + keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard + + await callback.message.edit_text( + message_text, + reply_markup=keyboard + ) + + await state.set_state(BalanceStates.waiting_for_amount) + await state.update_data(payment_method="stars") + await callback.answer() + + +@error_handler +async def start_yookassa_payment( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.is_yookassa_enabled(): + await callback.answer("❌ Оплата картой через YooKassa временно недоступна", show_alert=True) + return + + min_amount_rub = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100 + max_amount_rub = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100 + + # Формируем текст сообщения в зависимости от настройки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + message_text = ( + f"💳 Оплата банковской картой\n\n" + f"Выберите сумму пополнения или введите вручную сумму " + f"от {min_amount_rub:.0f} до {max_amount_rub:,.0f} рублей:" + ) + else: + message_text = ( + f"💳 Оплата банковской картой\n\n" + f"Введите сумму для пополнения от {min_amount_rub:.0f} до {max_amount_rub:,.0f} рублей:" + ) + + # Создаем клавиатуру + keyboard = get_back_keyboard(db_user.language) + + # Если включен быстрый выбор суммы и не отключены кнопки, добавляем кнопки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + quick_amount_buttons = get_quick_amount_buttons(db_user.language) + if quick_amount_buttons: + # Вставляем кнопки быстрого выбора перед кнопкой "Назад" + keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard + + await callback.message.edit_text( + message_text, + reply_markup=keyboard, + parse_mode="HTML" + ) + + await state.set_state(BalanceStates.waiting_for_amount) + await state.update_data(payment_method="yookassa") + await callback.answer() + + +@error_handler +async def start_yookassa_sbp_payment( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.is_yookassa_enabled() or not settings.YOOKASSA_SBP_ENABLED: + await callback.answer("❌ Оплата через СБП временно недоступна", show_alert=True) + return + + min_amount_rub = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100 + max_amount_rub = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100 + + # Формируем текст сообщения в зависимости от настройки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + message_text = ( + f"🏦 Оплата через СБП\n\n" + f"Выберите сумму пополнения или введите вручную сумму " + f"от {min_amount_rub:.0f} до {max_amount_rub:,.0f} рублей:" + ) + else: + message_text = ( + f"🏦 Оплата через СБП\n\n" + f"Введите сумму для пополнения от {min_amount_rub:.0f} до {max_amount_rub:,.0f} рублей:" + ) + + # Создаем клавиатуру + keyboard = get_back_keyboard(db_user.language) + + # Если включен быстрый выбор суммы и не отключены кнопки, добавляем кнопки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + quick_amount_buttons = get_quick_amount_buttons(db_user.language) + if quick_amount_buttons: + # Вставляем кнопки быстрого выбора перед кнопкой "Назад" + keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard + + await callback.message.edit_text( + message_text, + reply_markup=keyboard, + parse_mode="HTML" + ) + + await state.set_state(BalanceStates.waiting_for_amount) + await state.update_data(payment_method="yookassa_sbp") + await callback.answer() + + +@error_handler +async def start_mulenpay_payment( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext, +): + texts = get_texts(db_user.language) + + if not settings.is_mulenpay_enabled(): + await callback.answer("❌ Оплата через Mulen Pay временно недоступна", show_alert=True) + return + + message_text = texts.t( + "MULENPAY_TOPUP_PROMPT", + ( + "💳 Оплата через Mulen Pay\n\n" + "Введите сумму для пополнения от 100 до 100 000 ₽.\n" + "Оплата происходит через защищенную платформу Mulen Pay." + ), + ) + + keyboard = get_back_keyboard(db_user.language) + + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + quick_amount_buttons = get_quick_amount_buttons(db_user.language) + if quick_amount_buttons: + keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard + + await callback.message.edit_text( + message_text, + reply_markup=keyboard, + parse_mode="HTML", + ) + + await state.set_state(BalanceStates.waiting_for_amount) + await state.update_data(payment_method="mulenpay") + await callback.answer() + + +@error_handler +async def start_pal24_payment( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext, +): + texts = get_texts(db_user.language) + + if not settings.is_pal24_enabled(): + await callback.answer("❌ Оплата через PayPalych временно недоступна", show_alert=True) + return + + message_text = texts.t( + "PAL24_TOPUP_PROMPT", + ( + "🏦 Оплата через PayPalych (СБП)\n\n" + "Введите сумму для пополнения от 100 до 1 000 000 ₽.\n" + "Оплата проходит через систему быстрых платежей PayPalych." + ), + ) + + keyboard = get_back_keyboard(db_user.language) + + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + quick_amount_buttons = get_quick_amount_buttons(db_user.language) + if quick_amount_buttons: + keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard + + await callback.message.edit_text( + message_text, + reply_markup=keyboard, + parse_mode="HTML", + ) + + await state.set_state(BalanceStates.waiting_for_amount) + await state.update_data(payment_method="pal24") + await callback.answer() + + +@error_handler +async def start_tribute_payment( + callback: types.CallbackQuery, + db_user: User +): + texts = get_texts(db_user.language) + + if not settings.TRIBUTE_ENABLED: + await callback.answer("❌ Оплата картой временно недоступна", show_alert=True) + return + + try: + from app.services.tribute_service import TributeService + + tribute_service = TributeService(callback.bot) + payment_url = await tribute_service.create_payment_link( + user_id=db_user.telegram_id, + amount_kopeks=0, + description="Пополнение баланса VPN" + ) + + if not payment_url: + await callback.answer("❌ Ошибка создания платежа", show_alert=True) + return + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="💳 Перейти к оплате", url=payment_url)], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] + ]) + + await callback.message.edit_text( + f"💳 Пополнение банковской картой\n\n" + f"• Введите любую сумму от 100₽\n" + f"• Безопасная оплата через Tribute\n" + f"• Мгновенное зачисление на баланс\n" + f"• Принимаем карты Visa, MasterCard, МИР\n\n" + f"• 🚨 НЕ ОТПРАВЛЯТЬ ПЛАТЕЖ АНОНИМНО!\n\n" + f"Нажмите кнопку для перехода к оплате:", + reply_markup=keyboard, + parse_mode="HTML" + ) + + except Exception as e: + logger.error(f"Ошибка создания Tribute платежа: {e}") + await callback.answer("❌ Ошибка создания платежа", show_alert=True) + + await callback.answer() + +async def handle_successful_topup_with_cart( + user_id: int, + amount_kopeks: int, + bot, + db: AsyncSession +): + from app.database.crud.user import get_user_by_id + from aiogram.fsm.context import FSMContext + from aiogram.fsm.storage.base import StorageKey + from app.bot import dp + + user = await get_user_by_id(db, user_id) + if not user: + return + + storage = dp.storage + key = StorageKey(bot_id=bot.id, chat_id=user.telegram_id, user_id=user.telegram_id) + + try: + state_data = await storage.get_data(key) + current_state = await storage.get_state(key) + + if (current_state == "SubscriptionStates:cart_saved_for_topup" and + state_data.get('saved_cart')): + + texts = get_texts(user.language) + total_price = state_data.get('total_price', 0) + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton( + text="🛒 Вернуться к оформлению подписки", + callback_data="return_to_saved_cart" + )], + [types.InlineKeyboardButton( + text="💰 Мой баланс", + callback_data="menu_balance" + )], + [types.InlineKeyboardButton( + text="🏠 Главное меню", + callback_data="back_to_menu" + )] + ]) + + success_text = ( + f"✅ Баланс пополнен на {texts.format_price(amount_kopeks)}!\n\n" + f"💰 Текущий баланс: {texts.format_price(user.balance_kopeks)}\n\n" + f"🛒 У вас есть сохраненная корзина подписки\n" + f"Стоимость: {texts.format_price(total_price)}\n\n" + f"Хотите продолжить оформление?" + ) + + await bot.send_message( + chat_id=user.telegram_id, + text=success_text, + reply_markup=keyboard, + parse_mode="HTML" + ) + + except Exception as e: + logger.error(f"Ошибка обработки успешного пополнения с корзиной: {e}") + +@error_handler +async def request_support_topup( + callback: types.CallbackQuery, + db_user: User +): + texts = get_texts(db_user.language) + + support_text = f""" +🛠️ Пополнение через поддержку + +Для пополнения баланса обратитесь в техподдержку: +{settings.get_support_contact_display_html()} + +Укажите: +• ID: {db_user.telegram_id} +• Сумму пополнения +• Способ оплаты + +⏰ Время обработки: 1-24 часа + +Доступные способы: +• Криптовалюта +• Переводы между банками +• Другие платежные системы +""" + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton( + text="💬 Написать в поддержку", + url=settings.get_support_contact_url() or "https://t.me/" + )], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] + ]) + + await callback.message.edit_text( + support_text, + reply_markup=keyboard, + parse_mode="HTML" + ) + await callback.answer() + + +@error_handler +async def process_topup_amount( + message: types.Message, + db_user: User, + state: FSMContext +): + texts = get_texts(db_user.language) + + try: + if not message.text: + if message.successful_payment: + logger.info( + "Получено сообщение об успешном платеже без текста, " + "обработчик суммы пополнения завершает работу" + ) + await state.clear() + return + + await message.answer( + texts.INVALID_AMOUNT, + reply_markup=get_back_keyboard(db_user.language) + ) + return + + amount_text = message.text.strip() + if not amount_text: + await message.answer( + texts.INVALID_AMOUNT, + reply_markup=get_back_keyboard(db_user.language) + ) + return + + amount_rubles = float(amount_text.replace(',', '.')) + + if amount_rubles < 1: + await message.answer("Минимальная сумма пополнения: 1 ₽") + return + + if amount_rubles > 50000: + await message.answer("Максимальная сумма пополнения: 50,000 ₽") + return + + amount_kopeks = int(amount_rubles * 100) + data = await state.get_data() + payment_method = data.get("payment_method", "stars") + + if payment_method in ["yookassa", "yookassa_sbp"]: + if amount_kopeks < settings.YOOKASSA_MIN_AMOUNT_KOPEKS: + min_rubles = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Минимальная сумма для оплаты через YooKassa: {min_rubles:.0f} ₽") + return + + if amount_kopeks > settings.YOOKASSA_MAX_AMOUNT_KOPEKS: + max_rubles = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Максимальная сумма для оплаты через YooKassa: {max_rubles:,.0f} ₽".replace(',', ' ')) + return + + if payment_method == "stars": + await process_stars_payment_amount(message, db_user, amount_kopeks, state) + elif payment_method == "yookassa": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_yookassa_payment_amount(message, db_user, db, amount_kopeks, state) + elif payment_method == "yookassa_sbp": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_yookassa_sbp_payment_amount(message, db_user, db, amount_kopeks, state) + elif payment_method == "mulenpay": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_mulenpay_payment_amount(message, db_user, db, amount_kopeks, state) + elif payment_method == "pal24": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_pal24_payment_amount(message, db_user, db, amount_kopeks, state) + elif payment_method == "cryptobot": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_cryptobot_payment_amount(message, db_user, db, amount_kopeks, state) + else: + await message.answer("Неизвестный способ оплаты") + + except ValueError: + await message.answer( + texts.INVALID_AMOUNT, + reply_markup=get_back_keyboard(db_user.language) + ) + +@error_handler +async def process_stars_payment_amount( + message: types.Message, + db_user: User, + amount_kopeks: int, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.TELEGRAM_STARS_ENABLED: + await message.answer("⚠️ Оплата Stars временно недоступна") + return + + try: + from app.external.telegram_stars import TelegramStarsService + + amount_rubles = amount_kopeks / 100 + stars_amount = TelegramStarsService.calculate_stars_from_rubles(amount_rubles) + stars_rate = settings.get_stars_rate() + + payment_service = PaymentService(message.bot) + invoice_link = await payment_service.create_stars_invoice( + amount_kopeks=amount_kopeks, + description=f"Пополнение баланса на {texts.format_price(amount_kopeks)}", + payload=f"balance_{db_user.id}_{amount_kopeks}" + ) + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="⭐ Оплатить", url=invoice_link)], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] + ]) + + await message.answer( + f"⭐ Оплата через Telegram Stars\n\n" + f"💰 Сумма: {texts.format_price(amount_kopeks)}\n" + f"⭐ К оплате: {stars_amount} звезд\n" + f"📊 Курс: {stars_rate}₽ за звезду\n\n" + f"Нажмите кнопку ниже для оплаты:", + reply_markup=keyboard, + parse_mode="HTML" + ) + + await state.clear() + + except Exception as e: + logger.error(f"Ошибка создания Stars invoice: {e}") + await message.answer("⚠️ Ошибка создания платежа") + + + +@error_handler +async def process_yookassa_payment_amount( + message: types.Message, + db_user: User, + db: AsyncSession, + amount_kopeks: int, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.is_yookassa_enabled(): + await message.answer("❌ Оплата через YooKassa временно недоступна") + return + + if amount_kopeks < settings.YOOKASSA_MIN_AMOUNT_KOPEKS: + min_rubles = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Минимальная сумма для оплаты картой: {min_rubles:.0f} ₽") + return + + if amount_kopeks > settings.YOOKASSA_MAX_AMOUNT_KOPEKS: + max_rubles = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Максимальная сумма для оплаты картой: {max_rubles:,.0f} ₽".replace(',', ' ')) + return + + try: + payment_service = PaymentService(message.bot) + + payment_result = await payment_service.create_yookassa_payment( + db=db, + user_id=db_user.id, + amount_kopeks=amount_kopeks, + description=settings.get_balance_payment_description(amount_kopeks), + receipt_email=None, + receipt_phone=None, + metadata={ + "user_telegram_id": str(db_user.telegram_id), + "user_username": db_user.username or "", + "purpose": "balance_topup" + } + ) + + if not payment_result: + await message.answer("❌ Ошибка создания платежа. Попробуйте позже или обратитесь в поддержку.") + await state.clear() + return + + confirmation_url = payment_result.get("confirmation_url") + if not confirmation_url: + await message.answer("❌ Ошибка получения ссылки для оплаты. Обратитесь в поддержку.") + await state.clear() + return + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="💳 Оплатить картой", url=confirmation_url)], + [types.InlineKeyboardButton(text="📊 Проверить статус", callback_data=f"check_yookassa_{payment_result['local_payment_id']}")], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] + ]) + + await message.answer( + f"💳 Оплата банковской картой\n\n" + f"💰 Сумма: {settings.format_price(amount_kopeks)}\n" + f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...\n\n" + f"📱 Инструкция:\n" + f"1. Нажмите кнопку 'Оплатить картой'\n" + f"2. Введите данные вашей карты\n" + f"3. Подтвердите платеж\n" + f"4. Деньги поступят на баланс автоматически\n\n" + f"🔒 Оплата происходит через защищенную систему YooKassa\n" + f"✅ Принимаем карты: Visa, MasterCard, МИР\n\n" + f"❓ Если возникнут проблемы, обратитесь в {settings.get_support_contact_display_html()}", + reply_markup=keyboard, + parse_mode="HTML" + ) + + await state.clear() + + logger.info(f"Создан платеж YooKassa для пользователя {db_user.telegram_id}: " + f"{amount_kopeks//100}₽, ID: {payment_result['yookassa_payment_id']}") + + except Exception as e: + logger.error(f"Ошибка создания YooKassa платежа: {e}") + await message.answer("❌ Ошибка создания платежа. Попробуйте позже или обратитесь в поддержку.") + await state.clear() + + +@error_handler +async def process_yookassa_sbp_payment_amount( + message: types.Message, + db_user: User, + db: AsyncSession, + amount_kopeks: int, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.is_yookassa_enabled() or not settings.YOOKASSA_SBP_ENABLED: + await message.answer("❌ Оплата через СБП временно недоступна") + return + + if amount_kopeks < settings.YOOKASSA_MIN_AMOUNT_KOPEKS: + min_rubles = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Минимальная сумма для оплаты через СБП: {min_rubles:.0f} ₽") + return + + if amount_kopeks > settings.YOOKASSA_MAX_AMOUNT_KOPEKS: + max_rubles = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Максимальная сумма для оплаты через СБП: {max_rubles:,.0f} ₽".replace(',', ' ')) + return + + try: + payment_service = PaymentService(message.bot) + + payment_result = await payment_service.create_yookassa_sbp_payment( + db=db, + user_id=db_user.id, + amount_kopeks=amount_kopeks, + description=settings.get_balance_payment_description(amount_kopeks), + receipt_email=None, + receipt_phone=None, + metadata={ + "user_telegram_id": str(db_user.telegram_id), + "user_username": db_user.username or "", + "purpose": "balance_topup_sbp" + } + ) + + if not payment_result: + await message.answer("❌ Ошибка создания платежа через СБП. Попробуйте позже или обратитесь в поддержку.") + await state.clear() + return + + confirmation_url = payment_result.get("confirmation_url") + if not confirmation_url: + await message.answer("❌ Ошибка получения ссылки для оплаты через СБП. Обратитесь в поддержку.") + await state.clear() + return + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="🏦 Оплатить через СБП", url=confirmation_url)], + [types.InlineKeyboardButton(text="📊 Проверить статус", callback_data=f"check_yookassa_{payment_result['local_payment_id']}")], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] + ]) + + await message.answer( + f"🏦 Оплата через СБП\n\n" + f"💰 Сумма: {settings.format_price(amount_kopeks)}\n" + f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...\n\n" + f"📱 Инструкция:\n" + f"1. Нажмите кнопку 'Оплатить через СБП'\n" + f"2. Вас перенаправит в приложение вашего банка\n" + f"3. Подтвердите платеж через СБП\n" + f"4. Деньги поступят на баланс автоматически\n\n" + f"🔒 Оплата происходит через защищенную систему YooKassa\n" + f"✅ Принимаем СБП от всех банков-участников\n\n" + f"❓ Если возникнут проблемы, обратитесь в {settings.get_support_contact_display_html()}", + reply_markup=keyboard, + parse_mode="HTML" + ) + + await state.clear() + + logger.info(f"Создан платеж YooKassa СБП для пользователя {db_user.telegram_id}: " + f"{amount_kopeks//100}₽, ID: {payment_result['yookassa_payment_id']}") + + except Exception as e: + logger.error(f"Ошибка создания YooKassa СБП платежа: {e}") + await message.answer("❌ Ошибка создания платежа через СБП. Попробуйте позже или обратитесь в поддержку.") + await state.clear() + + +@error_handler +async def process_mulenpay_payment_amount( + message: types.Message, + db_user: User, + db: AsyncSession, + amount_kopeks: int, + state: FSMContext, +): + texts = get_texts(db_user.language) + + if not settings.is_mulenpay_enabled(): + await message.answer("❌ Оплата через Mulen Pay временно недоступна") + return + + if amount_kopeks < settings.MULENPAY_MIN_AMOUNT_KOPEKS: + await message.answer( + f"Минимальная сумма пополнения: {settings.format_price(settings.MULENPAY_MIN_AMOUNT_KOPEKS)}" + ) + return + + if amount_kopeks > settings.MULENPAY_MAX_AMOUNT_KOPEKS: + await message.answer( + f"Максимальная сумма пополнения: {settings.format_price(settings.MULENPAY_MAX_AMOUNT_KOPEKS)}" + ) + return + + amount_rubles = amount_kopeks / 100 + + try: + payment_service = PaymentService(message.bot) + payment_result = await payment_service.create_mulenpay_payment( + db=db, + user_id=db_user.id, + amount_kopeks=amount_kopeks, + description=settings.get_balance_payment_description(amount_kopeks), + language=db_user.language, + ) + + if not payment_result or not payment_result.get("payment_url"): + await message.answer( + texts.t( + "MULENPAY_PAYMENT_ERROR", + "❌ Ошибка создания платежа Mulen Pay. Попробуйте позже или обратитесь в поддержку.", + ) + ) + await state.clear() + return + + payment_url = payment_result.get("payment_url") + mulen_payment_id = payment_result.get("mulen_payment_id") + local_payment_id = payment_result.get("local_payment_id") + + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text=texts.t( + "MULENPAY_PAY_BUTTON", + "💳 Оплатить через Mulen Pay", + ), + url=payment_url, + ) + ], + [ + types.InlineKeyboardButton( + text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"), + callback_data=f"check_mulenpay_{local_payment_id}", + ) + ], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")], + ] + ) + + payment_id_display = mulen_payment_id if mulen_payment_id is not None else local_payment_id + + message_template = texts.t( + "MULENPAY_PAYMENT_INSTRUCTIONS", + ( + "💳 Оплата через Mulen Pay\n\n" + "💰 Сумма: {amount}\n" + "🆔 ID платежа: {payment_id}\n\n" + "📱 Инструкция:\n" + "1. Нажмите кнопку ‘Оплатить через Mulen Pay’\n" + "2. Следуйте подсказкам платежной системы\n" + "3. Подтвердите перевод\n" + "4. Средства зачислятся автоматически\n\n" + "❓ Если возникнут проблемы, обратитесь в {support}" + ), + ) + + message_text = message_template.format( + amount=settings.format_price(amount_kopeks), + payment_id=payment_id_display, + support=settings.get_support_contact_display_html(), + ) + + await message.answer( + message_text, + reply_markup=keyboard, + parse_mode="HTML", + ) + + await state.clear() + + logger.info( + "Создан MulenPay платеж для пользователя %s: %s₽, ID: %s", + db_user.telegram_id, + amount_rubles, + payment_id_display, + ) + + except Exception as e: + logger.error(f"Ошибка создания MulenPay платежа: {e}") + await message.answer( + texts.t( + "MULENPAY_PAYMENT_ERROR", + "❌ Ошибка создания платежа Mulen Pay. Попробуйте позже или обратитесь в поддержку.", + ) + ) + await state.clear() + + +@error_handler +async def process_pal24_payment_amount( + message: types.Message, + db_user: User, + db: AsyncSession, + amount_kopeks: int, + state: FSMContext, +): + texts = get_texts(db_user.language) + + if not settings.is_pal24_enabled(): + await message.answer("❌ Оплата через PayPalych временно недоступна") + return + + if amount_kopeks < settings.PAL24_MIN_AMOUNT_KOPEKS: + min_rubles = settings.PAL24_MIN_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Минимальная сумма для оплаты через PayPalych: {min_rubles:.0f} ₽") + return + + if amount_kopeks > settings.PAL24_MAX_AMOUNT_KOPEKS: + max_rubles = settings.PAL24_MAX_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Максимальная сумма для оплаты через PayPalych: {max_rubles:,.0f} ₽".replace(',', ' ')) + return + + try: + payment_service = PaymentService(message.bot) + payment_result = await payment_service.create_pal24_payment( + db=db, + user_id=db_user.id, + amount_kopeks=amount_kopeks, + description=settings.get_balance_payment_description(amount_kopeks), + language=db_user.language, + ) + + if not payment_result: + await message.answer( + texts.t( + "PAL24_PAYMENT_ERROR", + "❌ Ошибка создания платежа PayPalych. Попробуйте позже или обратитесь в поддержку.", + ) + ) + await state.clear() + return + + sbp_url = ( + payment_result.get("sbp_url") + or payment_result.get("transfer_url") + ) + card_url = payment_result.get("card_url") + fallback_url = ( + payment_result.get("link_page_url") + or payment_result.get("link_url") + ) + + if not (sbp_url or card_url or fallback_url): + await message.answer( + texts.t( + "PAL24_PAYMENT_ERROR", + "❌ Ошибка создания платежа PayPalych. Попробуйте позже или обратитесь в поддержку.", + ) + ) + await state.clear() + return + + if not sbp_url: + sbp_url = fallback_url + + bill_id = payment_result.get("bill_id") + local_payment_id = payment_result.get("local_payment_id") + + pay_buttons: list[list[types.InlineKeyboardButton]] = [] + steps: list[str] = [] + step_counter = 1 + + default_sbp_text = texts.t( + "PAL24_SBP_PAY_BUTTON", + "🏦 Оплатить через PayPalych (СБП)", + ) + sbp_button_text = settings.get_pal24_sbp_button_text(default_sbp_text) + + if sbp_url and settings.is_pal24_sbp_button_visible(): + pay_buttons.append( + [ + types.InlineKeyboardButton( + text=sbp_button_text, + url=sbp_url, + ) + ] + ) + steps.append( + texts.t( + "PAL24_INSTRUCTION_BUTTON", + "{step}. Нажмите кнопку «{button}»", + ).format(step=step_counter, button=html.escape(sbp_button_text)) + ) + step_counter += 1 + + default_card_text = texts.t( + "PAL24_CARD_PAY_BUTTON", + "💳 Оплатить банковской картой (PayPalych)", + ) + card_button_text = settings.get_pal24_card_button_text(default_card_text) + + if card_url and card_url != sbp_url and settings.is_pal24_card_button_visible(): + pay_buttons.append( + [ + types.InlineKeyboardButton( + text=card_button_text, + url=card_url, + ) + ] + ) + steps.append( + texts.t( + "PAL24_INSTRUCTION_BUTTON", + "{step}. Нажмите кнопку «{button}»", + ).format(step=step_counter, button=html.escape(card_button_text)) + ) + step_counter += 1 + + if not pay_buttons and fallback_url and settings.is_pal24_sbp_button_visible(): + pay_buttons.append( + [ + types.InlineKeyboardButton( + text=sbp_button_text, + url=fallback_url, + ) + ] + ) + steps.append( + texts.t( + "PAL24_INSTRUCTION_BUTTON", + "{step}. Нажмите кнопку «{button}»", + ).format(step=step_counter, button=html.escape(sbp_button_text)) + ) + step_counter += 1 + + follow_template = texts.t( + "PAL24_INSTRUCTION_FOLLOW", + "{step}. Следуйте подсказкам платёжной системы", + ) + steps.append(follow_template.format(step=step_counter)) + step_counter += 1 + + confirm_template = texts.t( + "PAL24_INSTRUCTION_CONFIRM", + "{step}. Подтвердите перевод", + ) + steps.append(confirm_template.format(step=step_counter)) + step_counter += 1 + + success_template = texts.t( + "PAL24_INSTRUCTION_COMPLETE", + "{step}. Средства зачислятся автоматически", + ) + steps.append(success_template.format(step=step_counter)) + + message_template = texts.t( + "PAL24_PAYMENT_INSTRUCTIONS", + ( + "🏦 Оплата через PayPalych\n\n" + "💰 Сумма: {amount}\n" + "🆔 ID счета: {bill_id}\n\n" + "📱 Инструкция:\n{steps}\n\n" + "❓ Если возникнут проблемы, обратитесь в {support}" + ), + ) + + keyboard_rows = pay_buttons + [ + [ + types.InlineKeyboardButton( + text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"), + callback_data=f"check_pal24_{local_payment_id}", + ) + ], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")], + ] + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows) + + message_text = message_template.format( + amount=settings.format_price(amount_kopeks), + bill_id=bill_id, + steps="\n".join(steps), + support=settings.get_support_contact_display_html(), + ) + + await message.answer( + message_text, + reply_markup=keyboard, + parse_mode="HTML", + ) + + await state.clear() + + logger.info( + "Создан PayPalych счет для пользователя %s: %s₽, ID: %s", + db_user.telegram_id, + amount_kopeks / 100, + bill_id, + ) + + except Exception as e: + logger.error(f"Ошибка создания PayPalych платежа: {e}") + await message.answer( + texts.t( + "PAL24_PAYMENT_ERROR", + "❌ Ошибка создания платежа PayPalych. Попробуйте позже или обратитесь в поддержку.", + ) + ) + await state.clear() + + +@error_handler +async def check_yookassa_payment_status( + callback: types.CallbackQuery, + db: AsyncSession +): + try: + local_payment_id = int(callback.data.split('_')[-1]) + + from app.database.crud.yookassa import get_yookassa_payment_by_local_id + payment = await get_yookassa_payment_by_local_id(db, local_payment_id) + + if not payment: + await callback.answer("❌ Платеж не найден", show_alert=True) + return + + status_emoji = { + "pending": "⏳", + "waiting_for_capture": "⌛", + "succeeded": "✅", + "canceled": "❌", + "failed": "❌" + } + + status_text = { + "pending": "Ожидает оплаты", + "waiting_for_capture": "Ожидает подтверждения", + "succeeded": "Оплачен", + "canceled": "Отменен", + "failed": "Ошибка" + } + + emoji = status_emoji.get(payment.status, "❓") + status = status_text.get(payment.status, "Неизвестно") + + message_text = (f"💳 Статус платежа:\n\n" + f"🆔 ID: {payment.yookassa_payment_id[:8]}...\n" + f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n" + f"📊 Статус: {emoji} {status}\n" + f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M')}\n") + + if payment.is_succeeded: + message_text += "\n✅ Платеж успешно завершен!\n\nСредства зачислены на баланс." + elif payment.is_pending: + message_text += "\n⏳ Платеж ожидает оплаты. Нажмите кнопку 'Оплатить' выше." + elif payment.is_failed: + message_text += ( + f"\n❌ Платеж не прошел. Обратитесь в {settings.get_support_contact_display()}" + ) + + await callback.answer(message_text, show_alert=True) + + except Exception as e: + logger.error(f"Ошибка проверки статуса платежа: {e}") + await callback.answer("❌ Ошибка проверки статуса", show_alert=True) + + +@error_handler +async def check_mulenpay_payment_status( + callback: types.CallbackQuery, + db: AsyncSession +): + try: + local_payment_id = int(callback.data.split('_')[-1]) + payment_service = PaymentService(callback.bot) + status_info = await payment_service.get_mulenpay_payment_status(db, local_payment_id) + + if not status_info: + await callback.answer("❌ Платеж не найден", show_alert=True) + return + + payment = status_info["payment"] + + status_labels = { + "created": ("⏳", "Ожидает оплаты"), + "processing": ("⌛", "Обрабатывается"), + "success": ("✅", "Оплачен"), + "canceled": ("❌", "Отменен"), + "error": ("⚠️", "Ошибка"), + "hold": ("🔒", "Холд"), + "unknown": ("❓", "Неизвестно"), + } + + emoji, status_text = status_labels.get(payment.status, ("❓", "Неизвестно")) + + message_lines = [ + "💳 Статус платежа Mulen Pay:\n\n", + f"🆔 ID: {payment.mulen_payment_id or payment.id}\n", + f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n", + f"📊 Статус: {emoji} {status_text}\n", + f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M')}\n", + ] + + if payment.is_paid: + message_lines.append("\n✅ Платеж успешно завершен! Средства уже на балансе.") + elif payment.status in {"created", "processing"}: + message_lines.append( + "\n⏳ Платеж еще не завершен. Завершите оплату по ссылке и проверьте статус позже." + ) + if payment.payment_url: + message_lines.append(f"\n🔗 Ссылка на оплату: {payment.payment_url}") + elif payment.status in {"canceled", "error"}: + message_lines.append( + f"\n❌ Платеж не был завершен. Попробуйте создать новый платеж или обратитесь в {settings.get_support_contact_display()}" + ) + + message_text = "".join(message_lines) + + if len(message_text) > 190: + await callback.message.answer(message_text) + await callback.answer("ℹ️ Статус платежа отправлен в чат", show_alert=True) + else: + await callback.answer(message_text, show_alert=True) + + except Exception as e: + logger.error(f"Ошибка проверки статуса MulenPay: {e}") + await callback.answer("❌ Ошибка проверки статуса", show_alert=True) + + +@error_handler +async def check_pal24_payment_status( + callback: types.CallbackQuery, + db: AsyncSession, +): + try: + local_payment_id = int(callback.data.split('_')[-1]) + payment_service = PaymentService(callback.bot) + status_info = await payment_service.get_pal24_payment_status(db, local_payment_id) + + if not status_info: + await callback.answer("❌ Платеж не найден", show_alert=True) + return + + payment = status_info["payment"] + + status_labels = { + "NEW": ("⏳", "Ожидает оплаты"), + "PROCESS": ("⌛", "Обрабатывается"), + "SUCCESS": ("✅", "Оплачен"), + "FAIL": ("❌", "Отменен"), + "UNDERPAID": ("⚠️", "Недоплата"), + "OVERPAID": ("⚠️", "Переплата"), + } + + emoji, status_text = status_labels.get(payment.status, ("❓", "Неизвестно")) + + metadata = payment.metadata_json or {} + links_meta = metadata.get("links") if isinstance(metadata, dict) else None + if not isinstance(links_meta, dict): + links_meta = {} + + sbp_link = links_meta.get("sbp") or payment.link_url + card_link = links_meta.get("card") + + if not card_link and payment.link_page_url and payment.link_page_url != sbp_link: + card_link = payment.link_page_url + + message_lines = [ + "🏦 Статус платежа PayPalych:", + "", + f"🆔 ID счета: {payment.bill_id}", + f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}", + f"📊 Статус: {emoji} {status_text}", + f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M')}", + ] + + if payment.is_paid: + message_lines.append("") + message_lines.append("✅ Платеж успешно завершен! Средства уже на балансе.") + elif payment.status in {"NEW", "PROCESS"}: + message_lines.append("") + message_lines.append("⏳ Платеж еще не завершен. Оплатите счет и проверьте статус позже.") + if sbp_link: + message_lines.append("") + message_lines.append(f"🏦 СБП: {sbp_link}") + if card_link and card_link != sbp_link: + message_lines.append(f"💳 Банковская карта: {card_link}") + elif payment.status in {"FAIL", "UNDERPAID", "OVERPAID"}: + message_lines.append("") + message_lines.append( + f"❌ Платеж не завершен корректно. Обратитесь в {settings.get_support_contact_display()}" + ) + + from app.localization.texts import get_texts + db_user = getattr(callback, 'db_user', None) + texts = get_texts(db_user.language if db_user else 'ru') if db_user else get_texts('ru') + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"), callback_data=f"check_pal24_{local_payment_id}")], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] + ]) + + await callback.answer() + await callback.message.edit_text( + "\n".join(message_lines), + reply_markup=keyboard, + disable_web_page_preview=True, + ) + + except Exception as e: + logger.error(f"Ошибка проверки статуса PayPalych: {e}") + await callback.answer("❌ Ошибка проверки статуса", show_alert=True) + + +@error_handler +async def start_cryptobot_payment( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.is_cryptobot_enabled(): + await callback.answer("❌ Оплата криптовалютой временно недоступна", show_alert=True) + return + + from app.utils.currency_converter import currency_converter + try: + current_rate = await currency_converter.get_usd_to_rub_rate() + rate_text = f"💱 Текущий курс: 1 USD = {current_rate:.2f} ₽" + except Exception as e: + logger.warning(f"Не удалось получить курс валют: {e}") + current_rate = 95.0 + rate_text = f"💱 Курс: 1 USD ≈ {current_rate:.0f} ₽" + + available_assets = settings.get_cryptobot_assets() + assets_text = ", ".join(available_assets) + + # Формируем текст сообщения в зависимости от настройки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + message_text = ( + f"🪙 Пополнение криптовалютой\n\n" + f"Выберите сумму пополнения или введите вручную сумму " + f"от 100 до 100,000 ₽:\n\n" + f"💰 Доступные активы: {assets_text}\n" + f"⚡ Мгновенное зачисление на баланс\n" + f"🔒 Безопасная оплата через CryptoBot\n\n" + f"{rate_text}\n" + f"Сумма будет автоматически конвертирована в USD для оплаты." + ) + else: + message_text = ( + f"🪙 Пополнение криптовалютой\n\n" + f"Введите сумму для пополнения от 100 до 100,000 ₽:\n\n" + f"💰 Доступные активы: {assets_text}\n" + f"⚡ Мгновенное зачисление на баланс\n" + f"🔒 Безопасная оплата через CryptoBot\n\n" + f"{rate_text}\n" + f"Сумма будет автоматически конвертирована в USD для оплаты." + ) + + # Создаем клавиатуру + keyboard = get_back_keyboard(db_user.language) + + # Если включен быстрый выбор суммы и не отключены кнопки, добавляем кнопки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + quick_amount_buttons = get_quick_amount_buttons(db_user.language) + if quick_amount_buttons: + # Вставляем кнопки быстрого выбора перед кнопкой "Назад" + keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard + + await callback.message.edit_text( + message_text, + reply_markup=keyboard, + parse_mode="HTML" + ) + + await state.set_state(BalanceStates.waiting_for_amount) + await state.update_data(payment_method="cryptobot", current_rate=current_rate) + await callback.answer() + +@error_handler +async def process_cryptobot_payment_amount( + message: types.Message, + db_user: User, + db: AsyncSession, + amount_kopeks: int, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.is_cryptobot_enabled(): + await message.answer("❌ Оплата криптовалютой временно недоступна") + return + + amount_rubles = amount_kopeks / 100 + + if amount_rubles < 100: + await message.answer("Минимальная сумма пополнения: 100 ₽") + return + + if amount_rubles > 100000: + await message.answer("Максимальная сумма пополнения: 100,000 ₽") + return + + try: + data = await state.get_data() + current_rate = data.get('current_rate') + + if not current_rate: + from app.utils.currency_converter import currency_converter + current_rate = await currency_converter.get_usd_to_rub_rate() + + amount_usd = amount_rubles / current_rate + + amount_usd = round(amount_usd, 2) + + if amount_usd < 1: + await message.answer("❌ Минимальная сумма для оплаты в USD: 1.00 USD") + return + + if amount_usd > 1000: + await message.answer("❌ Максимальная сумма для оплаты в USD: 1,000 USD") + return + + payment_service = PaymentService(message.bot) + + payment_result = await payment_service.create_cryptobot_payment( + db=db, + user_id=db_user.id, + amount_usd=amount_usd, + asset=settings.CRYPTOBOT_DEFAULT_ASSET, + description=f"Пополнение баланса на {amount_rubles:.0f} ₽ ({amount_usd:.2f} USD)", + payload=f"balance_{db_user.id}_{amount_kopeks}" + ) + + if not payment_result: + await message.answer("❌ Ошибка создания платежа. Попробуйте позже или обратитесь в поддержку.") + await state.clear() + return + + bot_invoice_url = payment_result.get("bot_invoice_url") + mini_app_invoice_url = payment_result.get("mini_app_invoice_url") + + payment_url = bot_invoice_url or mini_app_invoice_url + + if not payment_url: + await message.answer("❌ Ошибка получения ссылки для оплаты. Обратитесь в поддержку.") + await state.clear() + return + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="🪙 Оплатить", url=payment_url)], + [types.InlineKeyboardButton(text="📊 Проверить статус", callback_data=f"check_cryptobot_{payment_result['local_payment_id']}")], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] + ]) + + await message.answer( + f"🪙 Оплата криптовалютой\n\n" + f"💰 Сумма к зачислению: {amount_rubles:.0f} ₽\n" + f"💵 К оплате: {amount_usd:.2f} USD\n" + f"🪙 Актив: {payment_result['asset']}\n" + f"💱 Курс: 1 USD = {current_rate:.2f} ₽\n" + f"🆔 ID платежа: {payment_result['invoice_id'][:8]}...\n\n" + f"📱 Инструкция:\n" + f"1. Нажмите кнопку 'Оплатить'\n" + f"2. Выберите удобный актив\n" + f"3. Переведите указанную сумму\n" + f"4. Деньги поступят на баланс автоматически\n\n" + f"🔒 Оплата проходит через защищенную систему CryptoBot\n" + f"⚡ Поддерживаемые активы: USDT, TON, BTC, ETH\n\n" + f"❓ Если возникнут проблемы, обратитесь в {settings.get_support_contact_display_html()}", + reply_markup=keyboard, + parse_mode="HTML" + ) + + await state.clear() + + logger.info(f"Создан CryptoBot платеж для пользователя {db_user.telegram_id}: " + f"{amount_rubles:.0f} ₽ ({amount_usd:.2f} USD), ID: {payment_result['invoice_id']}") + + except Exception as e: + logger.error(f"Ошибка создания CryptoBot платежа: {e}") + await message.answer("❌ Ошибка создания платежа. Попробуйте позже или обратитесь в поддержку.") + await state.clear() + +@error_handler +async def check_cryptobot_payment_status( + callback: types.CallbackQuery, + db: AsyncSession +): + try: + local_payment_id = int(callback.data.split('_')[-1]) + + from app.database.crud.cryptobot import get_cryptobot_payment_by_id + payment = await get_cryptobot_payment_by_id(db, local_payment_id) + + if not payment: + await callback.answer("❌ Платеж не найден", show_alert=True) + return + + status_emoji = { + "active": "⏳", + "paid": "✅", + "expired": "❌" + } + + status_text = { + "active": "Ожидает оплаты", + "paid": "Оплачен", + "expired": "Истек" + } + + emoji = status_emoji.get(payment.status, "❓") + status = status_text.get(payment.status, "Неизвестно") + + message_text = (f"🪙 Статус платежа:\n\n" + f"🆔 ID: {payment.invoice_id[:8]}...\n" + f"💰 Сумма: {payment.amount} {payment.asset}\n" + f"📊 Статус: {emoji} {status}\n" + f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M')}\n") + + if payment.is_paid: + message_text += "\n✅ Платеж успешно завершен!\n\nСредства зачислены на баланс." + elif payment.is_pending: + message_text += "\n⏳ Платеж ожидает оплаты. Нажмите кнопку 'Оплатить' выше." + elif payment.is_expired: + message_text += ( + f"\n❌ Платеж истек. Обратитесь в {settings.get_support_contact_display()}" + ) + + await callback.answer(message_text, show_alert=True) + + except Exception as e: + logger.error(f"Ошибка проверки статуса CryptoBot платежа: {e}") + await callback.answer("❌ Ошибка проверки статуса", show_alert=True) + + + +@error_handler +async def handle_sbp_payment( + callback: types.CallbackQuery, + db: AsyncSession +): + try: + local_payment_id = int(callback.data.split('_')[-1]) + + from app.database.crud.yookassa import get_yookassa_payment_by_local_id + payment = await get_yookassa_payment_by_local_id(db, local_payment_id) + + if not payment: + await callback.answer("❌ Платеж не найден", show_alert=True) + return + + import json + metadata = json.loads(payment.metadata_json) if payment.metadata_json else {} + confirmation_token = metadata.get("confirmation_token") + + if not confirmation_token: + await callback.answer("❌ Токен подтверждения не найден", show_alert=True) + return + + await callback.message.answer( + f"Для оплаты через СБП откройте приложение вашего банка и подтвердите платеж.\\n\\n" + f"Если у вас не открылось банковское приложение автоматически, вы можете:\\n" + f"1. Скопировать этот токен: {confirmation_token}\\n" + f"2. Открыть приложение вашего банка\\n" + f"3. Найти функцию оплаты по токену\\n" + f"4. Вставить токен и подтвердить платеж", + parse_mode="HTML" + ) + + await callback.answer("Информация об оплате отправлена", show_alert=True) + + except Exception as e: + logger.error(f"Ошибка обработки embedded платежа СБП: {e}") + await callback.answer("❌ Ошибка обработки платежа", show_alert=True) + + + +@error_handler +async def handle_quick_amount_selection( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + """ + Обработчик выбора суммы через кнопки быстрого выбора + """ + # Извлекаем сумму из callback_data + try: + amount_kopeks = int(callback.data.split('_')[-1]) + amount_rubles = amount_kopeks / 100 + + # Получаем метод оплаты из состояния + data = await state.get_data() + payment_method = data.get("payment_method", "yookassa") + + # Проверяем, какой метод оплаты был выбран и вызываем соответствующий обработчик + if payment_method == "yookassa": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_yookassa_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif payment_method == "yookassa_sbp": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_yookassa_sbp_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif payment_method == "mulenpay": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_mulenpay_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif payment_method == "pal24": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_pal24_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + else: + await callback.answer("❌ Неизвестный способ оплаты", show_alert=True) + return + + except ValueError: + await callback.answer("❌ Ошибка обработки суммы", show_alert=True) + except Exception as e: + logger.error(f"Ошибка обработки быстрого выбора суммы: {e}") + await callback.answer("❌ Ошибка обработки запроса", show_alert=True) + + +@error_handler +async def handle_topup_amount_callback( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext, +): + try: + _, method, amount_str = callback.data.split("|", 2) + amount_kopeks = int(amount_str) + except ValueError: + await callback.answer("❌ Некорректный запрос", show_alert=True) + return + + if amount_kopeks <= 0: + await callback.answer("❌ Некорректная сумма", show_alert=True) + return + + try: + if method == "yookassa": + from app.database.database import AsyncSessionLocal + + async with AsyncSessionLocal() as db: + await process_yookassa_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif method == "yookassa_sbp": + from app.database.database import AsyncSessionLocal + + async with AsyncSessionLocal() as db: + await process_yookassa_sbp_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif method == "mulenpay": + from app.database.database import AsyncSessionLocal + + async with AsyncSessionLocal() as db: + await process_mulenpay_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif method == "pal24": + from app.database.database import AsyncSessionLocal + + async with AsyncSessionLocal() as db: + await process_pal24_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif method == "cryptobot": + from app.database.database import AsyncSessionLocal + + async with AsyncSessionLocal() as db: + await process_cryptobot_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif method == "stars": + await process_stars_payment_amount( + callback.message, db_user, amount_kopeks, state + ) + elif method == "tribute": + await start_tribute_payment(callback, db_user) + return + else: + await callback.answer("❌ Неизвестный способ оплаты", show_alert=True) + return + + await callback.answer() + + except Exception as error: + logger.error(f"Ошибка быстрого пополнения: {error}") + await callback.answer("❌ Ошибка обработки запроса", show_alert=True) + + +def register_handlers(dp: Dispatcher): + + dp.callback_query.register( + show_balance_menu, + F.data == "menu_balance" + ) + + dp.callback_query.register( + show_balance_history, + F.data == "balance_history" + ) + + dp.callback_query.register( + handle_balance_history_pagination, + F.data.startswith("balance_history_page_") + ) + + dp.callback_query.register( + show_payment_methods, + F.data == "balance_topup" + ) + + dp.callback_query.register( + start_stars_payment, + F.data == "topup_stars" + ) + + dp.callback_query.register( + start_yookassa_payment, + F.data == "topup_yookassa" + ) + + dp.callback_query.register( + start_yookassa_sbp_payment, + F.data == "topup_yookassa_sbp" + ) + + dp.callback_query.register( + start_mulenpay_payment, + F.data == "topup_mulenpay" + ) + + dp.callback_query.register( + start_pal24_payment, + F.data == "topup_pal24" + ) + + dp.callback_query.register( + check_yookassa_payment_status, + F.data.startswith("check_yookassa_") + ) + + dp.callback_query.register( + start_tribute_payment, + F.data == "topup_tribute" + ) + + dp.callback_query.register( + request_support_topup, + F.data == "topup_support" + ) + + dp.callback_query.register( + check_yookassa_payment_status, + F.data.startswith("check_yookassa_") + ) + + dp.message.register( + process_topup_amount, + BalanceStates.waiting_for_amount + ) + + dp.callback_query.register( + start_cryptobot_payment, + F.data == "topup_cryptobot" + ) + + dp.callback_query.register( + check_cryptobot_payment_status, + F.data.startswith("check_cryptobot_") + ) + + dp.callback_query.register( + check_mulenpay_payment_status, + F.data.startswith("check_mulenpay_") + ) + + dp.callback_query.register( + check_pal24_payment_status, + F.data.startswith("check_pal24_") + ) + + dp.callback_query.register( + handle_payment_methods_unavailable, + F.data == "payment_methods_unavailable" + ) + + # Регистрируем обработчик для кнопок быстрого выбора суммы + dp.callback_query.register( + handle_quick_amount_selection, + F.data.startswith("quick_amount_") + ) + + dp.callback_query.register( + handle_topup_amount_callback, + F.data.startswith("topup_amount|") + ) diff --git a/app/handlers/balance.py.bak2 b/app/handlers/balance.py.bak2 new file mode 100644 index 00000000..1bbcab0f --- /dev/null +++ b/app/handlers/balance.py.bak2 @@ -0,0 +1,1927 @@ +import html +import logging +from aiogram import Dispatcher, types, F +from aiogram.fsm.context import FSMContext +from sqlalchemy.ext.asyncio import AsyncSession + +from app.config import settings +from app.states import BalanceStates +from app.database.crud.user import add_user_balance +from app.database.crud.transaction import ( + get_user_transactions, get_user_transactions_count, + create_transaction +) +from app.database.models import User, TransactionType, PaymentMethod +from app.keyboards.inline import ( + get_balance_keyboard, get_payment_methods_keyboard, + get_back_keyboard, get_pagination_keyboard +) +from app.localization.texts import get_texts +from app.services.payment_service import PaymentService +from app.utils.pagination import paginate_list +from app.utils.decorators import error_handler + +logger = logging.getLogger(__name__) + +TRANSACTIONS_PER_PAGE = 10 + + +def get_quick_amount_buttons(language: str) -> list: + if not settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED or settings.DISABLE_TOPUP_BUTTONS: + return [] + + buttons = [] + periods = settings.get_available_subscription_periods() + + periods = periods[:6] + + for period in periods: + price_attr = f"PRICE_{period}_DAYS" + if hasattr(settings, price_attr): + price_kopeks = getattr(settings, price_attr) + price_rubles = price_kopeks // 100 + + callback_data = f"quick_amount_{price_kopeks}" + + buttons.append( + types.InlineKeyboardButton( + text=f"{price_rubles} ₽ ({period} дней)", + callback_data=callback_data + ) + ) + + keyboard_rows = [] + for i in range(0, len(buttons), 2): + keyboard_rows.append(buttons[i:i + 2]) + + return keyboard_rows + + +@error_handler + + +@error_handler +async def show_balance_menu( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession +): + texts = get_texts(db_user.language) + + balance_text = texts.BALANCE_INFO.format( + balance=texts.format_price(db_user.balance_kopeks) + ) + + await callback.message.edit_text( + balance_text, + reply_markup=get_balance_keyboard(db_user.language) + ) + await callback.answer() + + +@error_handler +async def show_balance_history( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + page: int = 1 +): + texts = get_texts(db_user.language) + + offset = (page - 1) * TRANSACTIONS_PER_PAGE + + raw_transactions = await get_user_transactions( + db, db_user.id, + limit=TRANSACTIONS_PER_PAGE * 3, + offset=offset + ) + + seen_transactions = set() + unique_transactions = [] + + for transaction in raw_transactions: + rounded_time = transaction.created_at.replace(second=0, microsecond=0) + transaction_key = ( + transaction.amount_kopeks, + transaction.description, + rounded_time + ) + + if transaction_key not in seen_transactions: + seen_transactions.add(transaction_key) + unique_transactions.append(transaction) + + if len(unique_transactions) >= TRANSACTIONS_PER_PAGE: + break + + all_transactions = await get_user_transactions(db, db_user.id, limit=1000) + seen_all = set() + total_unique = 0 + + for transaction in all_transactions: + rounded_time = transaction.created_at.replace(second=0, microsecond=0) + transaction_key = ( + transaction.amount_kopeks, + transaction.description, + rounded_time + ) + if transaction_key not in seen_all: + seen_all.add(transaction_key) + total_unique += 1 + + if not unique_transactions: + await callback.message.edit_text( + "📊 История операций пуста", + reply_markup=get_back_keyboard(db_user.language) + ) + await callback.answer() + return + + text = "📊 История операций\n\n" + + for transaction in unique_transactions: + emoji = "💰" if transaction.type == TransactionType.DEPOSIT.value else "💸" + amount_text = f"+{texts.format_price(transaction.amount_kopeks)}" if transaction.type == TransactionType.DEPOSIT.value else f"-{texts.format_price(transaction.amount_kopeks)}" + + text += f"{emoji} {amount_text}\n" + text += f"📝 {transaction.description}\n" + text += f"📅 {transaction.created_at.strftime('%d.%m.%Y %H:%M')}\n\n" + + keyboard = [] + total_pages = (total_unique + TRANSACTIONS_PER_PAGE - 1) // TRANSACTIONS_PER_PAGE + + if total_pages > 1: + pagination_row = get_pagination_keyboard( + page, total_pages, "balance_history", db_user.language + ) + keyboard.extend(pagination_row) + + keyboard.append([ + types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_balance") + ]) + + await callback.message.edit_text( + text, + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard), + parse_mode="HTML" + ) + await callback.answer() + + +@error_handler +async def handle_balance_history_pagination( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession +): + page = int(callback.data.split('_')[-1]) + await show_balance_history(callback, db_user, db, page) + + +@error_handler +async def show_payment_methods( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + from app.utils.payment_utils import get_payment_methods_text + + texts = get_texts(db_user.language) + payment_text = get_payment_methods_text(db_user.language) + + await callback.message.edit_text( + payment_text, + reply_markup=get_payment_methods_keyboard(0, db_user.language), + parse_mode="HTML" + ) + await callback.answer() + + +@error_handler +async def handle_payment_methods_unavailable( + callback: types.CallbackQuery, + db_user: User +): + texts = get_texts(db_user.language) + + await callback.answer( + texts.t( + "PAYMENT_METHODS_UNAVAILABLE_ALERT", + "⚠️ В данный момент автоматические способы оплаты временно недоступны. Для пополнения баланса обратитесь в техподдержку.", + ), + show_alert=True + ) + + +@error_handler +async def start_stars_payment( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.TELEGRAM_STARS_ENABLED: + await callback.answer("❌ Пополнение через Stars временно недоступно", show_alert=True) + return + + # Формируем текст сообщения в зависимости от настройки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + message_text = ( + f"⭐ Пополнение через Telegram Stars\n\n" + f"Выберите сумму пополнения или введите вручную:" + ) + else: + message_text = texts.TOP_UP_AMOUNT + + # Создаем клавиатуру + keyboard = get_back_keyboard(db_user.language) + + # Если включен быстрый выбор суммы и не отключены кнопки, добавляем кнопки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + quick_amount_buttons = get_quick_amount_buttons(db_user.language) + if quick_amount_buttons: + # Вставляем кнопки быстрого выбора перед кнопкой "Назад" + keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard + + await callback.message.edit_text( + message_text, + reply_markup=keyboard + ) + + await state.set_state(BalanceStates.waiting_for_amount) + await state.update_data(payment_method="stars") + await callback.answer() + + +@error_handler +async def start_yookassa_payment( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.is_yookassa_enabled(): + await callback.answer("❌ Оплата картой через YooKassa временно недоступна", show_alert=True) + return + + min_amount_rub = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100 + max_amount_rub = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100 + + # Формируем текст сообщения в зависимости от настройки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + message_text = ( + f"💳 Оплата банковской картой\n\n" + f"Выберите сумму пополнения или введите вручную сумму " + f"от {min_amount_rub:.0f} до {max_amount_rub:,.0f} рублей:" + ) + else: + message_text = ( + f"💳 Оплата банковской картой\n\n" + f"Введите сумму для пополнения от {min_amount_rub:.0f} до {max_amount_rub:,.0f} рублей:" + ) + + # Создаем клавиатуру + keyboard = get_back_keyboard(db_user.language) + + # Если включен быстрый выбор суммы и не отключены кнопки, добавляем кнопки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + quick_amount_buttons = get_quick_amount_buttons(db_user.language) + if quick_amount_buttons: + # Вставляем кнопки быстрого выбора перед кнопкой "Назад" + keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard + + await callback.message.edit_text( + message_text, + reply_markup=keyboard, + parse_mode="HTML" + ) + + await state.set_state(BalanceStates.waiting_for_amount) + await state.update_data(payment_method="yookassa") + await callback.answer() + + +@error_handler +async def start_yookassa_sbp_payment( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.is_yookassa_enabled() or not settings.YOOKASSA_SBP_ENABLED: + await callback.answer("❌ Оплата через СБП временно недоступна", show_alert=True) + return + + min_amount_rub = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100 + max_amount_rub = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100 + + # Формируем текст сообщения в зависимости от настройки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + message_text = ( + f"🏦 Оплата через СБП\n\n" + f"Выберите сумму пополнения или введите вручную сумму " + f"от {min_amount_rub:.0f} до {max_amount_rub:,.0f} рублей:" + ) + else: + message_text = ( + f"🏦 Оплата через СБП\n\n" + f"Введите сумму для пополнения от {min_amount_rub:.0f} до {max_amount_rub:,.0f} рублей:" + ) + + # Создаем клавиатуру + keyboard = get_back_keyboard(db_user.language) + + # Если включен быстрый выбор суммы и не отключены кнопки, добавляем кнопки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + quick_amount_buttons = get_quick_amount_buttons(db_user.language) + if quick_amount_buttons: + # Вставляем кнопки быстрого выбора перед кнопкой "Назад" + keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard + + await callback.message.edit_text( + message_text, + reply_markup=keyboard, + parse_mode="HTML" + ) + + await state.set_state(BalanceStates.waiting_for_amount) + await state.update_data(payment_method="yookassa_sbp") + await callback.answer() + + +@error_handler +async def start_mulenpay_payment( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext, +): + texts = get_texts(db_user.language) + + if not settings.is_mulenpay_enabled(): + await callback.answer("❌ Оплата через Mulen Pay временно недоступна", show_alert=True) + return + + message_text = texts.t( + "MULENPAY_TOPUP_PROMPT", + ( + "💳 Оплата через Mulen Pay\n\n" + "Введите сумму для пополнения от 100 до 100 000 ₽.\n" + "Оплата происходит через защищенную платформу Mulen Pay." + ), + ) + + keyboard = get_back_keyboard(db_user.language) + + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + quick_amount_buttons = get_quick_amount_buttons(db_user.language) + if quick_amount_buttons: + keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard + + await callback.message.edit_text( + message_text, + reply_markup=keyboard, + parse_mode="HTML", + ) + + await state.set_state(BalanceStates.waiting_for_amount) + await state.update_data(payment_method="mulenpay") + await callback.answer() + + +@error_handler +async def start_pal24_payment( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext, +): + texts = get_texts(db_user.language) + + if not settings.is_pal24_enabled(): + await callback.answer("❌ Оплата через PayPalych временно недоступна", show_alert=True) + return + + message_text = texts.t( + "PAL24_TOPUP_PROMPT", + ( + "🏦 Оплата через PayPalych (СБП)\n\n" + "Введите сумму для пополнения от 100 до 1 000 000 ₽.\n" + "Оплата проходит через систему быстрых платежей PayPalych." + ), + ) + + keyboard = get_back_keyboard(db_user.language) + + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + quick_amount_buttons = get_quick_amount_buttons(db_user.language) + if quick_amount_buttons: + keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard + + await callback.message.edit_text( + message_text, + reply_markup=keyboard, + parse_mode="HTML", + ) + + await state.set_state(BalanceStates.waiting_for_amount) + await state.update_data(payment_method="pal24") + await callback.answer() + + +@error_handler +async def start_tribute_payment( + callback: types.CallbackQuery, + db_user: User +): + texts = get_texts(db_user.language) + + if not settings.TRIBUTE_ENABLED: + await callback.answer("❌ Оплата картой временно недоступна", show_alert=True) + return + + try: + from app.services.tribute_service import TributeService + + tribute_service = TributeService(callback.bot) + payment_url = await tribute_service.create_payment_link( + user_id=db_user.telegram_id, + amount_kopeks=0, + description="Пополнение баланса VPN" + ) + + if not payment_url: + await callback.answer("❌ Ошибка создания платежа", show_alert=True) + return + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="💳 Перейти к оплате", url=payment_url)], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] + ]) + + await callback.message.edit_text( + f"💳 Пополнение банковской картой\n\n" + f"• Введите любую сумму от 100₽\n" + f"• Безопасная оплата через Tribute\n" + f"• Мгновенное зачисление на баланс\n" + f"• Принимаем карты Visa, MasterCard, МИР\n\n" + f"• 🚨 НЕ ОТПРАВЛЯТЬ ПЛАТЕЖ АНОНИМНО!\n\n" + f"Нажмите кнопку для перехода к оплате:", + reply_markup=keyboard, + parse_mode="HTML" + ) + + except Exception as e: + logger.error(f"Ошибка создания Tribute платежа: {e}") + await callback.answer("❌ Ошибка создания платежа", show_alert=True) + + await callback.answer() + +async def handle_successful_topup_with_cart( + user_id: int, + amount_kopeks: int, + bot, + db: AsyncSession +): + from app.database.crud.user import get_user_by_id + from aiogram.fsm.context import FSMContext + from aiogram.fsm.storage.base import StorageKey + from app.bot import dp + + user = await get_user_by_id(db, user_id) + if not user: + return + + storage = dp.storage + key = StorageKey(bot_id=bot.id, chat_id=user.telegram_id, user_id=user.telegram_id) + + try: + state_data = await storage.get_data(key) + current_state = await storage.get_state(key) + + if (current_state == "SubscriptionStates:cart_saved_for_topup" and + state_data.get('saved_cart')): + + texts = get_texts(user.language) + total_price = state_data.get('total_price', 0) + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton( + text="🛒 Вернуться к оформлению подписки", + callback_data="return_to_saved_cart" + )], + [types.InlineKeyboardButton( + text="💰 Мой баланс", + callback_data="menu_balance" + )], + [types.InlineKeyboardButton( + text="🏠 Главное меню", + callback_data="back_to_menu" + )] + ]) + + success_text = ( + f"✅ Баланс пополнен на {texts.format_price(amount_kopeks)}!\n\n" + f"💰 Текущий баланс: {texts.format_price(user.balance_kopeks)}\n\n" + f"🛒 У вас есть сохраненная корзина подписки\n" + f"Стоимость: {texts.format_price(total_price)}\n\n" + f"Хотите продолжить оформление?" + ) + + await bot.send_message( + chat_id=user.telegram_id, + text=success_text, + reply_markup=keyboard, + parse_mode="HTML" + ) + + except Exception as e: + logger.error(f"Ошибка обработки успешного пополнения с корзиной: {e}") + +@error_handler +async def request_support_topup( + callback: types.CallbackQuery, + db_user: User +): + texts = get_texts(db_user.language) + + support_text = f""" +🛠️ Пополнение через поддержку + +Для пополнения баланса обратитесь в техподдержку: +{settings.get_support_contact_display_html()} + +Укажите: +• ID: {db_user.telegram_id} +• Сумму пополнения +• Способ оплаты + +⏰ Время обработки: 1-24 часа + +Доступные способы: +• Криптовалюта +• Переводы между банками +• Другие платежные системы +""" + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton( + text="💬 Написать в поддержку", + url=settings.get_support_contact_url() or "https://t.me/" + )], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] + ]) + + await callback.message.edit_text( + support_text, + reply_markup=keyboard, + parse_mode="HTML" + ) + await callback.answer() + + +@error_handler +async def process_topup_amount( + message: types.Message, + db_user: User, + state: FSMContext +): + texts = get_texts(db_user.language) + + try: + if not message.text: + if message.successful_payment: + logger.info( + "Получено сообщение об успешном платеже без текста, " + "обработчик суммы пополнения завершает работу" + ) + await state.clear() + return + + await message.answer( + texts.INVALID_AMOUNT, + reply_markup=get_back_keyboard(db_user.language) + ) + return + + amount_text = message.text.strip() + if not amount_text: + await message.answer( + texts.INVALID_AMOUNT, + reply_markup=get_back_keyboard(db_user.language) + ) + return + + amount_rubles = float(amount_text.replace(',', '.')) + + if amount_rubles < 1: + await message.answer("Минимальная сумма пополнения: 1 ₽") + return + + if amount_rubles > 50000: + await message.answer("Максимальная сумма пополнения: 50,000 ₽") + return + + amount_kopeks = int(amount_rubles * 100) + data = await state.get_data() + payment_method = data.get("payment_method", "stars") + + if payment_method in ["yookassa", "yookassa_sbp"]: + if amount_kopeks < settings.YOOKASSA_MIN_AMOUNT_KOPEKS: + min_rubles = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Минимальная сумма для оплаты через YooKassa: {min_rubles:.0f} ₽") + return + + if amount_kopeks > settings.YOOKASSA_MAX_AMOUNT_KOPEKS: + max_rubles = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Максимальная сумма для оплаты через YooKassa: {max_rubles:,.0f} ₽".replace(',', ' ')) + return + + if payment_method == "stars": + await process_stars_payment_amount(message, db_user, amount_kopeks, state) + elif payment_method == "yookassa": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_yookassa_payment_amount(message, db_user, db, amount_kopeks, state) + elif payment_method == "yookassa_sbp": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_yookassa_sbp_payment_amount(message, db_user, db, amount_kopeks, state) + elif payment_method == "mulenpay": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_mulenpay_payment_amount(message, db_user, db, amount_kopeks, state) + elif payment_method == "pal24": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_pal24_payment_amount(message, db_user, db, amount_kopeks, state) + elif payment_method == "cryptobot": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_cryptobot_payment_amount(message, db_user, db, amount_kopeks, state) + else: + await message.answer("Неизвестный способ оплаты") + + except ValueError: + await message.answer( + texts.INVALID_AMOUNT, + reply_markup=get_back_keyboard(db_user.language) + ) + +@error_handler +async def process_stars_payment_amount( + message: types.Message, + db_user: User, + amount_kopeks: int, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.TELEGRAM_STARS_ENABLED: + await message.answer("⚠️ Оплата Stars временно недоступна") + return + + try: + from app.external.telegram_stars import TelegramStarsService + + amount_rubles = amount_kopeks / 100 + stars_amount = TelegramStarsService.calculate_stars_from_rubles(amount_rubles) + stars_rate = settings.get_stars_rate() + + payment_service = PaymentService(message.bot) + invoice_link = await payment_service.create_stars_invoice( + amount_kopeks=amount_kopeks, + description=f"Пополнение баланса на {texts.format_price(amount_kopeks)}", + payload=f"balance_{db_user.id}_{amount_kopeks}" + ) + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="⭐ Оплатить", url=invoice_link)], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] + ]) + + await message.answer( + f"⭐ Оплата через Telegram Stars\n\n" + f"💰 Сумма: {texts.format_price(amount_kopeks)}\n" + f"⭐ К оплате: {stars_amount} звезд\n" + f"📊 Курс: {stars_rate}₽ за звезду\n\n" + f"Нажмите кнопку ниже для оплаты:", + reply_markup=keyboard, + parse_mode="HTML" + ) + + await state.clear() + + except Exception as e: + logger.error(f"Ошибка создания Stars invoice: {e}") + await message.answer("⚠️ Ошибка создания платежа") + + + +@error_handler +async def process_yookassa_payment_amount( + message: types.Message, + db_user: User, + db: AsyncSession, + amount_kopeks: int, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.is_yookassa_enabled(): + await message.answer("❌ Оплата через YooKassa временно недоступна") + return + + if amount_kopeks < settings.YOOKASSA_MIN_AMOUNT_KOPEKS: + min_rubles = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Минимальная сумма для оплаты картой: {min_rubles:.0f} ₽") + return + + if amount_kopeks > settings.YOOKASSA_MAX_AMOUNT_KOPEKS: + max_rubles = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Максимальная сумма для оплаты картой: {max_rubles:,.0f} ₽".replace(',', ' ')) + return + + try: + payment_service = PaymentService(message.bot) + + payment_result = await payment_service.create_yookassa_payment( + db=db, + user_id=db_user.id, + amount_kopeks=amount_kopeks, + description=settings.get_balance_payment_description(amount_kopeks), + receipt_email=None, + receipt_phone=None, + metadata={ + "user_telegram_id": str(db_user.telegram_id), + "user_username": db_user.username or "", + "purpose": "balance_topup" + } + ) + + if not payment_result: + await message.answer("❌ Ошибка создания платежа. Попробуйте позже или обратитесь в поддержку.") + await state.clear() + return + + confirmation_url = payment_result.get("confirmation_url") + if not confirmation_url: + await message.answer("❌ Ошибка получения ссылки для оплаты. Обратитесь в поддержку.") + await state.clear() + return + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="💳 Оплатить картой", url=confirmation_url)], + [types.InlineKeyboardButton(text="📊 Проверить статус", callback_data=f"check_yookassa_{payment_result['local_payment_id']}")], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] + ]) + + await message.answer( + f"💳 Оплата банковской картой\n\n" + f"💰 Сумма: {settings.format_price(amount_kopeks)}\n" + f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...\n\n" + f"📱 Инструкция:\n" + f"1. Нажмите кнопку 'Оплатить картой'\n" + f"2. Введите данные вашей карты\n" + f"3. Подтвердите платеж\n" + f"4. Деньги поступят на баланс автоматически\n\n" + f"🔒 Оплата происходит через защищенную систему YooKassa\n" + f"✅ Принимаем карты: Visa, MasterCard, МИР\n\n" + f"❓ Если возникнут проблемы, обратитесь в {settings.get_support_contact_display_html()}", + reply_markup=keyboard, + parse_mode="HTML" + ) + + await state.clear() + + logger.info(f"Создан платеж YooKassa для пользователя {db_user.telegram_id}: " + f"{amount_kopeks//100}₽, ID: {payment_result['yookassa_payment_id']}") + + except Exception as e: + logger.error(f"Ошибка создания YooKassa платежа: {e}") + await message.answer("❌ Ошибка создания платежа. Попробуйте позже или обратитесь в поддержку.") + await state.clear() + + +@error_handler +async def process_yookassa_sbp_payment_amount( + message: types.Message, + db_user: User, + db: AsyncSession, + amount_kopeks: int, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.is_yookassa_enabled() or not settings.YOOKASSA_SBP_ENABLED: + await message.answer("❌ Оплата через СБП временно недоступна") + return + + if amount_kopeks < settings.YOOKASSA_MIN_AMOUNT_KOPEKS: + min_rubles = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Минимальная сумма для оплаты через СБП: {min_rubles:.0f} ₽") + return + + if amount_kopeks > settings.YOOKASSA_MAX_AMOUNT_KOPEKS: + max_rubles = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Максимальная сумма для оплаты через СБП: {max_rubles:,.0f} ₽".replace(',', ' ')) + return + + try: + payment_service = PaymentService(message.bot) + + payment_result = await payment_service.create_yookassa_sbp_payment( + db=db, + user_id=db_user.id, + amount_kopeks=amount_kopeks, + description=settings.get_balance_payment_description(amount_kopeks), + receipt_email=None, + receipt_phone=None, + metadata={ + "user_telegram_id": str(db_user.telegram_id), + "user_username": db_user.username or "", + "purpose": "balance_topup_sbp" + } + ) + + if not payment_result: + await message.answer("❌ Ошибка создания платежа через СБП. Попробуйте позже или обратитесь в поддержку.") + await state.clear() + return + + confirmation_url = payment_result.get("confirmation_url") + if not confirmation_url: + await message.answer("❌ Ошибка получения ссылки для оплаты через СБП. Обратитесь в поддержку.") + await state.clear() + return + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="🏦 Оплатить через СБП", url=confirmation_url)], + [types.InlineKeyboardButton(text="📊 Проверить статус", callback_data=f"check_yookassa_{payment_result['local_payment_id']}")], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] + ]) + + await message.answer( + f"🏦 Оплата через СБП\n\n" + f"💰 Сумма: {settings.format_price(amount_kopeks)}\n" + f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...\n\n" + f"📱 Инструкция:\n" + f"1. Нажмите кнопку 'Оплатить через СБП'\n" + f"2. Вас перенаправит в приложение вашего банка\n" + f"3. Подтвердите платеж через СБП\n" + f"4. Деньги поступят на баланс автоматически\n\n" + f"🔒 Оплата происходит через защищенную систему YooKassa\n" + f"✅ Принимаем СБП от всех банков-участников\n\n" + f"❓ Если возникнут проблемы, обратитесь в {settings.get_support_contact_display_html()}", + reply_markup=keyboard, + parse_mode="HTML" + ) + + await state.clear() + + logger.info(f"Создан платеж YooKassa СБП для пользователя {db_user.telegram_id}: " + f"{amount_kopeks//100}₽, ID: {payment_result['yookassa_payment_id']}") + + except Exception as e: + logger.error(f"Ошибка создания YooKassa СБП платежа: {e}") + await message.answer("❌ Ошибка создания платежа через СБП. Попробуйте позже или обратитесь в поддержку.") + await state.clear() + + +@error_handler +async def process_mulenpay_payment_amount( + message: types.Message, + db_user: User, + db: AsyncSession, + amount_kopeks: int, + state: FSMContext, +): + texts = get_texts(db_user.language) + + if not settings.is_mulenpay_enabled(): + await message.answer("❌ Оплата через Mulen Pay временно недоступна") + return + + if amount_kopeks < settings.MULENPAY_MIN_AMOUNT_KOPEKS: + await message.answer( + f"Минимальная сумма пополнения: {settings.format_price(settings.MULENPAY_MIN_AMOUNT_KOPEKS)}" + ) + return + + if amount_kopeks > settings.MULENPAY_MAX_AMOUNT_KOPEKS: + await message.answer( + f"Максимальная сумма пополнения: {settings.format_price(settings.MULENPAY_MAX_AMOUNT_KOPEKS)}" + ) + return + + amount_rubles = amount_kopeks / 100 + + try: + payment_service = PaymentService(message.bot) + payment_result = await payment_service.create_mulenpay_payment( + db=db, + user_id=db_user.id, + amount_kopeks=amount_kopeks, + description=settings.get_balance_payment_description(amount_kopeks), + language=db_user.language, + ) + + if not payment_result or not payment_result.get("payment_url"): + await message.answer( + texts.t( + "MULENPAY_PAYMENT_ERROR", + "❌ Ошибка создания платежа Mulen Pay. Попробуйте позже или обратитесь в поддержку.", + ) + ) + await state.clear() + return + + payment_url = payment_result.get("payment_url") + mulen_payment_id = payment_result.get("mulen_payment_id") + local_payment_id = payment_result.get("local_payment_id") + + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text=texts.t( + "MULENPAY_PAY_BUTTON", + "💳 Оплатить через Mulen Pay", + ), + url=payment_url, + ) + ], + [ + types.InlineKeyboardButton( + text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"), + callback_data=f"check_mulenpay_{local_payment_id}", + ) + ], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")], + ] + ) + + payment_id_display = mulen_payment_id if mulen_payment_id is not None else local_payment_id + + message_template = texts.t( + "MULENPAY_PAYMENT_INSTRUCTIONS", + ( + "💳 Оплата через Mulen Pay\n\n" + "💰 Сумма: {amount}\n" + "🆔 ID платежа: {payment_id}\n\n" + "📱 Инструкция:\n" + "1. Нажмите кнопку ‘Оплатить через Mulen Pay’\n" + "2. Следуйте подсказкам платежной системы\n" + "3. Подтвердите перевод\n" + "4. Средства зачислятся автоматически\n\n" + "❓ Если возникнут проблемы, обратитесь в {support}" + ), + ) + + message_text = message_template.format( + amount=settings.format_price(amount_kopeks), + payment_id=payment_id_display, + support=settings.get_support_contact_display_html(), + ) + + await message.answer( + message_text, + reply_markup=keyboard, + parse_mode="HTML", + ) + + await state.clear() + + logger.info( + "Создан MulenPay платеж для пользователя %s: %s₽, ID: %s", + db_user.telegram_id, + amount_rubles, + payment_id_display, + ) + + except Exception as e: + logger.error(f"Ошибка создания MulenPay платежа: {e}") + await message.answer( + texts.t( + "MULENPAY_PAYMENT_ERROR", + "❌ Ошибка создания платежа Mulen Pay. Попробуйте позже или обратитесь в поддержку.", + ) + ) + await state.clear() + + +@error_handler +async def process_pal24_payment_amount( + message: types.Message, + db_user: User, + db: AsyncSession, + amount_kopeks: int, + state: FSMContext, +): + texts = get_texts(db_user.language) + + if not settings.is_pal24_enabled(): + await message.answer("❌ Оплата через PayPalych временно недоступна") + return + + if amount_kopeks < settings.PAL24_MIN_AMOUNT_KOPEKS: + min_rubles = settings.PAL24_MIN_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Минимальная сумма для оплаты через PayPalych: {min_rubles:.0f} ₽") + return + + if amount_kopeks > settings.PAL24_MAX_AMOUNT_KOPEKS: + max_rubles = settings.PAL24_MAX_AMOUNT_KOPEKS / 100 + await message.answer(f"❌ Максимальная сумма для оплаты через PayPalych: {max_rubles:,.0f} ₽".replace(',', ' ')) + return + + try: + payment_service = PaymentService(message.bot) + payment_result = await payment_service.create_pal24_payment( + db=db, + user_id=db_user.id, + amount_kopeks=amount_kopeks, + description=settings.get_balance_payment_description(amount_kopeks), + language=db_user.language, + ) + + if not payment_result: + await message.answer( + texts.t( + "PAL24_PAYMENT_ERROR", + "❌ Ошибка создания платежа PayPalych. Попробуйте позже или обратитесь в поддержку.", + ) + ) + await state.clear() + return + + sbp_url = ( + payment_result.get("sbp_url") + or payment_result.get("transfer_url") + ) + card_url = payment_result.get("card_url") + fallback_url = ( + payment_result.get("link_page_url") + or payment_result.get("link_url") + ) + + if not (sbp_url or card_url or fallback_url): + await message.answer( + texts.t( + "PAL24_PAYMENT_ERROR", + "❌ Ошибка создания платежа PayPalych. Попробуйте позже или обратитесь в поддержку.", + ) + ) + await state.clear() + return + + if not sbp_url: + sbp_url = fallback_url + + bill_id = payment_result.get("bill_id") + local_payment_id = payment_result.get("local_payment_id") + + pay_buttons: list[list[types.InlineKeyboardButton]] = [] + steps: list[str] = [] + step_counter = 1 + + default_sbp_text = texts.t( + "PAL24_SBP_PAY_BUTTON", + "🏦 Оплатить через PayPalych (СБП)", + ) + sbp_button_text = settings.get_pal24_sbp_button_text(default_sbp_text) + + if sbp_url and settings.is_pal24_sbp_button_visible(): + pay_buttons.append( + [ + types.InlineKeyboardButton( + text=sbp_button_text, + url=sbp_url, + ) + ] + ) + steps.append( + texts.t( + "PAL24_INSTRUCTION_BUTTON", + "{step}. Нажмите кнопку «{button}»", + ).format(step=step_counter, button=html.escape(sbp_button_text)) + ) + step_counter += 1 + + default_card_text = texts.t( + "PAL24_CARD_PAY_BUTTON", + "💳 Оплатить банковской картой (PayPalych)", + ) + card_button_text = settings.get_pal24_card_button_text(default_card_text) + + if card_url and card_url != sbp_url and settings.is_pal24_card_button_visible(): + pay_buttons.append( + [ + types.InlineKeyboardButton( + text=card_button_text, + url=card_url, + ) + ] + ) + steps.append( + texts.t( + "PAL24_INSTRUCTION_BUTTON", + "{step}. Нажмите кнопку «{button}»", + ).format(step=step_counter, button=html.escape(card_button_text)) + ) + step_counter += 1 + + if not pay_buttons and fallback_url and settings.is_pal24_sbp_button_visible(): + pay_buttons.append( + [ + types.InlineKeyboardButton( + text=sbp_button_text, + url=fallback_url, + ) + ] + ) + steps.append( + texts.t( + "PAL24_INSTRUCTION_BUTTON", + "{step}. Нажмите кнопку «{button}»", + ).format(step=step_counter, button=html.escape(sbp_button_text)) + ) + step_counter += 1 + + follow_template = texts.t( + "PAL24_INSTRUCTION_FOLLOW", + "{step}. Следуйте подсказкам платёжной системы", + ) + steps.append(follow_template.format(step=step_counter)) + step_counter += 1 + + confirm_template = texts.t( + "PAL24_INSTRUCTION_CONFIRM", + "{step}. Подтвердите перевод", + ) + steps.append(confirm_template.format(step=step_counter)) + step_counter += 1 + + success_template = texts.t( + "PAL24_INSTRUCTION_COMPLETE", + "{step}. Средства зачислятся автоматически", + ) + steps.append(success_template.format(step=step_counter)) + + message_template = texts.t( + "PAL24_PAYMENT_INSTRUCTIONS", + ( + "🏦 Оплата через PayPalych\n\n" + "💰 Сумма: {amount}\n" + "🆔 ID счета: {bill_id}\n\n" + "📱 Инструкция:\n{steps}\n\n" + "❓ Если возникнут проблемы, обратитесь в {support}" + ), + ) + + keyboard_rows = pay_buttons + [ + [ + types.InlineKeyboardButton( + text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"), + callback_data=f"check_pal24_{local_payment_id}", + ) + ], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")], + ] + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows) + + message_text = message_template.format( + amount=settings.format_price(amount_kopeks), + bill_id=bill_id, + steps="\n".join(steps), + support=settings.get_support_contact_display_html(), + ) + + await message.answer( + message_text, + reply_markup=keyboard, + parse_mode="HTML", + ) + + await state.clear() + + logger.info( + "Создан PayPalych счет для пользователя %s: %s₽, ID: %s", + db_user.telegram_id, + amount_kopeks / 100, + bill_id, + ) + + except Exception as e: + logger.error(f"Ошибка создания PayPalych платежа: {e}") + await message.answer( + texts.t( + "PAL24_PAYMENT_ERROR", + "❌ Ошибка создания платежа PayPalych. Попробуйте позже или обратитесь в поддержку.", + ) + ) + await state.clear() + + +@error_handler +async def check_yookassa_payment_status( + callback: types.CallbackQuery, + db: AsyncSession +): + try: + local_payment_id = int(callback.data.split('_')[-1]) + + from app.database.crud.yookassa import get_yookassa_payment_by_local_id + payment = await get_yookassa_payment_by_local_id(db, local_payment_id) + + if not payment: + await callback.answer("❌ Платеж не найден", show_alert=True) + return + + status_emoji = { + "pending": "⏳", + "waiting_for_capture": "⌛", + "succeeded": "✅", + "canceled": "❌", + "failed": "❌" + } + + status_text = { + "pending": "Ожидает оплаты", + "waiting_for_capture": "Ожидает подтверждения", + "succeeded": "Оплачен", + "canceled": "Отменен", + "failed": "Ошибка" + } + + emoji = status_emoji.get(payment.status, "❓") + status = status_text.get(payment.status, "Неизвестно") + + message_text = (f"💳 Статус платежа:\n\n" + f"🆔 ID: {payment.yookassa_payment_id[:8]}...\n" + f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n" + f"📊 Статус: {emoji} {status}\n" + f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M')}\n") + + if payment.is_succeeded: + message_text += "\n✅ Платеж успешно завершен!\n\nСредства зачислены на баланс." + elif payment.is_pending: + message_text += "\n⏳ Платеж ожидает оплаты. Нажмите кнопку 'Оплатить' выше." + elif payment.is_failed: + message_text += ( + f"\n❌ Платеж не прошел. Обратитесь в {settings.get_support_contact_display()}" + ) + + await callback.answer(message_text, show_alert=True) + + except Exception as e: + logger.error(f"Ошибка проверки статуса платежа: {e}") + await callback.answer("❌ Ошибка проверки статуса", show_alert=True) + + +@error_handler +async def check_mulenpay_payment_status( + callback: types.CallbackQuery, + db: AsyncSession +): + try: + local_payment_id = int(callback.data.split('_')[-1]) + payment_service = PaymentService(callback.bot) + status_info = await payment_service.get_mulenpay_payment_status(db, local_payment_id) + + if not status_info: + await callback.answer("❌ Платеж не найден", show_alert=True) + return + + payment = status_info["payment"] + + status_labels = { + "created": ("⏳", "Ожидает оплаты"), + "processing": ("⌛", "Обрабатывается"), + "success": ("✅", "Оплачен"), + "canceled": ("❌", "Отменен"), + "error": ("⚠️", "Ошибка"), + "hold": ("🔒", "Холд"), + "unknown": ("❓", "Неизвестно"), + } + + emoji, status_text = status_labels.get(payment.status, ("❓", "Неизвестно")) + + message_lines = [ + "💳 Статус платежа Mulen Pay:\n\n", + f"🆔 ID: {payment.mulen_payment_id or payment.id}\n", + f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n", + f"📊 Статус: {emoji} {status_text}\n", + f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M')}\n", + ] + + if payment.is_paid: + message_lines.append("\n✅ Платеж успешно завершен! Средства уже на балансе.") + elif payment.status in {"created", "processing"}: + message_lines.append( + "\n⏳ Платеж еще не завершен. Завершите оплату по ссылке и проверьте статус позже." + ) + if payment.payment_url: + message_lines.append(f"\n🔗 Ссылка на оплату: {payment.payment_url}") + elif payment.status in {"canceled", "error"}: + message_lines.append( + f"\n❌ Платеж не был завершен. Попробуйте создать новый платеж или обратитесь в {settings.get_support_contact_display()}" + ) + + message_text = "".join(message_lines) + + if len(message_text) > 190: + await callback.message.answer(message_text) + await callback.answer("ℹ️ Статус платежа отправлен в чат", show_alert=True) + else: + await callback.answer(message_text, show_alert=True) + + except Exception as e: + logger.error(f"Ошибка проверки статуса MulenPay: {e}") + await callback.answer("❌ Ошибка проверки статуса", show_alert=True) + + +@error_handler +async def check_pal24_payment_status( + callback: types.CallbackQuery, + db: AsyncSession, +): + try: + local_payment_id = int(callback.data.split('_')[-1]) + payment_service = PaymentService(callback.bot) + status_info = await payment_service.get_pal24_payment_status(db, local_payment_id) + + if not status_info: + await callback.answer("❌ Платеж не найден", show_alert=True) + return + + payment = status_info["payment"] + + status_labels = { + "NEW": ("⏳", "Ожидает оплаты"), + "PROCESS": ("⌛", "Обрабатывается"), + "SUCCESS": ("✅", "Оплачен"), + "FAIL": ("❌", "Отменен"), + "UNDERPAID": ("⚠️", "Недоплата"), + "OVERPAID": ("⚠️", "Переплата"), + } + + emoji, status_text = status_labels.get(payment.status, ("❓", "Неизвестно")) + + metadata = payment.metadata_json or {} + links_meta = metadata.get("links") if isinstance(metadata, dict) else None + if not isinstance(links_meta, dict): + links_meta = {} + + sbp_link = links_meta.get("sbp") or payment.link_url + card_link = links_meta.get("card") + + if not card_link and payment.link_page_url and payment.link_page_url != sbp_link: + card_link = payment.link_page_url + + message_lines = [ + "🏦 Статус платежа PayPalych:", + "", + f"🆔 ID счета: {payment.bill_id}", + f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}", + f"📊 Статус: {emoji} {status_text}", + f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M')}", + ] + + if payment.is_paid: + message_lines.append("") + message_lines.append("✅ Платеж успешно завершен! Средства уже на балансе.") + elif payment.status in {"NEW", "PROCESS"}: + message_lines.append("") + message_lines.append("⏳ Платеж еще не завершен. Оплатите счет и проверьте статус позже.") + if sbp_link: + message_lines.append("") + message_lines.append(f"🏦 СБП: {sbp_link}") + if card_link and card_link != sbp_link: + message_lines.append(f"💳 Банковская карта: {card_link}") + elif payment.status in {"FAIL", "UNDERPAID", "OVERPAID"}: + message_lines.append("") + message_lines.append( + f"❌ Платеж не завершен корректно. Обратитесь в {settings.get_support_contact_display()}" + ) + + from app.localization.texts import get_texts + db_user = getattr(callback, 'db_user', None) + texts = get_texts(db_user.language if db_user else 'ru') if db_user else get_texts('ru') + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"), callback_data=f"check_pal24_{local_payment_id}")], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] + ]) + + await callback.answer() + await callback.message.edit_text( + "\n".join(message_lines), + reply_markup=keyboard, + disable_web_page_preview=True, + ) + + except Exception as e: + logger.error(f"Ошибка проверки статуса PayPalych: {e}") + await callback.answer("❌ Ошибка проверки статуса", show_alert=True) + + +@error_handler +async def start_cryptobot_payment( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.is_cryptobot_enabled(): + await callback.answer("❌ Оплата криптовалютой временно недоступна", show_alert=True) + return + + from app.utils.currency_converter import currency_converter + try: + current_rate = await currency_converter.get_usd_to_rub_rate() + rate_text = f"💱 Текущий курс: 1 USD = {current_rate:.2f} ₽" + except Exception as e: + logger.warning(f"Не удалось получить курс валют: {e}") + current_rate = 95.0 + rate_text = f"💱 Курс: 1 USD ≈ {current_rate:.0f} ₽" + + available_assets = settings.get_cryptobot_assets() + assets_text = ", ".join(available_assets) + + # Формируем текст сообщения в зависимости от настройки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + message_text = ( + f"🪙 Пополнение криптовалютой\n\n" + f"Выберите сумму пополнения или введите вручную сумму " + f"от 100 до 100,000 ₽:\n\n" + f"💰 Доступные активы: {assets_text}\n" + f"⚡ Мгновенное зачисление на баланс\n" + f"🔒 Безопасная оплата через CryptoBot\n\n" + f"{rate_text}\n" + f"Сумма будет автоматически конвертирована в USD для оплаты." + ) + else: + message_text = ( + f"🪙 Пополнение криптовалютой\n\n" + f"Введите сумму для пополнения от 100 до 100,000 ₽:\n\n" + f"💰 Доступные активы: {assets_text}\n" + f"⚡ Мгновенное зачисление на баланс\n" + f"🔒 Безопасная оплата через CryptoBot\n\n" + f"{rate_text}\n" + f"Сумма будет автоматически конвертирована в USD для оплаты." + ) + + # Создаем клавиатуру + keyboard = get_back_keyboard(db_user.language) + + # Если включен быстрый выбор суммы и не отключены кнопки, добавляем кнопки + if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: + quick_amount_buttons = get_quick_amount_buttons(db_user.language) + if quick_amount_buttons: + # Вставляем кнопки быстрого выбора перед кнопкой "Назад" + keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard + + await callback.message.edit_text( + message_text, + reply_markup=keyboard, + parse_mode="HTML" + ) + + await state.set_state(BalanceStates.waiting_for_amount) + await state.update_data(payment_method="cryptobot", current_rate=current_rate) + await callback.answer() + +@error_handler +async def process_cryptobot_payment_amount( + message: types.Message, + db_user: User, + db: AsyncSession, + amount_kopeks: int, + state: FSMContext +): + texts = get_texts(db_user.language) + + if not settings.is_cryptobot_enabled(): + await message.answer("❌ Оплата криптовалютой временно недоступна") + return + + amount_rubles = amount_kopeks / 100 + + if amount_rubles < 100: + await message.answer("Минимальная сумма пополнения: 100 ₽") + return + + if amount_rubles > 100000: + await message.answer("Максимальная сумма пополнения: 100,000 ₽") + return + + try: + data = await state.get_data() + current_rate = data.get('current_rate') + + if not current_rate: + from app.utils.currency_converter import currency_converter + current_rate = await currency_converter.get_usd_to_rub_rate() + + amount_usd = amount_rubles / current_rate + + amount_usd = round(amount_usd, 2) + + if amount_usd < 1: + await message.answer("❌ Минимальная сумма для оплаты в USD: 1.00 USD") + return + + if amount_usd > 1000: + await message.answer("❌ Максимальная сумма для оплаты в USD: 1,000 USD") + return + + payment_service = PaymentService(message.bot) + + payment_result = await payment_service.create_cryptobot_payment( + db=db, + user_id=db_user.id, + amount_usd=amount_usd, + asset=settings.CRYPTOBOT_DEFAULT_ASSET, + description=f"Пополнение баланса на {amount_rubles:.0f} ₽ ({amount_usd:.2f} USD)", + payload=f"balance_{db_user.id}_{amount_kopeks}" + ) + + if not payment_result: + await message.answer("❌ Ошибка создания платежа. Попробуйте позже или обратитесь в поддержку.") + await state.clear() + return + + bot_invoice_url = payment_result.get("bot_invoice_url") + mini_app_invoice_url = payment_result.get("mini_app_invoice_url") + + payment_url = bot_invoice_url or mini_app_invoice_url + + if not payment_url: + await message.answer("❌ Ошибка получения ссылки для оплаты. Обратитесь в поддержку.") + await state.clear() + return + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="🪙 Оплатить", url=payment_url)], + [types.InlineKeyboardButton(text="📊 Проверить статус", callback_data=f"check_cryptobot_{payment_result['local_payment_id']}")], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] + ]) + + await message.answer( + f"🪙 Оплата криптовалютой\n\n" + f"💰 Сумма к зачислению: {amount_rubles:.0f} ₽\n" + f"💵 К оплате: {amount_usd:.2f} USD\n" + f"🪙 Актив: {payment_result['asset']}\n" + f"💱 Курс: 1 USD = {current_rate:.2f} ₽\n" + f"🆔 ID платежа: {payment_result['invoice_id'][:8]}...\n\n" + f"📱 Инструкция:\n" + f"1. Нажмите кнопку 'Оплатить'\n" + f"2. Выберите удобный актив\n" + f"3. Переведите указанную сумму\n" + f"4. Деньги поступят на баланс автоматически\n\n" + f"🔒 Оплата проходит через защищенную систему CryptoBot\n" + f"⚡ Поддерживаемые активы: USDT, TON, BTC, ETH\n\n" + f"❓ Если возникнут проблемы, обратитесь в {settings.get_support_contact_display_html()}", + reply_markup=keyboard, + parse_mode="HTML" + ) + + await state.clear() + + logger.info(f"Создан CryptoBot платеж для пользователя {db_user.telegram_id}: " + f"{amount_rubles:.0f} ₽ ({amount_usd:.2f} USD), ID: {payment_result['invoice_id']}") + + except Exception as e: + logger.error(f"Ошибка создания CryptoBot платежа: {e}") + await message.answer("❌ Ошибка создания платежа. Попробуйте позже или обратитесь в поддержку.") + await state.clear() + +@error_handler +async def check_cryptobot_payment_status( + callback: types.CallbackQuery, + db: AsyncSession +): + try: + local_payment_id = int(callback.data.split('_')[-1]) + + from app.database.crud.cryptobot import get_cryptobot_payment_by_id + payment = await get_cryptobot_payment_by_id(db, local_payment_id) + + if not payment: + await callback.answer("❌ Платеж не найден", show_alert=True) + return + + status_emoji = { + "active": "⏳", + "paid": "✅", + "expired": "❌" + } + + status_text = { + "active": "Ожидает оплаты", + "paid": "Оплачен", + "expired": "Истек" + } + + emoji = status_emoji.get(payment.status, "❓") + status = status_text.get(payment.status, "Неизвестно") + + message_text = (f"🪙 Статус платежа:\n\n" + f"🆔 ID: {payment.invoice_id[:8]}...\n" + f"💰 Сумма: {payment.amount} {payment.asset}\n" + f"📊 Статус: {emoji} {status}\n" + f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M')}\n") + + if payment.is_paid: + message_text += "\n✅ Платеж успешно завершен!\n\nСредства зачислены на баланс." + elif payment.is_pending: + message_text += "\n⏳ Платеж ожидает оплаты. Нажмите кнопку 'Оплатить' выше." + elif payment.is_expired: + message_text += ( + f"\n❌ Платеж истек. Обратитесь в {settings.get_support_contact_display()}" + ) + + await callback.answer(message_text, show_alert=True) + + except Exception as e: + logger.error(f"Ошибка проверки статуса CryptoBot платежа: {e}") + await callback.answer("❌ Ошибка проверки статуса", show_alert=True) + + + +@error_handler +async def handle_sbp_payment( + callback: types.CallbackQuery, + db: AsyncSession +): + try: + local_payment_id = int(callback.data.split('_')[-1]) + + from app.database.crud.yookassa import get_yookassa_payment_by_local_id + payment = await get_yookassa_payment_by_local_id(db, local_payment_id) + + if not payment: + await callback.answer("❌ Платеж не найден", show_alert=True) + return + + import json + metadata = json.loads(payment.metadata_json) if payment.metadata_json else {} + confirmation_token = metadata.get("confirmation_token") + + if not confirmation_token: + await callback.answer("❌ Токен подтверждения не найден", show_alert=True) + return + + await callback.message.answer( + f"Для оплаты через СБП откройте приложение вашего банка и подтвердите платеж.\\n\\n" + f"Если у вас не открылось банковское приложение автоматически, вы можете:\\n" + f"1. Скопировать этот токен: {confirmation_token}\\n" + f"2. Открыть приложение вашего банка\\n" + f"3. Найти функцию оплаты по токену\\n" + f"4. Вставить токен и подтвердить платеж", + parse_mode="HTML" + ) + + await callback.answer("Информация об оплате отправлена", show_alert=True) + + except Exception as e: + logger.error(f"Ошибка обработки embedded платежа СБП: {e}") + await callback.answer("❌ Ошибка обработки платежа", show_alert=True) + + + +@error_handler +async def handle_quick_amount_selection( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext +): + """ + Обработчик выбора суммы через кнопки быстрого выбора + """ + # Извлекаем сумму из callback_data + try: + amount_kopeks = int(callback.data.split('_')[-1]) + amount_rubles = amount_kopeks / 100 + + # Получаем метод оплаты из состояния + data = await state.get_data() + payment_method = data.get("payment_method", "yookassa") + + # Проверяем, какой метод оплаты был выбран и вызываем соответствующий обработчик + if payment_method == "yookassa": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_yookassa_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif payment_method == "yookassa_sbp": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_yookassa_sbp_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif payment_method == "mulenpay": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_mulenpay_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif payment_method == "pal24": + from app.database.database import AsyncSessionLocal + async with AsyncSessionLocal() as db: + await process_pal24_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + else: + await callback.answer("❌ Неизвестный способ оплаты", show_alert=True) + return + + except ValueError: + await callback.answer("❌ Ошибка обработки суммы", show_alert=True) + except Exception as e: + logger.error(f"Ошибка обработки быстрого выбора суммы: {e}") + await callback.answer("❌ Ошибка обработки запроса", show_alert=True) + + +@error_handler +async def handle_topup_amount_callback( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext, +): + try: + _, method, amount_str = callback.data.split("|", 2) + amount_kopeks = int(amount_str) + except ValueError: + await callback.answer("❌ Некорректный запрос", show_alert=True) + return + + if amount_kopeks <= 0: + await callback.answer("❌ Некорректная сумма", show_alert=True) + return + + try: + if method == "yookassa": + from app.database.database import AsyncSessionLocal + + async with AsyncSessionLocal() as db: + await process_yookassa_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif method == "yookassa_sbp": + from app.database.database import AsyncSessionLocal + + async with AsyncSessionLocal() as db: + await process_yookassa_sbp_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif method == "mulenpay": + from app.database.database import AsyncSessionLocal + + async with AsyncSessionLocal() as db: + await process_mulenpay_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif method == "pal24": + from app.database.database import AsyncSessionLocal + + async with AsyncSessionLocal() as db: + await process_pal24_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif method == "cryptobot": + from app.database.database import AsyncSessionLocal + + async with AsyncSessionLocal() as db: + await process_cryptobot_payment_amount( + callback.message, db_user, db, amount_kopeks, state + ) + elif method == "stars": + await process_stars_payment_amount( + callback.message, db_user, amount_kopeks, state + ) + elif method == "tribute": + await start_tribute_payment(callback, db_user) + return + else: + await callback.answer("❌ Неизвестный способ оплаты", show_alert=True) + return + + await callback.answer() + + except Exception as error: + logger.error(f"Ошибка быстрого пополнения: {error}") + await callback.answer("❌ Ошибка обработки запроса", show_alert=True) + + +def register_handlers(dp: Dispatcher): + + dp.callback_query.register( + show_balance_menu, + F.data == "menu_balance" + ) + + dp.callback_query.register( + show_balance_history, + F.data == "balance_history" + ) + + dp.callback_query.register( + handle_balance_history_pagination, + F.data.startswith("balance_history_page_") + ) + + dp.callback_query.register( + show_payment_methods, + F.data == "balance_topup" + ) + + dp.callback_query.register( + start_stars_payment, + F.data == "topup_stars" + ) + + dp.callback_query.register( + start_yookassa_payment, + F.data == "topup_yookassa" + ) + + dp.callback_query.register( + start_yookassa_sbp_payment, + F.data == "topup_yookassa_sbp" + ) + + dp.callback_query.register( + start_mulenpay_payment, + F.data == "topup_mulenpay" + ) + + dp.callback_query.register( + start_pal24_payment, + F.data == "topup_pal24" + ) + + dp.callback_query.register( + check_yookassa_payment_status, + F.data.startswith("check_yookassa_") + ) + + dp.callback_query.register( + start_tribute_payment, + F.data == "topup_tribute" + ) + + dp.callback_query.register( + request_support_topup, + F.data == "topup_support" + ) + + dp.callback_query.register( + check_yookassa_payment_status, + F.data.startswith("check_yookassa_") + ) + + dp.message.register( + process_topup_amount, + BalanceStates.waiting_for_amount + ) + + dp.callback_query.register( + start_cryptobot_payment, + F.data == "topup_cryptobot" + ) + + dp.callback_query.register( + check_cryptobot_payment_status, + F.data.startswith("check_cryptobot_") + ) + + dp.callback_query.register( + check_mulenpay_payment_status, + F.data.startswith("check_mulenpay_") + ) + + dp.callback_query.register( + check_pal24_payment_status, + F.data.startswith("check_pal24_") + ) + + dp.callback_query.register( + handle_payment_methods_unavailable, + F.data == "payment_methods_unavailable" + ) + + # Регистрируем обработчик для кнопок быстрого выбора суммы + dp.callback_query.register( + handle_quick_amount_selection, + F.data.startswith("quick_amount_") + ) + + dp.callback_query.register( + handle_topup_amount_callback, + F.data.startswith("topup_amount|") + )