diff --git a/app/handlers/admin/bot_configuration.py b/app/handlers/admin/bot_configuration.py index 7b6b521c..01ca5c52 100644 --- a/app/handlers/admin/bot_configuration.py +++ b/app/handlers/admin/bot_configuration.py @@ -1,6 +1,6 @@ import math import time -from typing import Iterable, List, Tuple +from typing import Iterable, List, Optional, Tuple from aiogram import Dispatcher, F, types from aiogram.filters import BaseFilter, StateFilter @@ -8,11 +8,15 @@ from aiogram.fsm.context import FSMContext from sqlalchemy.ext.asyncio import AsyncSession from app.database.models import User +from app.config import settings from app.localization.texts import get_texts +from app.services.payment_service import PaymentService from app.services.remnawave_service import RemnaWaveService from app.services.system_settings_service import bot_configuration_service +from app.services.tribute_service import TributeService from app.states import BotConfigStates from app.utils.decorators import admin_required, error_handler +from app.utils.currency_converter import currency_converter CATEGORY_PAGE_SIZE = 10 @@ -96,6 +100,15 @@ CATEGORY_GROUP_DEFINITIONS: Tuple[Tuple[str, str, Tuple[str, ...]], ...] = ( CATEGORY_FALLBACK_KEY = "other" CATEGORY_FALLBACK_TITLE = "📦 Прочие настройки" +PAYMENT_TEST_DEFINITIONS: dict[str, tuple[str, str]] = { + "YOOKASSA": ("🧪 Тестовый платеж — YooKassa", "yookassa"), + "TRIBUTE": ("🧪 Тестовый платеж — Tribute", "tribute"), + "MULENPAY": ("🧪 Тестовый платеж — MulenPay", "mulenpay"), + "PAL24": ("🧪 Тестовый платеж — PayPalych", "pal24"), + "TELEGRAM": ("🧪 Тестовый платеж — Telegram Stars", "telegram"), + "CRYPTOBOT": ("🧪 Тестовый платеж — CryptoBot", "cryptobot"), +} + async def _store_setting_context( state: FSMContext, @@ -337,6 +350,12 @@ def _build_settings_keyboard( ] ) + test_button = _build_payment_test_button( + category_key, group_key, category_page, page + ) + if test_button: + rows.append([test_button]) + for definition in sliced: value_preview = bot_configuration_service.format_value_for_list(definition.key) button_text = f"{definition.display_name} · {value_preview}" @@ -391,6 +410,28 @@ def _build_settings_keyboard( return types.InlineKeyboardMarkup(inline_keyboard=rows) +def _build_payment_test_button( + category_key: str, + group_key: str, + category_page: int, + page: int, +) -> Optional[types.InlineKeyboardButton]: + mapping = PAYMENT_TEST_DEFINITIONS.get(category_key) + if not mapping: + return None + + button_text, method_key = mapping + callback_data = ( + f"botcfg_test_payment:{group_key}:{category_key}:{category_page}:{page}:{method_key}" + ) + + if len(callback_data) > 64: + # Fallback without group key if callback too long (safety) + callback_data = f"botcfg_test_payment::{category_key}:{category_page}:{page}:{method_key}" + + return types.InlineKeyboardButton(text=button_text, callback_data=callback_data) + + def _build_setting_keyboard( key: str, group_key: str, @@ -625,6 +666,279 @@ async def test_remnawave_connection( await callback.answer(message, show_alert=True) +@admin_required +@error_handler +async def test_payment_system( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, +): + parts = callback.data.split(":", 6) + group_key = parts[1] if len(parts) > 1 and parts[1] else CATEGORY_FALLBACK_KEY + category_key = parts[2] if len(parts) > 2 else "" + + try: + category_page = max(1, int(parts[3])) if len(parts) > 3 else 1 + except ValueError: + category_page = 1 + + try: + settings_page = max(1, int(parts[4])) if len(parts) > 4 else 1 + except ValueError: + settings_page = 1 + + method_key = parts[5] if len(parts) > 5 else "" + service = PaymentService(bot=callback.bot) + + alert_message = "❌ Не удалось выполнить тестовый платеж" + details_text: Optional[str] = None + reply_markup: Optional[types.InlineKeyboardMarkup] = None + success = False + user_language = db_user.language or getattr(settings, "DEFAULT_LANGUAGE", "ru") + + if method_key == "yookassa": + amount_kopeks = 10 * 100 + if not service.yookassa_service: + alert_message = "⚠️ YooKassa не настроена" + else: + payment = await service.create_yookassa_payment( + db, + db_user.id, + amount_kopeks, + "Тестовый платеж YooKassa (админ)", + metadata={ + "origin": "admin_test", + "provider": "yookassa", + }, + ) + if payment and payment.get("confirmation_url"): + alert_message = "✅ Платеж YooKassa создан" + success = True + confirmation_url = payment["confirmation_url"] + details_text = ( + "🧪 Тестовый платеж YooKassa\n\n" + f"Сумма: {settings.format_price(amount_kopeks)}\n" + f"ID: {payment['yookassa_payment_id']}\n\n" + "Перейдите по кнопке ниже, чтобы открыть страницу оплаты." + ) + reply_markup = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="💳 Открыть платеж", + url=confirmation_url, + ) + ] + ] + ) + else: + alert_message = "❌ Ошибка создания платежа YooKassa" + + elif method_key == "tribute": + tribute_service = TributeService(callback.bot) + payment_url = await tribute_service.create_payment_link( + user_id=db_user.telegram_id, + amount_kopeks=0, + description="Тестовое пополнение (админ)", + ) + if payment_url: + alert_message = "✅ Ссылка Tribute сформирована" + success = True + details_text = ( + "🧪 Тестовая оплата Tribute\n\n" + "Откройте платежную страницу и укажите сумму вручную, например 10 ₽." + ) + reply_markup = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="💳 Перейти к оплате", + url=payment_url, + ) + ] + ] + ) + else: + alert_message = "❌ Tribute недоступен или не настроен" + + elif method_key == "mulenpay": + amount_kopeks = 1 * 100 + if not service.mulenpay_service: + alert_message = "⚠️ MulenPay не настроен" + else: + payment = await service.create_mulenpay_payment( + db, + user_id=db_user.id, + amount_kopeks=amount_kopeks, + description="Тестовый платеж MulenPay (админ)", + language=user_language, + ) + if payment and payment.get("payment_url"): + alert_message = "✅ Платеж MulenPay создан" + success = True + details_text = ( + "🧪 Тестовый платеж MulenPay\n\n" + f"Сумма: {settings.format_price(amount_kopeks)}\n" + f"UUID: {payment['uuid']}\n\n" + "Перейдите по кнопке ниже, чтобы открыть счет." + ) + reply_markup = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="💳 Открыть счет", + url=payment["payment_url"], + ) + ] + ] + ) + else: + alert_message = "❌ Не удалось создать платеж MulenPay" + + elif method_key == "pal24": + amount_kopeks = 10 * 100 + if not service.pal24_service or not service.pal24_service.is_configured: + alert_message = "⚠️ Pal24 не настроен" + else: + payment = await service.create_pal24_payment( + db, + user_id=db_user.id, + amount_kopeks=amount_kopeks, + description="Тестовый платеж Pal24 (админ)", + language=user_language, + ) + link = payment.get("link_url") if payment else None + if link: + alert_message = "✅ Счет Pal24 создан" + success = True + details_text = ( + "🧪 Тестовый счет Pal24\n\n" + f"Сумма: {settings.format_price(amount_kopeks)}\n" + f"ID счета: {payment['bill_id']}\n\n" + "Перейдите по кнопке ниже, чтобы открыть счет." + ) + reply_markup = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="💳 Открыть счет", + url=link, + ) + ] + ] + ) + else: + alert_message = "❌ Не удалось создать счет Pal24" + + elif method_key == "telegram": + if not service.stars_service: + alert_message = "⚠️ Telegram Stars недоступны" + else: + rubles_amount = settings.stars_to_rubles(1) + amount_kopeks = max(1, int(rubles_amount * 100)) + invoice_link = await service.create_stars_invoice( + amount_kopeks=amount_kopeks, + description="Тестовый платеж Telegram Stars (1 ⭐)", + payload=f"admin_stars_test_{db_user.id}", + ) + if invoice_link: + alert_message = "✅ Ссылка Telegram Stars создана" + success = True + details_text = ( + "🧪 Тестовый платеж Telegram Stars\n\n" + "Сумма: 1 ⭐ (≈" + f"{settings.format_price(amount_kopeks)}).\n\n" + "Откройте ссылку ниже, чтобы оплатить." + ) + reply_markup = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="⭐ Открыть счет", + url=invoice_link, + ) + ] + ] + ) + else: + alert_message = "❌ Не удалось создать счет Telegram Stars" + + elif method_key == "cryptobot": + if not service.cryptobot_service: + alert_message = "⚠️ CryptoBot не настроен" + else: + rub_amount = 100 + usd_amount = await currency_converter.rub_to_usd(float(rub_amount)) + usd_amount = max(0.01, round(usd_amount, 2)) + payment = await service.create_cryptobot_payment( + db, + user_id=db_user.id, + amount_usd=usd_amount, + description="Тестовый платеж CryptoBot (админ)", + payload=f"admin_cryptobot_test_{db_user.id}", + ) + link = None + if payment: + link = ( + payment.get("web_app_invoice_url") + or payment.get("mini_app_invoice_url") + or payment.get("bot_invoice_url") + ) + if link: + alert_message = "✅ Счет CryptoBot создан" + success = True + usd_amount_str = f"{usd_amount:.2f}" + details_text = ( + "🧪 Тестовый платеж CryptoBot\n\n" + "Сумма: ≈100 ₽ (" + f"{usd_amount_str} USDT).\n" + f"Invoice ID: {payment['invoice_id']}\n\n" + "Перейдите по кнопке, чтобы открыть счет." + ) + reply_markup = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="💠 Открыть счет", + url=link, + ) + ] + ] + ) + else: + alert_message = "❌ Не удалось создать счет CryptoBot" + + else: + alert_message = "⚠️ Тестирование для этого провайдера недоступно" + + definitions = bot_configuration_service.get_settings_for_category(category_key) + if definitions: + keyboard = _build_settings_keyboard( + category_key, + group_key, + category_page, + user_language, + settings_page, + ) + try: + await callback.message.edit_reply_markup(reply_markup=keyboard) + except Exception: + pass + + if details_text and success: + try: + await callback.message.answer( + details_text, + reply_markup=reply_markup, + parse_mode="HTML", + ) + except Exception: + success = False + alert_message = "❌ Не удалось отправить сообщение с платежом" + + await callback.answer(alert_message, show_alert=not success) + + @admin_required @error_handler async def show_bot_config_setting( @@ -964,6 +1278,10 @@ def register_handlers(dp: Dispatcher) -> None: test_remnawave_connection, F.data.startswith("botcfg_test_remnawave:"), ) + dp.callback_query.register( + test_payment_system, + F.data.startswith("botcfg_test_payment:"), + ) dp.callback_query.register( show_bot_config_setting, F.data.startswith("botcfg_setting:"),