From a4de6a8330e11d9c77a4711752cb9d57c213813e Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 14 Oct 2025 21:39:20 +0300 Subject: [PATCH] =?UTF-8?q?Revert=20"=D0=B4=D0=B0=20=D0=B2=D1=81=D0=B5=20?= =?UTF-8?q?=D0=B4=D0=BE=20=D0=BA=D1=83=D1=87=D0=B8"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 4 - app/config.py | 8 - app/handlers/__init__.py | 3 - app/handlers/balance.py | 1917 +++++++++++++++++++++++++++++ app/handlers/balance/__init__.py | 3 - app/handlers/balance/cryptobot.py | 241 ---- app/handlers/balance/main.py | 702 ----------- app/handlers/balance/mulenpay.py | 238 ---- app/handlers/balance/pal24.py | 364 ------ app/handlers/balance/stars.py | 103 -- app/handlers/balance/tribute.py | 59 - app/handlers/balance/yookassa.py | 340 ----- 12 files changed, 1917 insertions(+), 2065 deletions(-) create mode 100644 app/handlers/balance.py delete mode 100644 app/handlers/balance/__init__.py delete mode 100644 app/handlers/balance/cryptobot.py delete mode 100644 app/handlers/balance/main.py delete mode 100644 app/handlers/balance/mulenpay.py delete mode 100644 app/handlers/balance/pal24.py delete mode 100644 app/handlers/balance/stars.py delete mode 100644 app/handlers/balance/tribute.py delete mode 100644 app/handlers/balance/yookassa.py diff --git a/.env.example b/.env.example index 0c040c5e..873d2f1e 100644 --- a/.env.example +++ b/.env.example @@ -277,10 +277,6 @@ 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 71065597..3714a3e7 100644 --- a/app/config.py +++ b/app/config.py @@ -228,8 +228,6 @@ 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" @@ -462,12 +460,6 @@ 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/__init__.py b/app/handlers/__init__.py index 84d30575..e69de29b 100644 --- a/app/handlers/__init__.py +++ b/app/handlers/__init__.py @@ -1,3 +0,0 @@ -from . import balance - -__all__ = ["balance"] \ No newline at end of file diff --git a/app/handlers/balance.py b/app/handlers/balance.py new file mode 100644 index 00000000..15562dc9 --- /dev/null +++ b/app/handlers/balance.py @@ -0,0 +1,1917 @@ +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: + 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: + 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: + 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()}" + ) + + await callback.answer() + await callback.message.answer( + "\n".join(message_lines), + 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/__init__.py b/app/handlers/balance/__init__.py deleted file mode 100644 index 268702f6..00000000 --- a/app/handlers/balance/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .main import register_balance_handlers - -__all__ = ["register_balance_handlers"] \ No newline at end of file diff --git a/app/handlers/balance/cryptobot.py b/app/handlers/balance/cryptobot.py deleted file mode 100644 index 4456c09e..00000000 --- a/app/handlers/balance/cryptobot.py +++ /dev/null @@ -1,241 +0,0 @@ -import logging -from aiogram import types -from aiogram.fsm.context import FSMContext -from sqlalchemy.ext.asyncio import AsyncSession - -from app.config import settings -from app.database.models import User -from app.keyboards.inline import get_back_keyboard -from app.localization.texts import get_texts -from app.services.payment_service import PaymentService -from app.utils.decorators import error_handler -from app.states import BalanceStates - -logger = logging.getLogger(__name__) - - -@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: - from .main import get_quick_amount_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) \ No newline at end of file diff --git a/app/handlers/balance/main.py b/app/handlers/balance/main.py deleted file mode 100644 index b94320f0..00000000 --- a/app/handlers/balance/main.py +++ /dev/null @@ -1,702 +0,0 @@ -import html -import logging -from aiogram import 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 -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 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": - from .stars import process_stars_payment_amount - await process_stars_payment_amount(message, db_user, amount_kopeks, state) - elif payment_method == "yookassa": - from app.database.database import AsyncSessionLocal - from .yookassa import process_yookassa_payment_amount - 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 - from .yookassa import process_yookassa_sbp_payment_amount - 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 - from .mulenpay import process_mulenpay_payment_amount - 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 - from .pal24 import process_pal24_payment_amount - 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 - from .cryptobot import process_cryptobot_payment_amount - 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 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 - from .yookassa import process_yookassa_payment_amount - 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 - from .yookassa import process_yookassa_sbp_payment_amount - 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 - from .mulenpay import process_mulenpay_payment_amount - 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 - from .pal24 import process_pal24_payment_amount - 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 - from .yookassa import process_yookassa_payment_amount - 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 - from .yookassa import process_yookassa_sbp_payment_amount - 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 - from .mulenpay import process_mulenpay_payment_amount - 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 - from .pal24 import process_pal24_payment_amount - 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 - from .cryptobot import process_cryptobot_payment_amount - async with AsyncSessionLocal() as db: - await process_cryptobot_payment_amount( - callback.message, db_user, db, amount_kopeks, state - ) - elif method == "stars": - from .stars import process_stars_payment_amount - await process_stars_payment_amount( - callback.message, db_user, amount_kopeks, state - ) - elif method == "tribute": - from .tribute import start_tribute_payment - 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_balance_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" - ) - - from .stars import start_stars_payment - dp.callback_query.register( - start_stars_payment, - F.data == "topup_stars" - ) - - from .yookassa import start_yookassa_payment - dp.callback_query.register( - start_yookassa_payment, - F.data == "topup_yookassa" - ) - - from .yookassa import start_yookassa_sbp_payment - dp.callback_query.register( - start_yookassa_sbp_payment, - F.data == "topup_yookassa_sbp" - ) - - from .mulenpay import start_mulenpay_payment - dp.callback_query.register( - start_mulenpay_payment, - F.data == "topup_mulenpay" - ) - - from .pal24 import start_pal24_payment - dp.callback_query.register( - start_pal24_payment, - F.data == "topup_pal24" - ) - - from .yookassa import check_yookassa_payment_status - dp.callback_query.register( - check_yookassa_payment_status, - F.data.startswith("check_yookassa_") - ) - - from .tribute import start_tribute_payment - dp.callback_query.register( - start_tribute_payment, - F.data == "topup_tribute" - ) - - dp.callback_query.register( - request_support_topup, - F.data == "topup_support" - ) - - from .yookassa import check_yookassa_payment_status - dp.callback_query.register( - check_yookassa_payment_status, - F.data.startswith("check_yookassa_") - ) - - dp.message.register( - process_topup_amount, - BalanceStates.waiting_for_amount - ) - - from .cryptobot import start_cryptobot_payment - dp.callback_query.register( - start_cryptobot_payment, - F.data == "topup_cryptobot" - ) - - from .cryptobot import check_cryptobot_payment_status - dp.callback_query.register( - check_cryptobot_payment_status, - F.data.startswith("check_cryptobot_") - ) - - from .mulenpay import check_mulenpay_payment_status - dp.callback_query.register( - check_mulenpay_payment_status, - F.data.startswith("check_mulenpay_") - ) - - from .pal24 import check_pal24_payment_status - 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|") - ) \ No newline at end of file diff --git a/app/handlers/balance/mulenpay.py b/app/handlers/balance/mulenpay.py deleted file mode 100644 index f6fb5362..00000000 --- a/app/handlers/balance/mulenpay.py +++ /dev/null @@ -1,238 +0,0 @@ -import logging -from aiogram import types -from aiogram.fsm.context import FSMContext -from sqlalchemy.ext.asyncio import AsyncSession - -from app.config import settings -from app.database.models import User -from app.keyboards.inline import get_back_keyboard -from app.localization.texts import get_texts -from app.services.payment_service import PaymentService -from app.utils.decorators import error_handler -from app.states import BalanceStates - -logger = logging.getLogger(__name__) - - -@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: - from .main import get_quick_amount_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 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 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) \ No newline at end of file diff --git a/app/handlers/balance/pal24.py b/app/handlers/balance/pal24.py deleted file mode 100644 index 2c68120a..00000000 --- a/app/handlers/balance/pal24.py +++ /dev/null @@ -1,364 +0,0 @@ -import html -import logging -from aiogram import types -from aiogram.fsm.context import FSMContext -from sqlalchemy.ext.asyncio import AsyncSession - -from app.config import settings -from app.database.models import User -from app.keyboards.inline import get_back_keyboard -from app.localization.texts import get_texts -from app.services.payment_service import PaymentService -from app.utils.decorators import error_handler -from app.states import BalanceStates - -logger = logging.getLogger(__name__) - - -@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 - - # Формируем текст сообщения в зависимости от доступных способов оплаты - 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", - ( - f"🏦 Оплата через PayPalych ({payment_methods_text})\n\n" - "Введите сумму для пополнения от 100 до 1 000 000 ₽.\n" - f"Оплата проходит через PayPalych ({payment_methods_text})." - ), - ) - - keyboard = get_back_keyboard(db_user.language) - - if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: - from .main import get_quick_amount_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 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_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) \ No newline at end of file diff --git a/app/handlers/balance/stars.py b/app/handlers/balance/stars.py deleted file mode 100644 index eac14f0a..00000000 --- a/app/handlers/balance/stars.py +++ /dev/null @@ -1,103 +0,0 @@ -import logging -from aiogram import types -from aiogram.fsm.context import FSMContext -from sqlalchemy.ext.asyncio import AsyncSession - -from app.config import settings -from app.database.models import User -from app.keyboards.inline import get_back_keyboard, get_payment_methods_keyboard -from app.localization.texts import get_texts -from app.services.payment_service import PaymentService -from app.utils.decorators import error_handler -from app.external.telegram_stars import TelegramStarsService - -logger = logging.getLogger(__name__) - - -@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: - from .main import get_quick_amount_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 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: - 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("⚠️ Ошибка создания платежа") \ No newline at end of file diff --git a/app/handlers/balance/tribute.py b/app/handlers/balance/tribute.py deleted file mode 100644 index e6d24015..00000000 --- a/app/handlers/balance/tribute.py +++ /dev/null @@ -1,59 +0,0 @@ -import logging -from aiogram import types - -from app.config import settings -from app.database.models import User -from app.keyboards.inline import get_back_keyboard -from app.localization.texts import get_texts -from app.utils.decorators import error_handler - -logger = logging.getLogger(__name__) - - -@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() \ No newline at end of file diff --git a/app/handlers/balance/yookassa.py b/app/handlers/balance/yookassa.py deleted file mode 100644 index c7e9fe97..00000000 --- a/app/handlers/balance/yookassa.py +++ /dev/null @@ -1,340 +0,0 @@ -import logging -from aiogram import types -from aiogram.fsm.context import FSMContext -from sqlalchemy.ext.asyncio import AsyncSession - -from app.config import settings -from app.database.models import User -from app.keyboards.inline import get_back_keyboard -from app.localization.texts import get_texts -from app.services.payment_service import PaymentService -from app.utils.decorators import error_handler -from app.states import BalanceStates - -logger = logging.getLogger(__name__) - - -@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: - from .main import get_quick_amount_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: - from .main import get_quick_amount_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 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 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) \ No newline at end of file