"""Обработчики для простой покупки подписки.""" import html import logging from datetime import datetime from typing import Any, Dict, Optional, Tuple from aiogram import types, F from aiogram.exceptions import TelegramBadRequest 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_happ_download_button_row from app.localization.texts import get_texts from app.services.payment_service import PaymentService from app.services.subscription_purchase_service import SubscriptionPurchaseService from app.utils.decorators import error_handler from app.states import SubscriptionStates from app.utils.subscription_utils import ( get_display_subscription_link, resolve_simple_subscription_device_limit, ) from app.utils.pricing_utils import compute_simple_subscription_price logger = logging.getLogger(__name__) @error_handler async def start_simple_subscription_purchase( callback: types.CallbackQuery, db_user: User, state: FSMContext, db: AsyncSession, ): """Начинает процесс простой покупки подписки.""" texts = get_texts(db_user.language) if not settings.SIMPLE_SUBSCRIPTION_ENABLED: await callback.answer("❌ Простая покупка подписки временно недоступна", show_alert=True) return # Проверка ограничения на покупку/продление подписки if getattr(db_user, 'restriction_subscription', False): reason = getattr(db_user, 'restriction_reason', None) or "Действие ограничено администратором" support_url = settings.get_support_contact_url() keyboard = [] if support_url: keyboard.append([types.InlineKeyboardButton(text="🆘 Обжаловать", url=support_url)]) keyboard.append([types.InlineKeyboardButton(text=texts.BACK, callback_data="subscription")]) await callback.message.edit_text( f"🚫 Покупка подписки ограничена\n\n{reason}\n\n" "Если вы считаете это ошибкой, вы можете обжаловать решение.", reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard) ) await callback.answer() return # Проверяем, есть ли у пользователя подписка from app.database.crud.subscription import get_subscription_by_user_id current_subscription = await get_subscription_by_user_id(db, db_user.id) device_limit = resolve_simple_subscription_device_limit() # При продлении учитываем количество устройств из текущей подписки if current_subscription and settings.is_devices_selection_enabled(): current_device_limit = current_subscription.device_limit or device_limit # Модем добавляет +1 к device_limit, но оплачивается отдельно if getattr(current_subscription, 'modem_enabled', False): current_device_limit = max(1, current_device_limit - 1) # Используем максимум из текущего и дефолтного device_limit = max(device_limit, current_device_limit) # Подготовим параметры простой подписки subscription_params = { "period_days": settings.SIMPLE_SUBSCRIPTION_PERIOD_DAYS, "device_limit": device_limit, "traffic_limit_gb": settings.SIMPLE_SUBSCRIPTION_TRAFFIC_GB, "squad_uuid": settings.SIMPLE_SUBSCRIPTION_SQUAD_UUID } # Сохраняем параметры в состояние await state.update_data(subscription_params=subscription_params) data = await state.get_data() resolved_squad_uuid = await _ensure_simple_subscription_squad_uuid( db, state, subscription_params, user_id=db_user.id, state_data=data, ) price_kopeks, price_breakdown = await _calculate_simple_subscription_price( db, subscription_params, user=db_user, resolved_squad_uuid=resolved_squad_uuid, ) period_days = subscription_params["period_days"] user_balance_kopeks = getattr(db_user, "balance_kopeks", 0) logger.warning( "SIMPLE_SUBSCRIPTION_DEBUG_START | user=%s | period=%s | base=%s | traffic=%s | devices=%s | servers=%s | discount=%s | total=%s | squads=%s", db_user.id, period_days, price_breakdown.get("base_price", 0), price_breakdown.get("traffic_price", 0), price_breakdown.get("devices_price", 0), price_breakdown.get("servers_price", 0), price_breakdown.get("total_discount", 0), price_kopeks, ",".join(price_breakdown.get("resolved_squad_uuids", [])) if price_breakdown.get("resolved_squad_uuids") else "none", ) can_pay_from_balance = user_balance_kopeks >= price_kopeks logger.warning( "SIMPLE_SUBSCRIPTION_DEBUG_START_BALANCE | user=%s | balance=%s | min_required=%s | can_pay=%s", db_user.id, user_balance_kopeks, price_kopeks, can_pay_from_balance, ) # Проверяем, является ли у пользователя текущая подписка активной платной подпиской has_active_paid_subscription = False trial_notice = "" if current_subscription: if not getattr(current_subscription, "is_trial", False) and current_subscription.is_active: # Это платная активная подписка - требуем подтверждение has_active_paid_subscription = True elif getattr(current_subscription, "is_trial", False): # Это тестовая подписка try: days_left = max(0, (current_subscription.end_date - datetime.utcnow()).days) except Exception: days_left = 0 key = "SIMPLE_SUBSCRIPTION_TRIAL_NOTICE_ACTIVE" if current_subscription.is_active else "SIMPLE_SUBSCRIPTION_TRIAL_NOTICE_TRIAL" trial_notice = texts.t( key, "ℹ️ У вас уже есть триальная подписка. Она истекает через {days} дн.", ).format(days=days_left) server_label = _get_simple_subscription_server_label( texts, subscription_params, resolved_squad_uuid, ) show_devices = settings.is_devices_selection_enabled() message_lines = [ "⚡ Простая покупка подписки", "", f"📅 Период: {subscription_params['period_days']} дней", ] if show_devices: message_lines.append(f"📱 Устройства: {subscription_params['device_limit']}") traffic_limit_gb = subscription_params["traffic_limit_gb"] traffic_label = "Безлимит" if traffic_limit_gb == 0 else f"{traffic_limit_gb} ГБ" message_lines.extend([ f"📊 Трафик: {traffic_label}", f"🌍 Сервер: {server_label}", "", f"💰 Стоимость: {settings.format_price(price_kopeks)}", f"💳 Ваш баланс: {settings.format_price(user_balance_kopeks)}", "", ]) # Если у пользователя уже есть активная платная подписка, требуем подтверждение if has_active_paid_subscription: # У пользователя уже есть активная платная подписка message_lines.append( "⚠️ У вас уже есть активная платная подписка. " "Покупка простой подписки изменит параметры вашей текущей подписки. " "Требуется подтверждение." ) message_text = "\n".join(message_lines) # Клавиатура с подтверждением keyboard_rows = [ [types.InlineKeyboardButton( text="✅ Подтвердить покупку", callback_data="simple_subscription_confirm_purchase" )], [types.InlineKeyboardButton( text=texts.BACK, callback_data="subscription_purchase" )] ] keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows) else: # У пользователя нет активной платной подписки (или есть только пробная) # Показываем стандартный выбор метода оплаты if can_pay_from_balance: message_lines.append( "Вы можете оплатить подписку с баланса или выбрать другой способ оплаты." ) else: message_lines.append( "Баланс пока недостаточный для мгновенной оплаты. Выберите подходящий способ оплаты:" ) message_text = "\n".join(message_lines) if trial_notice: message_text = f"{trial_notice}\n\n{message_text}" methods_keyboard = _get_simple_subscription_payment_keyboard(db_user.language) keyboard_rows = [] if can_pay_from_balance: keyboard_rows.append([ types.InlineKeyboardButton( text="✅ Оплатить с баланса", callback_data="simple_subscription_pay_with_balance", ) ]) keyboard_rows.extend(methods_keyboard.inline_keyboard) keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows) await callback.message.edit_text( message_text, reply_markup=keyboard, parse_mode="HTML" ) # Устанавливаем соответствующее состояние if has_active_paid_subscription: await state.set_state(SubscriptionStates.waiting_for_simple_subscription_confirmation) else: await state.set_state(SubscriptionStates.waiting_for_simple_subscription_payment_method) await callback.answer() async def _calculate_simple_subscription_price( db: AsyncSession, params: dict, *, user: Optional[User] = None, resolved_squad_uuid: Optional[str] = None, ) -> Tuple[int, Dict[str, Any]]: """Рассчитывает цену простой подписки.""" resolved_uuids = [resolved_squad_uuid] if resolved_squad_uuid else None return await compute_simple_subscription_price( db, params, user=user, resolved_squad_uuids=resolved_uuids, ) def _get_simple_subscription_payment_keyboard(language: str) -> types.InlineKeyboardMarkup: """Создает клавиатуру с методами оплаты для простой подписки.""" texts = get_texts(language) keyboard = [] # Добавляем доступные методы оплаты if settings.TELEGRAM_STARS_ENABLED: keyboard.append([types.InlineKeyboardButton( text="⭐ Telegram Stars", callback_data="simple_subscription_stars" )]) if settings.is_yookassa_enabled(): yookassa_methods = [] if settings.YOOKASSA_SBP_ENABLED: yookassa_methods.append(types.InlineKeyboardButton( text="🏦 YooKassa (СБП)", callback_data="simple_subscription_yookassa_sbp" )) yookassa_methods.append(types.InlineKeyboardButton( text="💳 YooKassa (Карта)", callback_data="simple_subscription_yookassa" )) if yookassa_methods: keyboard.append(yookassa_methods) if settings.is_cryptobot_enabled(): keyboard.append([types.InlineKeyboardButton( text="🪙 CryptoBot", callback_data="simple_subscription_cryptobot" )]) if settings.is_heleket_enabled(): keyboard.append([types.InlineKeyboardButton( text="🪙 Heleket", callback_data="simple_subscription_heleket" )]) if settings.is_mulenpay_enabled(): mulenpay_name = settings.get_mulenpay_display_name() keyboard.append([types.InlineKeyboardButton( text=f"💳 {mulenpay_name}", callback_data="simple_subscription_mulenpay" )]) if settings.is_pal24_enabled(): keyboard.append([types.InlineKeyboardButton( text="💳 PayPalych", callback_data="simple_subscription_pal24" )]) if settings.is_wata_enabled(): keyboard.append([types.InlineKeyboardButton( text="💳 WATA", callback_data="simple_subscription_wata" )]) # Кнопка назад keyboard.append([types.InlineKeyboardButton( text=texts.BACK, callback_data="subscription_purchase" )]) return types.InlineKeyboardMarkup(inline_keyboard=keyboard) def _get_simple_subscription_server_label( texts, subscription_params: Dict[str, Any], resolved_squad_uuid: Optional[str] = None, ) -> str: """Возвращает локализованное описание выбранного сервера.""" if subscription_params.get("squad_uuid"): return texts.t("SIMPLE_SUBSCRIPTION_SERVER_SELECTED", "Выбранный") if resolved_squad_uuid: return texts.t( "SIMPLE_SUBSCRIPTION_SERVER_ASSIGNED", "Назначен автоматически", ) return texts.t("SIMPLE_SUBSCRIPTION_SERVER_ANY", "Любой доступный") async def _ensure_simple_subscription_squad_uuid( db: AsyncSession, state: FSMContext, subscription_params: Dict[str, Any], *, user_id: Optional[int] = None, state_data: Optional[Dict[str, Any]] = None, ) -> Optional[str]: """Определяет UUID сквада для простой подписки.""" explicit_uuid = subscription_params.get("squad_uuid") if explicit_uuid: return explicit_uuid if state_data is None: state_data = await state.get_data() resolved_uuid = state_data.get("resolved_squad_uuid") if resolved_uuid: return resolved_uuid try: from app.database.crud.server_squad import get_random_active_squad_uuid resolved_uuid = await get_random_active_squad_uuid(db) except Exception as error: # pragma: no cover - defensive logging logger.error( "SIMPLE_SUBSCRIPTION_RANDOM_SQUAD_ERROR | user=%s | error=%s", user_id, error, ) return None if resolved_uuid: await state.update_data(resolved_squad_uuid=resolved_uuid) logger.info( "SIMPLE_SUBSCRIPTION_RANDOM_SQUAD_ASSIGNED | user=%s | squad=%s", user_id, resolved_uuid, ) return resolved_uuid @error_handler async def handle_simple_subscription_pay_with_balance( callback: types.CallbackQuery, db_user: User, state: FSMContext, db: AsyncSession, ): """Обрабатывает оплату простой подписки с баланса.""" texts = get_texts(db_user.language) data = await state.get_data() subscription_params = data.get("subscription_params", {}) if not subscription_params: await callback.answer("❌ Данные подписки устарели. Пожалуйста, начните сначала.", show_alert=True) return # Проверяем, имеет ли пользователь активную платную подписку from app.database.crud.subscription import get_subscription_by_user_id current_subscription = await get_subscription_by_user_id(db, db_user.id) if current_subscription and not getattr(current_subscription, "is_trial", False) and current_subscription.is_active: # У пользователя есть активная платная подписка - требуем подтверждение await callback.answer("⚠️ У вас уже есть активная платная подписка. Пожалуйста, подтвердите покупку.", show_alert=True) return resolved_squad_uuid = await _ensure_simple_subscription_squad_uuid( db, state, subscription_params, user_id=db_user.id, state_data=data, ) # Рассчитываем цену подписки price_kopeks, price_breakdown = await _calculate_simple_subscription_price( db, subscription_params, user=db_user, resolved_squad_uuid=resolved_squad_uuid, ) total_required = price_kopeks logger.warning( "SIMPLE_SUBSCRIPTION_DEBUG_PAY_BALANCE | user=%s | period=%s | base=%s | traffic=%s | devices=%s | servers=%s | discount=%s | total_required=%s | balance=%s", db_user.id, subscription_params["period_days"], price_breakdown.get("base_price", 0), price_breakdown.get("traffic_price", 0), price_breakdown.get("devices_price", 0), price_breakdown.get("servers_price", 0), price_breakdown.get("total_discount", 0), total_required, getattr(db_user, "balance_kopeks", 0), ) # Проверяем баланс пользователя user_balance_kopeks = getattr(db_user, "balance_kopeks", 0) if user_balance_kopeks < total_required: await callback.answer("❌ Недостаточно средств на балансе для оплаты подписки", show_alert=True) return try: # Списываем средства с баланса пользователя from app.database.crud.user import subtract_user_balance success = await subtract_user_balance( db, db_user, price_kopeks, f"Оплата подписки на {subscription_params['period_days']} дней", consume_promo_offer=False, ) if not success: await callback.answer("❌ Ошибка списания средств с баланса", show_alert=True) return # Проверяем, есть ли у пользователя уже подписка from app.database.crud.subscription import get_subscription_by_user_id, extend_subscription existing_subscription = await get_subscription_by_user_id(db, db_user.id) if existing_subscription: # Если подписка уже существует (платная или тестовая), продлеваем её # Сохраняем информацию о текущей подписке, особенно является ли она пробной was_trial = getattr(existing_subscription, "is_trial", False) subscription = await extend_subscription( db=db, subscription=existing_subscription, days=subscription_params["period_days"] ) # Обновляем параметры подписки subscription.traffic_limit_gb = subscription_params["traffic_limit_gb"] subscription.device_limit = subscription_params["device_limit"] # Если текущая подписка была пробной, и мы обновляем её # нужно изменить статус подписки if was_trial: from app.database.models import SubscriptionStatus # Переводим подписку из пробной в активную платную subscription.status = SubscriptionStatus.ACTIVE.value subscription.is_trial = False # Устанавливаем новый выбранный сквад if resolved_squad_uuid: subscription.connected_squads = [resolved_squad_uuid] await db.commit() await db.refresh(subscription) else: # Если подписки нет, создаём новую from app.database.crud.subscription import create_paid_subscription subscription = await create_paid_subscription( db=db, user_id=db_user.id, duration_days=subscription_params["period_days"], traffic_limit_gb=subscription_params["traffic_limit_gb"], device_limit=subscription_params["device_limit"], connected_squads=[resolved_squad_uuid] if resolved_squad_uuid else [], update_server_counters=True, ) if not subscription: # Возвращаем средства на баланс в случае ошибки from app.services.payment_service import add_user_balance await add_user_balance( db, db_user.id, price_kopeks, f"Возврат средств за неудавшуюся подписку на {subscription_params['period_days']} дней", ) await callback.answer("❌ Ошибка создания подписки. Средства возвращены на баланс.", show_alert=True) return # Обновляем баланс пользователя await db.refresh(db_user) # Обновляем или создаём ссылку подписки в RemnaWave try: from app.services.subscription_service import SubscriptionService subscription_service = SubscriptionService() remnawave_user = await subscription_service.create_remnawave_user(db, subscription) if remnawave_user: await db.refresh(subscription) except Exception as sync_error: logger.error(f"Ошибка синхронизации подписки с RemnaWave для пользователя {db_user.id}: {sync_error}", exc_info=True) # Отправляем уведомление об успешной покупке server_label = _get_simple_subscription_server_label( texts, subscription_params, resolved_squad_uuid, ) show_devices = settings.is_devices_selection_enabled() success_lines = [ "✅ Подписка успешно активирована!", "", f"📅 Период: {subscription_params['period_days']} дней", ] if show_devices: success_lines.append(f"📱 Устройства: {subscription_params['device_limit']}") success_traffic_gb = subscription_params["traffic_limit_gb"] success_traffic_label = "Безлимит" if success_traffic_gb == 0 else f"{success_traffic_gb} ГБ" success_lines.extend([ f"📊 Трафик: {success_traffic_label}", f"🌍 Сервер: {server_label}", "", f"💰 Списано с баланса: {settings.format_price(price_kopeks)}", f"💳 Ваш баланс: {settings.format_price(db_user.balance_kopeks)}", "", "🔗 Для подключения перейдите в раздел 'Подключиться'", ]) success_message = "\n".join(success_lines) connect_mode = settings.CONNECT_BUTTON_MODE subscription_link = get_display_subscription_link(subscription) connect_button_text = texts.t("CONNECT_BUTTON", "🔗 Подключиться") def _fallback_connect_button() -> types.InlineKeyboardButton: return types.InlineKeyboardButton( text=connect_button_text, callback_data="subscription_connect", ) if connect_mode == "miniapp_subscription": if subscription_link: connect_row = [ types.InlineKeyboardButton( text=connect_button_text, web_app=types.WebAppInfo(url=subscription_link), ) ] else: connect_row = [_fallback_connect_button()] elif connect_mode == "miniapp_custom": custom_url = settings.MINIAPP_CUSTOM_URL if custom_url: connect_row = [ types.InlineKeyboardButton( text=connect_button_text, web_app=types.WebAppInfo(url=custom_url), ) ] else: connect_row = [_fallback_connect_button()] elif connect_mode == "link": if subscription_link: connect_row = [ types.InlineKeyboardButton( text=connect_button_text, url=subscription_link, ) ] else: connect_row = [_fallback_connect_button()] elif connect_mode == "happ_cryptolink": if subscription_link: connect_row = [ types.InlineKeyboardButton( text=connect_button_text, callback_data="open_subscription_link", ) ] else: connect_row = [_fallback_connect_button()] else: connect_row = [_fallback_connect_button()] keyboard_rows = [connect_row] happ_row = get_happ_download_button_row(texts) if happ_row: keyboard_rows.append(happ_row) keyboard_rows.append( [types.InlineKeyboardButton(text="🏠 Главное меню", callback_data="back_to_menu")] ) keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows) await callback.message.edit_text( success_message, reply_markup=keyboard, parse_mode="HTML" ) # Отправляем уведомление админам try: from app.services.admin_notification_service import AdminNotificationService notification_service = AdminNotificationService(callback.bot) await notification_service.send_subscription_purchase_notification( db, db_user, subscription, None, # transaction subscription_params["period_days"], False, # was_trial_conversion amount_kopeks=price_kopeks, ) except Exception as e: logger.error(f"Ошибка отправки уведомления админам о покупке: {e}") await state.clear() await callback.answer() logger.info(f"Пользователь {db_user.telegram_id} успешно купил подписку с баланса на {price_kopeks/100}₽") except Exception as error: logger.error( "Ошибка оплаты простой подписки с баланса для пользователя %s: %s", db_user.id, error, exc_info=True, ) await callback.answer( "❌ Ошибка оплаты подписки. Попробуйте позже или обратитесь в поддержку.", show_alert=True, ) await state.clear() @error_handler async def handle_simple_subscription_pay_with_balance_disabled( callback: types.CallbackQuery, db_user: User, state: FSMContext, db: AsyncSession, ): """Показывает уведомление, если баланса недостаточно для прямой оплаты.""" await callback.answer( "❌ Недостаточно средств на балансе. Пополните баланс или выберите другой способ оплаты.", show_alert=True, ) @error_handler async def handle_simple_subscription_other_payment_methods( callback: types.CallbackQuery, db_user: User, state: FSMContext, db: AsyncSession, ): """Обрабатывает выбор других способов оплаты.""" texts = get_texts(db_user.language) data = await state.get_data() subscription_params = data.get("subscription_params", {}) if not subscription_params: await callback.answer("❌ Данные подписки устарели. Пожалуйста, начните сначала.", show_alert=True) return resolved_squad_uuid = await _ensure_simple_subscription_squad_uuid( db, state, subscription_params, user_id=db_user.id, state_data=data, ) # Рассчитываем цену подписки price_kopeks, price_breakdown = await _calculate_simple_subscription_price( db, subscription_params, user=db_user, resolved_squad_uuid=resolved_squad_uuid, ) user_balance_kopeks = getattr(db_user, "balance_kopeks", 0) can_pay_from_balance = user_balance_kopeks >= price_kopeks logger.warning( "SIMPLE_SUBSCRIPTION_DEBUG_METHODS | user=%s | balance=%s | base=%s | traffic=%s | devices=%s | servers=%s | discount=%s | total_required=%s | can_pay=%s", db_user.id, user_balance_kopeks, price_breakdown.get("base_price", 0), price_breakdown.get("traffic_price", 0), price_breakdown.get("devices_price", 0), price_breakdown.get("servers_price", 0), price_breakdown.get("total_discount", 0), price_kopeks, can_pay_from_balance, ) # Отображаем доступные методы оплаты server_label = _get_simple_subscription_server_label( texts, subscription_params, resolved_squad_uuid, ) show_devices = settings.is_devices_selection_enabled() message_lines = [ "💳 Оплата подписки", "", f"📅 Период: {subscription_params['period_days']} дней", ] if show_devices: message_lines.append(f"📱 Устройства: {subscription_params['device_limit']}") payment_traffic_gb = subscription_params["traffic_limit_gb"] payment_traffic_label = "Безлимит" if payment_traffic_gb == 0 else f"{payment_traffic_gb} ГБ" message_lines.extend([ f"📊 Трафик: {payment_traffic_label}", f"🌍 Сервер: {server_label}", "", f"💰 Стоимость: {settings.format_price(price_kopeks)}", "", ( "Вы можете оплатить подписку с баланса или выбрать другой способ оплаты:" if can_pay_from_balance else "Выберите подходящий способ оплаты:" ), ]) message_text = "\n".join(message_lines) base_keyboard = _get_simple_subscription_payment_keyboard(db_user.language) keyboard_rows = [] if can_pay_from_balance: keyboard_rows.append([ types.InlineKeyboardButton( text="✅ Оплатить с баланса", callback_data="simple_subscription_pay_with_balance" ) ]) keyboard_rows.extend(base_keyboard.inline_keyboard) keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows) await callback.message.edit_text( message_text, reply_markup=keyboard, parse_mode="HTML" ) await callback.answer() @error_handler async def handle_simple_subscription_payment_method( callback: types.CallbackQuery, db_user: User, state: FSMContext, db: AsyncSession, ): """Обрабатывает выбор метода оплаты для простой подписки.""" texts = get_texts(db_user.language) data = await state.get_data() subscription_params = data.get("subscription_params", {}) if not subscription_params: await callback.answer("❌ Данные подписки устарели. Пожалуйста, начните сначала.", show_alert=True) return # Проверяем, имеет ли пользователь активную платную подписку from app.database.crud.subscription import get_subscription_by_user_id current_subscription = await get_subscription_by_user_id(db, db_user.id) if current_subscription and not getattr(current_subscription, "is_trial", False) and current_subscription.is_active: # У пользователя есть активная платная подписка - показываем сообщение await callback.answer("⚠️ У вас уже есть активная платная подписка. Пожалуйста, подтвердите покупку через главное меню.", show_alert=True) return payment_method = callback.data.replace("simple_subscription_", "") try: payment_service = PaymentService(callback.bot) purchase_service = SubscriptionPurchaseService() resolved_squad_uuid = await _ensure_simple_subscription_squad_uuid( db, state, subscription_params, user_id=db_user.id, state_data=data, ) # Рассчитываем цену подписки price_kopeks, _ = await _calculate_simple_subscription_price( db, subscription_params, user=db_user, resolved_squad_uuid=resolved_squad_uuid, ) if payment_method == "stars": # Оплата через Telegram Stars order = await purchase_service.create_subscription_order( db=db, user_id=db_user.id, period_days=subscription_params["period_days"], device_limit=subscription_params["device_limit"], traffic_limit_gb=subscription_params["traffic_limit_gb"], squad_uuid=resolved_squad_uuid, payment_method="telegram_stars", total_price_kopeks=price_kopeks, ) if not order: await callback.answer("❌ Не удалось подготовить заказ. Попробуйте позже.", show_alert=True) return stars_count = settings.rubles_to_stars(settings.kopeks_to_rubles(price_kopeks)) stars_traffic_gb = subscription_params["traffic_limit_gb"] stars_traffic_label = "Безлимит" if stars_traffic_gb == 0 else f"{stars_traffic_gb} ГБ" await callback.bot.send_invoice( chat_id=callback.from_user.id, title=f"Подписка на {subscription_params['period_days']} дней", description=( f"Простая покупка подписки\n" f"Период: {subscription_params['period_days']} дней\n" f"Устройства: {subscription_params['device_limit']}\n" f"Трафик: {stars_traffic_label}" ), payload=( f"simple_sub_{db_user.id}_{order.id}_{subscription_params['period_days']}" ), provider_token="", # Пустой токен для Telegram Stars currency="XTR", # Telegram Stars prices=[types.LabeledPrice(label="Подписка", amount=stars_count)] ) await state.clear() await callback.answer() elif payment_method in ["yookassa", "yookassa_sbp"]: # Оплата через YooKassa if not settings.is_yookassa_enabled(): await callback.answer("❌ Оплата через YooKassa временно недоступна", show_alert=True) return if payment_method == "yookassa_sbp" and not settings.YOOKASSA_SBP_ENABLED: await callback.answer("❌ Оплата через СБП временно недоступна", show_alert=True) return # Создаем заказ на подписку order = await purchase_service.create_subscription_order( db=db, user_id=db_user.id, period_days=subscription_params["period_days"], device_limit=subscription_params["device_limit"], traffic_limit_gb=subscription_params["traffic_limit_gb"], squad_uuid=resolved_squad_uuid, payment_method="yookassa_sbp" if payment_method == "yookassa_sbp" else "yookassa", total_price_kopeks=price_kopeks ) if not order: await callback.answer("❌ Ошибка создания заказа", show_alert=True) return # Создаем платеж через YooKassa if payment_method == "yookassa_sbp": payment_result = await payment_service.create_yookassa_sbp_payment( db=db, user_id=db_user.id, amount_kopeks=price_kopeks, description=f"Оплата подписки на {subscription_params['period_days']} дней", receipt_email=db_user.email if hasattr(db_user, 'email') and db_user.email else None, receipt_phone=db_user.phone if hasattr(db_user, 'phone') and db_user.phone else None, metadata={ "user_telegram_id": str(db_user.telegram_id), "user_username": db_user.username or "", "order_id": str(order.id), "subscription_period": str(subscription_params["period_days"]), "payment_purpose": "simple_subscription_purchase" } ) else: payment_result = await payment_service.create_yookassa_payment( db=db, user_id=db_user.id, amount_kopeks=price_kopeks, description=f"Оплата подписки на {subscription_params['period_days']} дней", receipt_email=db_user.email if hasattr(db_user, 'email') and db_user.email else None, receipt_phone=db_user.phone if hasattr(db_user, 'phone') and db_user.phone else None, metadata={ "user_telegram_id": str(db_user.telegram_id), "user_username": db_user.username or "", "order_id": str(order.id), "subscription_period": str(subscription_params["period_days"]), "payment_purpose": "simple_subscription_purchase" } ) if not payment_result: await callback.answer("❌ Ошибка создания платежа", show_alert=True) return # Отправляем QR-код и/или ссылку для оплаты confirmation_url = payment_result.get("confirmation_url") qr_confirmation_data = payment_result.get("qr_confirmation_data") if not confirmation_url and not qr_confirmation_data: await callback.answer("❌ Ошибка получения данных для оплаты", show_alert=True) return # Подготовим QR-код для вставки в основное сообщение qr_photo = None if qr_confirmation_data or confirmation_url: try: # Импортируем необходимые модули для генерации QR-кода import base64 from io import BytesIO import qrcode from aiogram.types import BufferedInputFile # Используем qr_confirmation_data если доступно, иначе confirmation_url qr_data = qr_confirmation_data if qr_confirmation_data else confirmation_url # Создаем QR-код из полученных данных qr = qrcode.QRCode(version=1, box_size=10, border=5) qr.add_data(qr_data) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") # Сохраняем изображение в байты img_bytes = BytesIO() img.save(img_bytes, format='PNG') img_bytes.seek(0) qr_photo = BufferedInputFile(img_bytes.getvalue(), filename="qrcode.png") except ImportError: logger.warning("qrcode библиотека не установлена, QR-код не будет сгенерирован") except Exception as e: logger.error(f"Ошибка генерации QR-кода: {e}") # Создаем клавиатуру с кнопками для оплаты по ссылке и проверки статуса keyboard_buttons = [] # Добавляем кнопку оплаты, если доступна ссылка if confirmation_url: keyboard_buttons.append([types.InlineKeyboardButton(text="🔗 Перейти к оплате", url=confirmation_url)]) else: # Если ссылка недоступна, предлагаем оплатить через ID платежа в приложении банка keyboard_buttons.append([types.InlineKeyboardButton(text="📱 Оплатить в приложении банка", callback_data="temp_disabled")]) # Добавляем общие кнопки keyboard_buttons.append([types.InlineKeyboardButton(text="📊 Проверить статус", callback_data=f"check_yookassa_{payment_result['local_payment_id']}")]) keyboard_buttons.append([types.InlineKeyboardButton(text=texts.BACK, callback_data="subscription_purchase")]) keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_buttons) # Подготавливаем текст сообщения show_devices = settings.is_devices_selection_enabled() message_lines = [ "💳 Оплата подписки через YooKassa", "", f"📅 Период: {subscription_params['period_days']} дней", ] if show_devices: message_lines.append(f"📱 Устройства: {subscription_params['device_limit']}") yookassa_traffic_gb = subscription_params["traffic_limit_gb"] yookassa_traffic_label = "Безлимит" if yookassa_traffic_gb == 0 else f"{yookassa_traffic_gb} ГБ" message_lines.extend([ f"📊 Трафик: {yookassa_traffic_label}", f"💰 Сумма: {settings.format_price(price_kopeks)}", f"🆔 ID платежа: {payment_result['yookassa_payment_id'][:8]}...", "", ]) message_text = "\n".join(message_lines) # Добавляем инструкции в зависимости от доступных способов оплаты if not confirmation_url: message_text += ( f"📱 Инструкция по оплате:\n" f"1. Откройте приложение вашего банка\n" f"2. Найдите функцию оплаты по реквизитам или перевод по СБП\n" f"3. Введите ID платежа: {payment_result['yookassa_payment_id']}\n" f"4. Подтвердите платеж в приложении банка\n" f"5. Деньги поступят на баланс автоматически\n\n" ) message_text += ( f"🔒 Оплата происходит через защищенную систему YooKassa\n" f"✅ Принимаем карты: Visa, MasterCard, МИР\n\n" f"❓ Если возникнут проблемы, обратитесь в {settings.get_support_contact_display_html()}" ) # Отправляем сообщение с инструкциями и клавиатурой # Если есть QR-код, отправляем его как медиа-сообщение if qr_photo: # Используем метод отправки фото с описанием await callback.message.edit_media( media=types.InputMediaPhoto( media=qr_photo, caption=message_text, parse_mode="HTML" ), reply_markup=keyboard ) else: # Если QR-код недоступен, отправляем обычное текстовое сообщение await callback.message.edit_text( message_text, reply_markup=keyboard, parse_mode="HTML" ) await state.clear() await callback.answer() elif payment_method == "cryptobot": # Оплата через CryptoBot if not settings.is_cryptobot_enabled(): await callback.answer("❌ Оплата через CryptoBot временно недоступна", show_alert=True) return amount_rubles = price_kopeks / 100 if amount_rubles < 100 or amount_rubles > 100000: await callback.answer( "❌ Сумма должна быть от 100 до 100 000 ₽ для оплаты через CryptoBot", show_alert=True, ) return try: from app.utils.currency_converter import currency_converter usd_rate = await currency_converter.get_usd_to_rub_rate() except Exception as rate_error: logger.warning("Не удалось получить курс USD: %s", rate_error) usd_rate = 95.0 amount_usd = round(amount_rubles / usd_rate, 2) if amount_usd < 1: await callback.answer( "❌ Минимальная сумма для оплаты через CryptoBot — примерно 1 USD", show_alert=True, ) return if amount_usd > 1000: await callback.answer( "❌ Максимальная сумма для оплаты через CryptoBot — 1000 USD", show_alert=True, ) return payment_service = PaymentService(callback.bot) crypto_result = await payment_service.create_cryptobot_payment( db=db, user_id=db_user.id, amount_usd=amount_usd, asset=settings.CRYPTOBOT_DEFAULT_ASSET, description=settings.get_subscription_payment_description( subscription_params["period_days"], price_kopeks, ), payload=f"simple_subscription_{db_user.id}_{price_kopeks}", ) if not crypto_result: await callback.answer( "❌ Ошибка создания платежа через CryptoBot. Попробуйте позже или обратитесь в поддержку.", show_alert=True, ) return payment_url = ( crypto_result.get("mini_app_invoice_url") or crypto_result.get("bot_invoice_url") or crypto_result.get("web_app_invoice_url") ) if not payment_url: await callback.answer( "❌ Не удалось получить ссылку для оплаты. Обратитесь в поддержку.", show_alert=True, ) return keyboard = types.InlineKeyboardMarkup( inline_keyboard=[ [ types.InlineKeyboardButton( text="🪙 Оплатить через CryptoBot", url=payment_url, ) ], [ types.InlineKeyboardButton( text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"), callback_data=f"check_simple_cryptobot_{crypto_result['local_payment_id']}", ) ], [types.InlineKeyboardButton(text=texts.BACK, callback_data="subscription_purchase")], ] ) message_text = ( "🪙 Оплата через CryptoBot\n\n" f"💰 Сумма к оплате: {amount_rubles:.0f} ₽\n" f"💵 В долларах: {amount_usd:.2f} USD\n" f"🪙 Актив: {crypto_result['asset']}\n" f"💱 Курс: 1 USD ≈ {usd_rate:.2f} ₽\n" f"🆔 ID платежа: {crypto_result['invoice_id'][:8]}...\n\n" "📱 Инструкция:\n" "1. Нажмите кнопку 'Оплатить через CryptoBot'\n" "2. Выберите актив и следуйте подсказкам\n" "3. Подтвердите перевод\n" "4. Средства зачислятся автоматически\n\n" f"❓ Если возникнут проблемы, обратитесь в {settings.get_support_contact_display_html()}" ) await callback.message.edit_text( message_text, reply_markup=keyboard, parse_mode="HTML", ) await state.clear() await callback.answer() return elif payment_method == "heleket": if not settings.is_heleket_enabled(): await callback.answer("❌ Оплата через Heleket временно недоступна", show_alert=True) return amount_rubles = price_kopeks / 100 if amount_rubles < 100 or amount_rubles > 100000: await callback.answer( "❌ Сумма должна быть от 100 до 100 000 ₽ для оплаты через Heleket", show_alert=True, ) return heleket_result = await payment_service.create_heleket_payment( db=db, user_id=db_user.id, amount_kopeks=price_kopeks, description=settings.get_subscription_payment_description( subscription_params["period_days"], price_kopeks, ), language=db_user.language, ) if not heleket_result: await callback.answer( "❌ Ошибка создания платежа Heleket. Попробуйте позже или обратитесь в поддержку.", show_alert=True, ) return payment_url = heleket_result.get("payment_url") if not payment_url: await callback.answer( "❌ Не удалось получить ссылку для оплаты Heleket. Обратитесь в поддержку.", show_alert=True, ) return local_payment_id = heleket_result.get("local_payment_id") payer_amount = heleket_result.get("payer_amount") payer_currency = heleket_result.get("payer_currency") discount_percent = heleket_result.get("discount_percent") markup_percent = None if discount_percent is not None: try: markup_percent = -int(discount_percent) except (TypeError, ValueError): markup_percent = None keyboard = types.InlineKeyboardMarkup( inline_keyboard=[ [ types.InlineKeyboardButton( text="🪙 Оплатить через Heleket", url=payment_url, ) ], [ types.InlineKeyboardButton( text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"), callback_data=f"check_simple_heleket_{local_payment_id}", ) ], [types.InlineKeyboardButton(text=texts.BACK, callback_data="subscription_purchase")], ] ) message_lines = [ "🪙 Оплата через Heleket", "", f"💰 Сумма: {settings.format_price(price_kopeks)}", ] if payer_amount and payer_currency: message_lines.append(f"🪙 К оплате: {payer_amount} {payer_currency}") try: payer_amount_float = float(payer_amount) if payer_amount_float > 0: rub_per_currency = amount_rubles / payer_amount_float message_lines.append( f"💱 Курс: 1 {payer_currency} ≈ {rub_per_currency:.2f} ₽" ) except (TypeError, ValueError, ZeroDivisionError): pass if markup_percent: sign = "+" if markup_percent > 0 else "" message_lines.append(f"📈 Наценка: {sign}{markup_percent}%") message_lines.extend( [ "", "📱 Инструкция:", "1. Нажмите кнопку 'Оплатить через Heleket'", "2. Следуйте подсказкам на странице оплаты", "3. Подтвердите перевод", "4. Средства зачислятся автоматически", "", f"❓ Если возникнут проблемы, обратитесь в {settings.get_support_contact_display_html()}", ] ) await callback.message.edit_text( "\n".join(message_lines), reply_markup=keyboard, parse_mode="HTML", ) await state.clear() await callback.answer() return elif payment_method == "mulenpay": # Оплата через MulenPay mulenpay_name = settings.get_mulenpay_display_name() if not settings.is_mulenpay_enabled(): await callback.answer( f"❌ Оплата через {mulenpay_name} временно недоступна", show_alert=True, ) return if price_kopeks < settings.MULENPAY_MIN_AMOUNT_KOPEKS or price_kopeks > settings.MULENPAY_MAX_AMOUNT_KOPEKS: await callback.answer( "❌ Сумма для Mulen Pay должна быть в пределах от {min_amount} до {max_amount}".format( min_amount=settings.format_price(settings.MULENPAY_MIN_AMOUNT_KOPEKS), max_amount=settings.format_price(settings.MULENPAY_MAX_AMOUNT_KOPEKS), ), show_alert=True, ) return payment_service = PaymentService(callback.bot) mulen_result = await payment_service.create_mulenpay_payment( db=db, user_id=db_user.id, amount_kopeks=price_kopeks, description=settings.get_subscription_payment_description( subscription_params["period_days"], price_kopeks, ), language=db_user.language, ) if not mulen_result or not mulen_result.get("payment_url"): await callback.answer( texts.t( "MULENPAY_PAYMENT_ERROR", "❌ Ошибка создания платежа Mulen Pay. Попробуйте позже или обратитесь в поддержку.", ), show_alert=True, ) return payment_url = mulen_result["payment_url"] local_payment_id = mulen_result.get("local_payment_id") payment_id_display = mulen_result.get("mulen_payment_id") or 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_simple_mulenpay_{local_payment_id}", ) ], [types.InlineKeyboardButton(text=texts.BACK, callback_data="subscription_purchase")], ] ) message_template = texts.t( "MULENPAY_PAYMENT_INSTRUCTIONS", ( "💳 Оплата через {mulenpay_name_html}\n\n" "💰 Сумма: {amount}\n" "🆔 ID платежа: {payment_id}\n\n" "📱 Инструкция:\n" "1. Нажмите кнопку 'Оплатить через {mulenpay_name}'\n" "2. Следуйте подсказкам платежной системы\n" "3. Подтвердите перевод\n" "4. Средства зачислятся автоматически\n\n" "❓ Если возникнут проблемы, обратитесь в {support}" ), ) await callback.message.edit_text( message_template.format( mulenpay_name=mulenpay_name, mulenpay_name_html=settings.get_mulenpay_display_name_html(), amount=settings.format_price(price_kopeks), payment_id=payment_id_display, support=settings.get_support_contact_display_html(), ), reply_markup=keyboard, parse_mode="HTML", ) await state.clear() await callback.answer() return elif payment_method == "pal24": # Оплата через PayPalych if not settings.is_pal24_enabled(): await callback.answer("❌ Оплата через PayPalych временно недоступна", show_alert=True) return payment_service = PaymentService(callback.bot) pal24_result = await payment_service.create_pal24_payment( db=db, user_id=db_user.id, amount_kopeks=price_kopeks, description=settings.get_subscription_payment_description( subscription_params["period_days"], price_kopeks, ), language=db_user.language, ) if not pal24_result: await callback.answer( texts.t( "PAL24_PAYMENT_ERROR", "❌ Ошибка создания платежа PayPalych. Попробуйте позже или обратитесь в поддержку.", ), show_alert=True, ) return sbp_url = pal24_result.get("sbp_url") or pal24_result.get("transfer_url") card_url = pal24_result.get("card_url") fallback_url = pal24_result.get("link_page_url") or pal24_result.get("link_url") if not (sbp_url or card_url or fallback_url): await callback.answer( texts.t( "PAL24_PAYMENT_ERROR", "❌ Ошибка создания платежа PayPalych. Попробуйте позже или обратитесь в поддержку.", ), show_alert=True, ) return if not sbp_url: sbp_url = fallback_url bill_id = pal24_result.get("bill_id") local_payment_id = pal24_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_simple_pal24_{local_payment_id}", ) ], [types.InlineKeyboardButton(text=texts.BACK, callback_data="subscription_purchase")], ] keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows) message_text = message_template.format( amount=settings.format_price(price_kopeks), bill_id=bill_id, steps="\n".join(steps), support=settings.get_support_contact_display_html(), ) await callback.message.edit_text( message_text, reply_markup=keyboard, parse_mode="HTML", ) await state.clear() await callback.answer() return elif payment_method == "wata": # Оплата через WATA if not settings.is_wata_enabled(): await callback.answer("❌ Оплата через WATA временно недоступна", show_alert=True) return if price_kopeks < settings.WATA_MIN_AMOUNT_KOPEKS or price_kopeks > settings.WATA_MAX_AMOUNT_KOPEKS: await callback.answer( "❌ Сумма для WATA должна быть между {min_amount} и {max_amount}.".format( min_amount=settings.format_price(settings.WATA_MIN_AMOUNT_KOPEKS), max_amount=settings.format_price(settings.WATA_MAX_AMOUNT_KOPEKS), ), show_alert=True, ) return payment_service = PaymentService(callback.bot) try: wata_result = await payment_service.create_wata_payment( db=db, user_id=db_user.id, amount_kopeks=price_kopeks, description=settings.get_subscription_payment_description( subscription_params["period_days"], price_kopeks, ), language=db_user.language, ) except Exception as error: logger.error("Ошибка создания WATA платежа: %s", error) wata_result = None if not wata_result or not wata_result.get("payment_url"): await callback.answer( texts.t( "WATA_PAYMENT_ERROR", "❌ Ошибка создания платежа WATA. Попробуйте позже или обратитесь в поддержку.", ), show_alert=True, ) return payment_url = wata_result["payment_url"] payment_link_id = wata_result.get("payment_link_id") local_payment_id = wata_result.get("local_payment_id") keyboard = types.InlineKeyboardMarkup( inline_keyboard=[ [ types.InlineKeyboardButton( text=texts.t("WATA_PAY_BUTTON", "💳 Оплатить через WATA"), url=payment_url, ) ], [ types.InlineKeyboardButton( text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"), callback_data=f"check_simple_wata_{local_payment_id}", ) ], [types.InlineKeyboardButton(text=texts.BACK, callback_data="subscription_purchase")], ] ) message_template = texts.t( "WATA_PAYMENT_INSTRUCTIONS", ( "💳 Оплата через WATA\n\n" "💰 Сумма: {amount}\n" "🆔 ID платежа: {payment_id}\n\n" "📱 Инструкция:\n" "1. Нажмите кнопку 'Оплатить через WATA'\n" "2. Следуйте подсказкам платежной системы\n" "3. Подтвердите перевод\n" "4. Средства зачислятся автоматически\n\n" "❓ Если возникнут проблемы, обратитесь в {support}" ), ) await callback.message.edit_text( message_template.format( amount=settings.format_price(price_kopeks), payment_id=payment_link_id, support=settings.get_support_contact_display_html(), ), reply_markup=keyboard, parse_mode="HTML", ) await state.clear() await callback.answer() return else: await callback.answer("❌ Неизвестный способ оплаты", show_alert=True) except Exception as e: logger.error(f"Ошибка обработки метода оплаты простой подписки: {e}") await callback.answer("❌ Ошибка обработки запроса. Попробуйте позже или обратитесь в поддержку.", show_alert=True) await state.clear() @error_handler async def check_simple_pal24_payment_status( callback: types.CallbackQuery, db: AsyncSession, ): try: local_payment_id = int(callback.data.rsplit('_', 1)[-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 {} if not isinstance(links_meta, dict): links_meta = {} links_info = status_info.get("links") or {} sbp_link = ( links_info.get("sbp") or links_meta.get("sbp") or status_info.get("sbp_url") or payment.link_url ) card_link = ( links_info.get("card") or links_meta.get("card") or status_info.get("card_url") ) if not card_link and payment.link_page_url and payment.link_page_url != sbp_link: card_link = payment.link_page_url db_user = getattr(callback, "db_user", None) texts = get_texts(db_user.language if db_user else settings.DEFAULT_LANGUAGE) 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 += ["", "✅ Платеж успешно завершен! Средства уже зачислены."] elif payment.status in {"NEW", "PROCESS"}: message_lines += [ "", "⏳ Платеж еще не завершен. Оплатите счет и проверьте статус позже.", ] if sbp_link: message_lines += ["", 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 += [ "", f"❌ Платеж не завершен корректно. Обратитесь в {settings.get_support_contact_display()}", ] pay_rows: list[list[types.InlineKeyboardButton]] = [] if not payment.is_paid and payment.status in {"NEW", "PROCESS"}: default_sbp_text = texts.t( "PAL24_SBP_PAY_BUTTON", "🏦 Оплатить через PayPalych (СБП)", ) sbp_button_text = settings.get_pal24_sbp_button_text(default_sbp_text) if sbp_link and settings.is_pal24_sbp_button_visible(): pay_rows.append( [ types.InlineKeyboardButton( text=sbp_button_text, url=sbp_link, ) ] ) default_card_text = texts.t( "PAL24_CARD_PAY_BUTTON", "💳 Оплатить банковской картой (PayPalych)", ) card_button_text = settings.get_pal24_card_button_text(default_card_text) if card_link and settings.is_pal24_card_button_visible(): if not pay_rows or pay_rows[-1][0].url != card_link: pay_rows.append( [ types.InlineKeyboardButton( text=card_button_text, url=card_link, ) ] ) keyboard = types.InlineKeyboardMarkup( inline_keyboard=pay_rows + [ [ types.InlineKeyboardButton( text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"), callback_data=f"check_simple_pal24_{local_payment_id}", ) ], [types.InlineKeyboardButton(text=texts.BACK, callback_data="subscription_purchase")], ] ) await callback.answer() try: await callback.message.edit_text( "\n".join(message_lines), reply_markup=keyboard, disable_web_page_preview=True, ) except TelegramBadRequest as error: if "message is not modified" in str(error).lower(): await callback.answer(texts.t("CHECK_STATUS_NO_CHANGES", "Статус не изменился")) else: raise except Exception as error: logger.error(f"Ошибка проверки статуса PayPalych для простой подписки: {error}") await callback.answer("❌ Ошибка проверки статуса", show_alert=True) @error_handler async def check_simple_mulenpay_payment_status( callback: types.CallbackQuery, db: AsyncSession, ): try: local_payment_id = int(callback.data.rsplit('_', 1)[-1]) except (ValueError, IndexError): await callback.answer("❌ Некорректный идентификатор платежа", show_alert=True) return 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"] user_language = settings.DEFAULT_LANGUAGE try: from app.services.payment_service import get_user_by_id as fetch_user_by_id user = await fetch_user_by_id(db, payment.user_id) if user and getattr(user, "language", None): user_language = user.language except Exception as error: logger.debug("Не удалось получить пользователя для MulenPay статуса: %s", error) texts = get_texts(user_language) status_labels = { "created": ("⏳", "Ожидает оплаты"), "processing": ("⌛", "Обрабатывается"), "success": ("✅", "Оплачен"), "canceled": ("❌", "Отменен"), "error": ("⚠️", "Ошибка"), "hold": ("🔒", "Холд"), "unknown": ("❓", "Неизвестно"), } emoji, status_text = status_labels.get(payment.status, ("❓", "Неизвестно")) message_lines = [ "💳 Статус платежа Mulen Pay:", "", f"🆔 ID: {payment.mulen_payment_id or payment.id}", f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}", f"📊 Статус: {emoji} {status_text}", f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M') if payment.created_at else '—'}", ] if payment.is_paid: message_lines.append("\n✅ Платеж успешно завершен! Средства уже зачислены.") elif payment.status in {"created", "processing"}: message_lines.append("\n⏳ Платеж еще не завершен. Завершите оплату и проверьте статус позже.") keyboard = types.InlineKeyboardMarkup( inline_keyboard=[ [ types.InlineKeyboardButton( text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"), callback_data=f"check_simple_mulenpay_{local_payment_id}", ) ], [types.InlineKeyboardButton(text=texts.BACK, callback_data="subscription_purchase")], ] ) await callback.answer() await callback.message.edit_text( "\n".join(message_lines), reply_markup=keyboard, parse_mode="HTML", ) @error_handler async def check_simple_cryptobot_payment_status( callback: types.CallbackQuery, db: AsyncSession, ): try: local_payment_id = int(callback.data.rsplit('_', 1)[-1]) except (ValueError, IndexError): await callback.answer("❌ Некорректный идентификатор платежа", show_alert=True) return 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_labels = { "active": ("⏳", "Ожидает оплаты"), "paid": ("✅", "Оплачен"), "expired": ("❌", "Истек"), } emoji, status_text = status_labels.get(payment.status, ("❓", "Неизвестно")) language = settings.DEFAULT_LANGUAGE try: from app.services.payment_service import get_user_by_id as fetch_user_by_id user = await fetch_user_by_id(db, payment.user_id) if user and getattr(user, "language", None): language = user.language except Exception as error: logger.debug("Не удалось получить пользователя для CryptoBot статуса: %s", error) texts = get_texts(language) message_lines = [ "🪙 Статус платежа CryptoBot", "", f"🆔 ID: {payment.invoice_id}", f"💰 Сумма: {payment.amount} {payment.asset}", f"📊 Статус: {emoji} {status_text}", f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M') if payment.created_at else '—'}", ] if payment.status == "paid": message_lines.append("\n✅ Платеж подтвержден. Средства уже зачислены.") elif payment.status == "active": message_lines.append("\n⏳ Платеж еще ожидает подтверждения. Оплатите счет и проверьте статус позже.") keyboard = types.InlineKeyboardMarkup( inline_keyboard=[ [ types.InlineKeyboardButton( text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"), callback_data=f"check_simple_cryptobot_{local_payment_id}", ) ], [types.InlineKeyboardButton(text=texts.BACK, callback_data="subscription_purchase")], ] ) await callback.answer() await callback.message.edit_text( "\n".join(message_lines), reply_markup=keyboard, parse_mode="HTML", ) @error_handler async def check_simple_heleket_payment_status( callback: types.CallbackQuery, db: AsyncSession, ): try: local_payment_id = int(callback.data.rsplit('_', 1)[-1]) except (ValueError, IndexError): await callback.answer("❌ Некорректный идентификатор платежа", show_alert=True) return from app.database.crud.heleket import get_heleket_payment_by_id payment = await get_heleket_payment_by_id(db, local_payment_id) if not payment: await callback.answer("❌ Платеж не найден", show_alert=True) return status_labels = { "check": ("⏳", "Ожидает оплаты"), "paid": ("✅", "Оплачен"), "paid_over": ("✅", "Оплачен (переплата)"), "wrong_amount": ("⚠️", "Неверная сумма"), "cancel": ("❌", "Отменен"), "fail": ("❌", "Ошибка"), "process": ("⌛", "Обрабатывается"), "confirm_check": ("⌛", "Ожидает подтверждения"), } emoji, status_text = status_labels.get(payment.status, ("❓", "Неизвестно")) language = settings.DEFAULT_LANGUAGE try: from app.services.payment_service import get_user_by_id as fetch_user_by_id user = await fetch_user_by_id(db, payment.user_id) if user and getattr(user, "language", None): language = user.language except Exception as error: logger.debug("Не удалось получить пользователя для Heleket статуса: %s", error) texts = get_texts(language) message_lines = [ "🪙 Статус платежа Heleket:", "", f"🆔 UUID: {payment.uuid[:8]}...", f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}", f"📊 Статус: {emoji} {status_text}", f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M') if payment.created_at else '—'}", ] if payment.payer_amount and payment.payer_currency: message_lines.append( f"🪙 Оплата: {payment.payer_amount} {payment.payer_currency}" ) if payment.is_paid: message_lines.append("\n✅ Платеж успешно завершен! Средства уже зачислены.") elif payment.status in {"check", "process", "confirm_check"}: message_lines.append("\n⏳ Платеж еще обрабатывается. Завершите оплату и проверьте статус позже.") if payment.payment_url: message_lines.append(f"\n🔗 Ссылка на оплату: {payment.payment_url}") elif payment.status in {"fail", "cancel", "wrong_amount"}: message_lines.append( f"\n❌ Платеж не завершен корректно. Обратитесь в {settings.get_support_contact_display()}" ) keyboard = types.InlineKeyboardMarkup( inline_keyboard=[ [ types.InlineKeyboardButton( text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"), callback_data=f"check_simple_heleket_{local_payment_id}", ) ], [types.InlineKeyboardButton(text=texts.BACK, callback_data="subscription_purchase")], ] ) await callback.answer() await callback.message.edit_text( "\n".join(message_lines), reply_markup=keyboard, parse_mode="HTML", ) @error_handler async def check_simple_wata_payment_status( callback: types.CallbackQuery, db: AsyncSession, ): try: local_payment_id = int(callback.data.rsplit('_', 1)[-1]) except (ValueError, IndexError): await callback.answer("❌ Некорректный идентификатор платежа", show_alert=True) return payment_service = PaymentService(callback.bot) status_info = await payment_service.get_wata_payment_status(db, local_payment_id) if not status_info: await callback.answer("❌ Платеж не найден", show_alert=True) return payment = status_info["payment"] texts = get_texts(settings.DEFAULT_LANGUAGE) status_labels = { "Opened": ("⏳", texts.t("WATA_STATUS_OPENED", "Ожидает оплаты")), "Closed": ("⌛", texts.t("WATA_STATUS_CLOSED", "Обрабатывается")), "Paid": ("✅", texts.t("WATA_STATUS_PAID", "Оплачен")), "Declined": ("❌", texts.t("WATA_STATUS_DECLINED", "Отклонен")), } emoji, status_text = status_labels.get(payment.status, ("❓", texts.t("WATA_STATUS_UNKNOWN", "Неизвестно"))) message_lines = [ texts.t("WATA_STATUS_TITLE", "💳 Статус платежа WATA"), "", f"🆔 ID: {payment.payment_link_id}", f"💰 Сумма: {settings.format_price(payment.amount_kopeks)}", f"📊 Статус: {emoji} {status_text}", f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M') if payment.created_at else '—'}", ] if payment.is_paid: message_lines.append("\n✅ Платеж успешно завершен! Средства уже зачислены.") elif payment.status in {"Opened", "Closed"}: message_lines.append("\n⏳ Платеж еще не завершен. Завершите оплату и проверьте статус позже.") keyboard = types.InlineKeyboardMarkup( inline_keyboard=[ [ types.InlineKeyboardButton( text=texts.t("CHECK_STATUS_BUTTON", "📊 Проверить статус"), callback_data=f"check_simple_wata_{local_payment_id}", ) ], [types.InlineKeyboardButton(text=texts.BACK, callback_data="subscription_purchase")], ] ) await callback.answer() await callback.message.edit_text( "\n".join(message_lines), reply_markup=keyboard, parse_mode="HTML", ) @error_handler async def confirm_simple_subscription_purchase( callback: types.CallbackQuery, db_user: User, state: FSMContext, db: AsyncSession, ): """Обрабатывает подтверждение простой покупки подписки при наличии активной платной подписки.""" texts = get_texts(db_user.language) data = await state.get_data() subscription_params = data.get("subscription_params", {}) if not subscription_params: await callback.answer("❌ Данные подписки устарели. Пожалуйста, начните сначала.", show_alert=True) return resolved_squad_uuid = await _ensure_simple_subscription_squad_uuid( db, state, subscription_params, user_id=db_user.id, state_data=data, ) # Рассчитываем цену подписки price_kopeks, price_breakdown = await _calculate_simple_subscription_price( db, subscription_params, user=db_user, resolved_squad_uuid=resolved_squad_uuid, ) total_required = price_kopeks logger.warning( "SIMPLE_SUBSCRIPTION_DEBUG_CONFIRM | user=%s | period=%s | base=%s | traffic=%s | devices=%s | servers=%s | discount=%s | total_required=%s | balance=%s", db_user.id, subscription_params["period_days"], price_breakdown.get("base_price", 0), price_breakdown.get("traffic_price", 0), price_breakdown.get("devices_price", 0), price_breakdown.get("servers_price", 0), price_breakdown.get("total_discount", 0), total_required, getattr(db_user, "balance_kopeks", 0), ) # Проверяем баланс пользователя user_balance_kopeks = getattr(db_user, "balance_kopeks", 0) if user_balance_kopeks < total_required: await callback.answer("❌ Недостаточно средств на балансе для оплаты подписки", show_alert=True) return try: # Списываем средства с баланса пользователя from app.database.crud.user import subtract_user_balance success = await subtract_user_balance( db, db_user, price_kopeks, f"Оплата подписки на {subscription_params['period_days']} дней", consume_promo_offer=False, ) if not success: await callback.answer("❌ Ошибка списания средств с баланса", show_alert=True) return # Проверяем, есть ли у пользователя уже подписка from app.database.crud.subscription import get_subscription_by_user_id, extend_subscription existing_subscription = await get_subscription_by_user_id(db, db_user.id) if existing_subscription: # Если подписка уже существует, продлеваем её # Сохраняем информацию о текущей подписке, особенно является ли она пробной was_trial = getattr(existing_subscription, "is_trial", False) subscription = await extend_subscription( db=db, subscription=existing_subscription, days=subscription_params["period_days"] ) # Обновляем параметры подписки subscription.traffic_limit_gb = subscription_params["traffic_limit_gb"] subscription.device_limit = subscription_params["device_limit"] # Если текущая подписка была пробной, и мы обновляем её # нужно изменить статус подписки if was_trial: from app.database.models import SubscriptionStatus # Переводим подписку из пробной в активную платную subscription.status = SubscriptionStatus.ACTIVE.value subscription.is_trial = False # Устанавливаем новый выбранный сквад if resolved_squad_uuid: subscription.connected_squads = [resolved_squad_uuid] await db.commit() await db.refresh(subscription) else: # Если подписки нет, создаём новую from app.database.crud.subscription import create_paid_subscription subscription = await create_paid_subscription( db=db, user_id=db_user.id, duration_days=subscription_params["period_days"], traffic_limit_gb=subscription_params["traffic_limit_gb"], device_limit=subscription_params["device_limit"], connected_squads=[resolved_squad_uuid] if resolved_squad_uuid else [], update_server_counters=True, ) if not subscription: # Возвращаем средства на баланс в случае ошибки from app.services.payment_service import add_user_balance await add_user_balance( db, db_user.id, price_kopeks, f"Возврат средств за неудавшуюся подписку на {subscription_params['period_days']} дней", ) await callback.answer("❌ Ошибка создания подписки. Средства возвращены на баланс.", show_alert=True) return # Обновляем баланс пользователя await db.refresh(db_user) # Обновляем или создаём ссылку подписки в RemnaWave try: from app.services.subscription_service import SubscriptionService subscription_service = SubscriptionService() remnawave_user = await subscription_service.create_remnawave_user(db, subscription) if remnawave_user: await db.refresh(subscription) except Exception as sync_error: logger.error(f"Ошибка синхронизации подписки с RemnaWave для пользователя {db_user.id}: {sync_error}", exc_info=True) # Отправляем уведомление об успешной покупке server_label = _get_simple_subscription_server_label( texts, subscription_params, resolved_squad_uuid, ) show_devices = settings.is_devices_selection_enabled() success_lines = [ "✅ Подписка успешно активирована!", "", f"📅 Период: {subscription_params['period_days']} дней", ] if show_devices: success_lines.append(f"📱 Устройства: {subscription_params['device_limit']}") success_traffic_gb = subscription_params["traffic_limit_gb"] success_traffic_label = "Безлимит" if success_traffic_gb == 0 else f"{success_traffic_gb} ГБ" success_lines.extend([ f"📊 Трафик: {success_traffic_label}", f"🌍 Сервер: {server_label}", "", f"💰 Списано с баланса: {settings.format_price(price_kopeks)}", f"💳 Ваш баланс: {settings.format_price(db_user.balance_kopeks)}", "", "🔗 Для подключения перейдите в раздел 'Подключиться'", ]) success_message = "\n".join(success_lines) connect_mode = settings.CONNECT_BUTTON_MODE subscription_link = get_display_subscription_link(subscription) connect_button_text = texts.t("CONNECT_BUTTON", "🔗 Подключиться") def _fallback_connect_button() -> types.InlineKeyboardButton: return types.InlineKeyboardButton( text=connect_button_text, callback_data="subscription_connect", ) if connect_mode == "miniapp_subscription": if subscription_link: connect_row = [ types.InlineKeyboardButton( text=connect_button_text, web_app=types.WebAppInfo(url=subscription_link), ) ] else: connect_row = [_fallback_connect_button()] elif connect_mode == "miniapp_custom": custom_url = settings.MINIAPP_CUSTOM_URL if custom_url: connect_row = [ types.InlineKeyboardButton( text=connect_button_text, web_app=types.WebAppInfo(url=custom_url), ) ] else: connect_row = [_fallback_connect_button()] elif connect_mode == "link": if subscription_link: connect_row = [ types.InlineKeyboardButton( text=connect_button_text, url=subscription_link, ) ] else: connect_row = [_fallback_connect_button()] elif connect_mode == "happ_cryptolink": if subscription_link: connect_row = [ types.InlineKeyboardButton( text=connect_button_text, callback_data="open_subscription_link", ) ] else: connect_row = [_fallback_connect_button()] else: connect_row = [_fallback_connect_button()] keyboard_rows = [connect_row] happ_row = get_happ_download_button_row(texts) if happ_row: keyboard_rows.append(happ_row) keyboard_rows.append( [types.InlineKeyboardButton(text="🏠 Главное меню", callback_data="back_to_menu")] ) keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows) await callback.message.edit_text( success_message, reply_markup=keyboard, parse_mode="HTML" ) # Отправляем уведомление админам try: from app.services.admin_notification_service import AdminNotificationService notification_service = AdminNotificationService(callback.bot) await notification_service.send_subscription_purchase_notification( db, db_user, subscription, None, # transaction subscription_params["period_days"], False, # was_trial_conversion amount_kopeks=price_kopeks, ) except Exception as e: logger.error(f"Ошибка отправки уведомления админам о покупке: {e}") await state.clear() await callback.answer() logger.info(f"Пользователь {db_user.telegram_id} успешно купил подписку с баланса на {price_kopeks/100}₽") except Exception as error: logger.error( "Ошибка подтверждения простой подписки с баланса для пользователя %s: %s", db_user.id, error, exc_info=True, ) await callback.answer( "❌ Ошибка оплаты подписки. Попробуйте позже или обратитесь в поддержку.", show_alert=True, ) await state.clear() def register_simple_subscription_handlers(dp): """Регистрирует обработчики простой покупки подписки.""" dp.callback_query.register( start_simple_subscription_purchase, F.data == "simple_subscription_purchase" ) dp.callback_query.register( confirm_simple_subscription_purchase, F.data == "simple_subscription_confirm_purchase" ) dp.callback_query.register( handle_simple_subscription_pay_with_balance, F.data == "simple_subscription_pay_with_balance" ) dp.callback_query.register( handle_simple_subscription_pay_with_balance_disabled, F.data == "simple_subscription_pay_with_balance_disabled" ) dp.callback_query.register( handle_simple_subscription_other_payment_methods, F.data == "simple_subscription_other_payment_methods" ) dp.callback_query.register( handle_simple_subscription_payment_method, F.data.startswith("simple_subscription_") ) dp.callback_query.register( check_simple_pal24_payment_status, F.data.startswith("check_simple_pal24_") ) dp.callback_query.register( check_simple_mulenpay_payment_status, F.data.startswith("check_simple_mulenpay_") ) dp.callback_query.register( check_simple_cryptobot_payment_status, F.data.startswith("check_simple_cryptobot_") ) dp.callback_query.register( check_simple_heleket_payment_status, F.data.startswith("check_simple_heleket_") ) dp.callback_query.register( check_simple_wata_payment_status, F.data.startswith("check_simple_wata_") ) dp.callback_query.register( check_simple_pal24_payment_status, F.data.startswith("check_simple_pal24_") )