mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 19:32:10 +00:00
2447 lines
103 KiB
Python
2447 lines
103 KiB
Python
"""Обработчики для простой покупки подписки."""
|
||
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"🚫 <b>Покупка подписки ограничена</b>\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 = [
|
||
"⚡ <b>Простая покупка подписки</b>",
|
||
"",
|
||
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 = [
|
||
"✅ <b>Подписка успешно активирована!</b>",
|
||
"",
|
||
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 = [
|
||
"💳 <b>Оплата подписки</b>",
|
||
"",
|
||
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 = [
|
||
"💳 <b>Оплата подписки через YooKassa</b>",
|
||
"",
|
||
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"📱 <b>Инструкция по оплате:</b>\n"
|
||
f"1. Откройте приложение вашего банка\n"
|
||
f"2. Найдите функцию оплаты по реквизитам или перевод по СБП\n"
|
||
f"3. Введите ID платежа: <code>{payment_result['yookassa_payment_id']}</code>\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 = (
|
||
"🪙 <b>Оплата через CryptoBot</b>\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"
|
||
"📱 <b>Инструкция:</b>\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 = [
|
||
"🪙 <b>Оплата через Heleket</b>",
|
||
"",
|
||
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(
|
||
[
|
||
"",
|
||
"📱 <b>Инструкция:</b>",
|
||
"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",
|
||
(
|
||
"💳 <b>Оплата через {mulenpay_name_html}</b>\n\n"
|
||
"💰 Сумма: {amount}\n"
|
||
"🆔 ID платежа: {payment_id}\n\n"
|
||
"📱 <b>Инструкция:</b>\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",
|
||
(
|
||
"🏦 <b>Оплата через PayPalych</b>\n\n"
|
||
"💰 Сумма: {amount}\n"
|
||
"🆔 ID счета: {bill_id}\n\n"
|
||
"📱 <b>Инструкция:</b>\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",
|
||
(
|
||
"💳 <b>Оплата через WATA</b>\n\n"
|
||
"💰 Сумма: {amount}\n"
|
||
"🆔 ID платежа: {payment_id}\n\n"
|
||
"📱 <b>Инструкция:</b>\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 = [
|
||
"🪙 <b>Статус платежа CryptoBot</b>",
|
||
"",
|
||
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", "💳 <b>Статус платежа WATA</b>"),
|
||
"",
|
||
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 = [
|
||
"✅ <b>Подписка успешно активирована!</b>",
|
||
"",
|
||
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_")
|
||
)
|