diff --git a/app/handlers/balance.py.backup2 b/app/handlers/balance.py.backup2 deleted file mode 100644 index 1bbcab0f..00000000 --- a/app/handlers/balance.py.backup2 +++ /dev/null @@ -1,1927 +0,0 @@ -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|") - )