diff --git a/app/handlers/admin/bot_configuration.py b/app/handlers/admin/bot_configuration.py index 7b6b521c..9c26c893 100644 --- a/app/handlers/admin/bot_configuration.py +++ b/app/handlers/admin/bot_configuration.py @@ -1,24 +1,44 @@ import math +import logging import time -from typing import Iterable, List, Tuple +import uuid +from typing import Iterable, List, Optional, Tuple from aiogram import Dispatcher, F, types from aiogram.filters import BaseFilter, StateFilter from aiogram.fsm.context import FSMContext +from aiogram.types import LabeledPrice from sqlalchemy.ext.asyncio import AsyncSession +from app.config import settings from app.database.models import User from app.localization.texts import get_texts +from app.services.payment_service import PaymentService from app.services.remnawave_service import RemnaWaveService +from app.services.tribute_service import TributeService from app.services.system_settings_service import bot_configuration_service 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 SETTINGS_PAGE_SIZE = 8 +logger = logging.getLogger(__name__) + + +PAYMENT_TEST_BUTTONS: dict[str, str] = { + "YOOKASSA": "🧪 Тест: 10 ₽", + "TRIBUTE": "🧪 Тест: Tribute", + "MULENPAY": "🧪 Тест: 1 ₽", + "PAL24": "🧪 Тест: 10 ₽", + "TELEGRAM": "🧪 Тест: 1 ⭐", + "CRYPTOBOT": "🧪 Тест: 100 ₽", +} + + CATEGORY_GROUP_DEFINITIONS: Tuple[Tuple[str, str, Tuple[str, ...]], ...] = ( ( "telegram_bot", @@ -337,6 +357,18 @@ def _build_settings_keyboard( ] ) + if category_key in PAYMENT_TEST_BUTTONS: + rows.append( + [ + types.InlineKeyboardButton( + text=PAYMENT_TEST_BUTTONS[category_key], + callback_data=( + f"botcfg_test_payment:{category_key}:{group_key}:{category_key}:{category_page}:{page}" + ), + ) + ] + ) + for definition in sliced: value_preview = bot_configuration_service.format_value_for_list(definition.key) button_text = f"{definition.display_name} · {value_preview}" @@ -625,6 +657,299 @@ 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) + provider = parts[1] if len(parts) > 1 else "" + group_key = parts[2] if len(parts) > 2 else CATEGORY_FALLBACK_KEY + category_key = parts[3] if len(parts) > 3 else provider or CATEGORY_FALLBACK_KEY + + try: + category_page = max(1, int(parts[4])) if len(parts) > 4 else 1 + except ValueError: + category_page = 1 + + try: + settings_page = max(1, int(parts[5])) if len(parts) > 5 else 1 + except ValueError: + settings_page = 1 + + if provider not in PAYMENT_TEST_BUTTONS: + await callback.answer("Тестирование недоступно для этой категории", show_alert=True) + return + + texts = get_texts(db_user.language) + payment_service = PaymentService(callback.bot) + + message_text: Optional[str] = None + keyboard: Optional[types.InlineKeyboardMarkup] = None + status_message = "✅ Готово" + show_alert = False + + if provider == "YOOKASSA": + if not settings.is_yookassa_enabled(): + await callback.answer("❌ YooKassa отключена", show_alert=True) + return + + result = await payment_service.create_yookassa_payment( + db=db, + user_id=db_user.id, + amount_kopeks=1000, + description="Тестовый платеж YooKassa (админ)", + metadata={ + "test_payment": "true", + "initiator": "admin_bot_config", + }, + ) + + if not result or not result.get("confirmation_url"): + await callback.answer("❌ Не удалось создать платеж YooKassa", show_alert=True) + return + + confirmation_url = result["confirmation_url"] + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="💳 Перейти к оплате", + url=confirmation_url, + ) + ] + ] + ) + + message_text = ( + "🧪 Тестовый платеж YooKassa\n\n" + f"ID: {result['yookassa_payment_id']}\n" + f"Сумма: {texts.format_price(1000)}" + ) + + elif provider == "TRIBUTE": + if not settings.TRIBUTE_ENABLED: + await callback.answer("❌ Tribute отключен", show_alert=True) + return + + tribute_service = TributeService(callback.bot) + payment_url = await tribute_service.create_payment_link( + user_id=db_user.telegram_id, + amount_kopeks=0, + description="Тестовый платеж Tribute (админ)", + ) + + if not payment_url: + await callback.answer("❌ Не удалось создать ссылку Tribute", show_alert=True) + return + + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="💳 Перейти к оплате", + url=payment_url, + ) + ] + ] + ) + + message_text = ( + "🧪 Тестовая ссылка Tribute\n\n" + "Откройте форму оплаты и выполните тестовый платеж." + ) + + elif provider == "MULENPAY": + if not settings.is_mulenpay_enabled(): + await callback.answer("❌ MulenPay отключен", show_alert=True) + return + + result = await payment_service.create_mulenpay_payment( + db=db, + user_id=db_user.id, + amount_kopeks=100, + description="Тестовый платеж MulenPay (админ)", + language=db_user.language, + ignore_limits=True, + ) + + payment_url = result.get("payment_url") if result else None + + if not payment_url: + await callback.answer("❌ Не удалось создать платеж MulenPay", show_alert=True) + return + + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="💳 Перейти к оплате", + url=payment_url, + ) + ] + ] + ) + + message_text = ( + "🧪 Тестовый платеж MulenPay\n\n" + f"UUID: {result.get('uuid')}\n" + f"Сумма: {texts.format_price(100)}" + ) + + elif provider == "PAL24": + if not settings.is_pal24_enabled(): + await callback.answer("❌ PayPalych отключен", show_alert=True) + return + + result = await payment_service.create_pal24_payment( + db=db, + user_id=db_user.id, + amount_kopeks=1000, + description="Тестовый счет PayPalych (админ)", + language=db_user.language or "ru", + ignore_limits=True, + ) + + link_url = None + if result: + link_url = result.get("link_url") or result.get("link_page_url") + + if not link_url: + await callback.answer("❌ Не удалось создать счет PayPalych", show_alert=True) + return + + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="💳 Перейти к оплате", + url=link_url, + ) + ] + ] + ) + + message_text = ( + "🧪 Тестовый счет PayPalych\n\n" + f"Bill ID: {result.get('bill_id')}\n" + f"Сумма: {texts.format_price(1000)}" + ) + + elif provider == "TELEGRAM": + if not settings.TELEGRAM_STARS_ENABLED: + await callback.answer("❌ Telegram Stars отключены", show_alert=True) + return + + payload = f"test_stars_{db_user.id}_{uuid.uuid4().hex}" + + try: + invoice_link = await callback.bot.create_invoice_link( + title="Тестовый платеж Stars", + description="Тестовый платеж на 1 ⭐", + payload=payload, + provider_token="", + currency="XTR", + prices=[LabeledPrice(label="Тестовый платеж", amount=1)], + ) + except Exception as error: + logger.error("Ошибка создания Stars инвойса: %s", error, exc_info=True) + invoice_link = None + + if not invoice_link: + await callback.answer("❌ Не удалось создать счет Stars", show_alert=True) + return + + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="⭐ Оплатить 1 звезду", + url=invoice_link, + ) + ] + ] + ) + + message_text = ( + "🧪 Тестовый платеж Telegram Stars\n\n" + "Нажмите кнопку, чтобы открыть счет на 1 ⭐." + ) + + elif provider == "CRYPTOBOT": + if not settings.is_cryptobot_enabled(): + await callback.answer("❌ CryptoBot отключен", show_alert=True) + return + + rate = await currency_converter.get_usd_to_rub_rate() + if not rate or rate <= 0: + rate = 100.0 + amount_usd = round(100 / rate, 2) + if amount_usd < 1: + amount_usd = 1.0 + + result = await payment_service.create_cryptobot_payment( + db=db, + user_id=db_user.id, + amount_usd=amount_usd, + asset=settings.CRYPTOBOT_DEFAULT_ASSET, + description="Тестовый платеж CryptoBot (админ) на 100 ₽", + payload=f"test_cryptobot_{db_user.id}_{uuid.uuid4().hex}", + ) + + payment_url = None + if result: + payment_url = ( + result.get("bot_invoice_url") + or result.get("mini_app_invoice_url") + or result.get("web_app_invoice_url") + ) + + if not payment_url: + await callback.answer("❌ Не удалось создать счет CryptoBot", show_alert=True) + return + + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="🪙 Перейти к оплате", + url=payment_url, + ) + ] + ] + ) + + message_text = ( + "🧪 Тестовый платеж CryptoBot\n\n" + f"Сумма: 100 ₽ (≈{amount_usd:.2f} USD)" + ) + + if message_text: + await callback.message.answer( + message_text, + reply_markup=keyboard, + parse_mode="HTML", + ) + + definitions = bot_configuration_service.get_settings_for_category(category_key) + if definitions: + keyboard_markup = _build_settings_keyboard( + category_key, + group_key, + category_page, + db_user.language, + settings_page, + ) + try: + await callback.message.edit_reply_markup(reply_markup=keyboard_markup) + except Exception: + pass + + await callback.answer(status_message, show_alert=show_alert) + + @admin_required @error_handler async def show_bot_config_setting( @@ -964,6 +1289,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:"), diff --git a/app/services/payment_service.py b/app/services/payment_service.py index 63d07597..cd6e7087 100644 --- a/app/services/payment_service.py +++ b/app/services/payment_service.py @@ -716,27 +716,30 @@ class PaymentService: amount_kopeks: int, description: str, language: Optional[str] = None, + *, + ignore_limits: bool = False, ) -> Optional[Dict[str, Any]]: if not self.mulenpay_service: logger.error("MulenPay сервис не инициализирован") return None - if amount_kopeks < settings.MULENPAY_MIN_AMOUNT_KOPEKS: - logger.warning( - "Сумма MulenPay меньше минимальной: %s < %s", - amount_kopeks, - settings.MULENPAY_MIN_AMOUNT_KOPEKS, - ) - return None + if not ignore_limits: + if amount_kopeks < settings.MULENPAY_MIN_AMOUNT_KOPEKS: + logger.warning( + "Сумма MulenPay меньше минимальной: %s < %s", + amount_kopeks, + settings.MULENPAY_MIN_AMOUNT_KOPEKS, + ) + return None - if amount_kopeks > settings.MULENPAY_MAX_AMOUNT_KOPEKS: - logger.warning( - "Сумма MulenPay больше максимальной: %s > %s", - amount_kopeks, - settings.MULENPAY_MAX_AMOUNT_KOPEKS, - ) - return None + if amount_kopeks > settings.MULENPAY_MAX_AMOUNT_KOPEKS: + logger.warning( + "Сумма MulenPay больше максимальной: %s > %s", + amount_kopeks, + settings.MULENPAY_MAX_AMOUNT_KOPEKS, + ) + return None try: payment_uuid = f"mulen_{user_id}_{uuid.uuid4().hex}" @@ -818,27 +821,29 @@ class PaymentService: language: str, ttl_seconds: Optional[int] = None, payer_email: Optional[str] = None, + ignore_limits: bool = False, ) -> Optional[Dict[str, Any]]: if not self.pal24_service or not self.pal24_service.is_configured: logger.error("Pal24 сервис не инициализирован") return None - if amount_kopeks < settings.PAL24_MIN_AMOUNT_KOPEKS: - logger.warning( - "Сумма Pal24 меньше минимальной: %s < %s", - amount_kopeks, - settings.PAL24_MIN_AMOUNT_KOPEKS, - ) - return None + if not ignore_limits: + if amount_kopeks < settings.PAL24_MIN_AMOUNT_KOPEKS: + logger.warning( + "Сумма Pal24 меньше минимальной: %s < %s", + amount_kopeks, + settings.PAL24_MIN_AMOUNT_KOPEKS, + ) + return None - if amount_kopeks > settings.PAL24_MAX_AMOUNT_KOPEKS: - logger.warning( - "Сумма Pal24 больше максимальной: %s > %s", - amount_kopeks, - settings.PAL24_MAX_AMOUNT_KOPEKS, - ) - return None + if amount_kopeks > settings.PAL24_MAX_AMOUNT_KOPEKS: + logger.warning( + "Сумма Pal24 больше максимальной: %s > %s", + amount_kopeks, + settings.PAL24_MAX_AMOUNT_KOPEKS, + ) + return None order_id = f"pal24_{user_id}_{uuid.uuid4().hex}"