diff --git a/.env.example b/.env.example
index 66d36d5a..9615c3dc 100644
--- a/.env.example
+++ b/.env.example
@@ -102,6 +102,9 @@ REMNAWAVE_USER_DELETE_MODE=delete
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_LIMIT_GB=10
TRIAL_DEVICE_LIMIT=1
+# Платный триал: если TRIAL_ACTIVATION_PRICE > 0, триал становится платным
+# Цена в копейках (1000 = 10 рублей). Пользователь может оплатить триал любым методом оплаты.
+# TRIAL_PAYMENT_ENABLED опционален (для обратной совместимости)
TRIAL_PAYMENT_ENABLED=false
TRIAL_ACTIVATION_PRICE=0
diff --git a/app/config.py b/app/config.py
index 1e173265..6c8bc8fe 100644
--- a/app/config.py
+++ b/app/config.py
@@ -1147,6 +1147,10 @@ class Settings(BaseSettings):
return applicable_discount
def is_trial_paid_activation_enabled(self) -> bool:
+ # Если цена > 0, триал автоматически платный
+ # (TRIAL_PAYMENT_ENABLED теперь опционален - для обратной совместимости)
+ if self.TRIAL_ACTIVATION_PRICE > 0:
+ return True
return bool(self.TRIAL_PAYMENT_ENABLED)
def get_trial_activation_price(self) -> int:
diff --git a/app/database/crud/subscription.py b/app/database/crud/subscription.py
index 04d9c892..8429d43e 100644
--- a/app/database/crud/subscription.py
+++ b/app/database/crud/subscription.py
@@ -1547,7 +1547,84 @@ async def create_pending_subscription(
subscription.id,
payment_method,
)
-
+
+ return subscription
+
+
+async def create_pending_trial_subscription(
+ db: AsyncSession,
+ user_id: int,
+ duration_days: int,
+ traffic_limit_gb: int = 0,
+ device_limit: int = 1,
+ connected_squads: List[str] = None,
+ payment_method: str = "pending",
+ total_price_kopeks: int = 0
+) -> Subscription:
+ """Creates a pending trial subscription that will be activated after payment."""
+
+ current_time = datetime.utcnow()
+ end_date = current_time + timedelta(days=duration_days)
+
+ existing_subscription = await get_subscription_by_user_id(db, user_id)
+
+ if existing_subscription:
+ if (
+ existing_subscription.status == SubscriptionStatus.ACTIVE.value
+ and existing_subscription.end_date > current_time
+ ):
+ logger.warning(
+ "⚠️ Попытка создать pending триал для активного пользователя %s. Возвращаем существующую запись.",
+ user_id,
+ )
+ return existing_subscription
+
+ # Обновляем существующую подписку
+ existing_subscription.status = SubscriptionStatus.PENDING.value
+ existing_subscription.is_trial = True # Помечаем как триальную
+ existing_subscription.start_date = current_time
+ existing_subscription.end_date = end_date
+ existing_subscription.traffic_limit_gb = traffic_limit_gb
+ existing_subscription.device_limit = device_limit
+ existing_subscription.connected_squads = connected_squads or []
+ existing_subscription.traffic_used_gb = 0.0
+ existing_subscription.updated_at = current_time
+
+ await db.commit()
+ await db.refresh(existing_subscription)
+
+ logger.info(
+ "♻️ Обновлена ожидающая триальная подписка пользователя %s, ID: %s, метод оплаты: %s",
+ user_id,
+ existing_subscription.id,
+ payment_method,
+ )
+ return existing_subscription
+
+ subscription = Subscription(
+ user_id=user_id,
+ status=SubscriptionStatus.PENDING.value,
+ is_trial=True, # Помечаем как триальную
+ start_date=current_time,
+ end_date=end_date,
+ traffic_limit_gb=traffic_limit_gb,
+ device_limit=device_limit,
+ connected_squads=connected_squads or [],
+ autopay_enabled=settings.is_autopay_enabled_by_default(),
+ autopay_days_before=settings.DEFAULT_AUTOPAY_DAYS_BEFORE,
+ )
+
+ db.add(subscription)
+ await db.commit()
+ await db.refresh(subscription)
+
+ logger.info(
+ "💳 Создана ожидающая триальная подписка для пользователя %s, ID: %s, метод оплаты: %s",
+ user_id,
+ subscription.id,
+ payment_method,
+ )
+
return subscription
diff --git a/app/handlers/subscription/purchase.py b/app/handlers/subscription/purchase.py
index 7e9e5f94..3545ba68 100644
--- a/app/handlers/subscription/purchase.py
+++ b/app/handlers/subscription/purchase.py
@@ -17,6 +17,7 @@ from app.database.crud.discount_offer import (
from app.database.crud.promo_offer_template import get_promo_offer_template_by_id
from app.database.crud.subscription import (
create_trial_subscription,
+ create_pending_trial_subscription,
create_paid_subscription, add_subscription_traffic, add_subscription_devices,
update_subscription_autopay
)
@@ -49,6 +50,7 @@ from app.keyboards.inline import (
)
from app.services.user_cart_service import user_cart_service
from app.localization.texts import get_texts
+from app.utils.decorators import error_handler
from app.services.admin_notification_service import AdminNotificationService
from app.services.remnawave_service import RemnaWaveConfigurationError, RemnaWaveService
from app.services.blacklist_service import blacklist_service
@@ -498,12 +500,86 @@ async def show_trial_offer(
)
await callback.answer()
+def _get_trial_payment_keyboard(language: str, can_pay_from_balance: bool = False) -> types.InlineKeyboardMarkup:
+ """Создает клавиатуру с методами оплаты для платного триала."""
+ texts = get_texts(language)
+ keyboard = []
+
+ # Кнопка оплаты с баланса (если хватает средств)
+ if can_pay_from_balance:
+ keyboard.append([types.InlineKeyboardButton(
+ text="✅ Оплатить с баланса",
+ callback_data="trial_pay_with_balance"
+ )])
+
+ # Добавляем доступные методы оплаты
+ if settings.TELEGRAM_STARS_ENABLED:
+ keyboard.append([types.InlineKeyboardButton(
+ text="⭐ Telegram Stars",
+ callback_data="trial_payment_stars"
+ )])
+
+ if settings.is_yookassa_enabled():
+ yookassa_methods = []
+ if settings.YOOKASSA_SBP_ENABLED:
+ yookassa_methods.append(types.InlineKeyboardButton(
+ text="🏦 YooKassa (СБП)",
+ callback_data="trial_payment_yookassa_sbp"
+ ))
+ yookassa_methods.append(types.InlineKeyboardButton(
+ text="💳 YooKassa (Карта)",
+ callback_data="trial_payment_yookassa"
+ ))
+ if yookassa_methods:
+ keyboard.append(yookassa_methods)
+
+ if settings.is_cryptobot_enabled():
+ keyboard.append([types.InlineKeyboardButton(
+ text="🪙 CryptoBot",
+ callback_data="trial_payment_cryptobot"
+ )])
+
+ if settings.is_heleket_enabled():
+ keyboard.append([types.InlineKeyboardButton(
+ text="🪙 Heleket",
+ callback_data="trial_payment_heleket"
+ )])
+
+ if settings.is_mulenpay_enabled():
+ mulenpay_name = settings.get_mulenpay_display_name()
+ keyboard.append([types.InlineKeyboardButton(
+ text=f"💳 {mulenpay_name}",
+ callback_data="trial_payment_mulenpay"
+ )])
+
+ if settings.is_pal24_enabled():
+ keyboard.append([types.InlineKeyboardButton(
+ text="💳 PayPalych",
+ callback_data="trial_payment_pal24"
+ )])
+
+ if settings.is_wata_enabled():
+ keyboard.append([types.InlineKeyboardButton(
+ text="💳 WATA",
+ callback_data="trial_payment_wata"
+ )])
+
+ # Кнопка назад
+ keyboard.append([types.InlineKeyboardButton(
+ text=texts.BACK,
+ callback_data="menu_trial"
+ )])
+
+ return types.InlineKeyboardMarkup(inline_keyboard=keyboard)
+
+
async def activate_trial(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
from app.services.admin_notification_service import AdminNotificationService
+ from app.services.trial_activation_service import get_trial_activation_charge_amount
texts = get_texts(db_user.language)
@@ -515,29 +591,51 @@ async def activate_trial(
await callback.answer()
return
- try:
- preview_trial_activation_charge(db_user)
- except TrialPaymentInsufficientFunds as error:
- required_label = settings.format_price(error.required_amount)
- balance_label = settings.format_price(error.balance_amount)
- missing_label = settings.format_price(error.missing_amount)
- message = texts.t(
- "TRIAL_PAYMENT_INSUFFICIENT_FUNDS",
- "⚠️ Недостаточно средств для активации триала.\n"
- "Необходимо: {required}\nНа балансе: {balance}\n"
- "Не хватает: {missing}\n\nПополните баланс и попробуйте снова.",
- ).format(required=required_label, balance=balance_label, missing=missing_label)
+ # Проверяем, платный ли триал
+ trial_price_kopeks = get_trial_activation_charge_amount()
+
+ if trial_price_kopeks > 0:
+ # Платный триал - показываем экран с выбором метода оплаты
+ user_balance_kopeks = getattr(db_user, "balance_kopeks", 0) or 0
+ can_pay_from_balance = user_balance_kopeks >= trial_price_kopeks
+
+ traffic_label = "Безлимит" if settings.TRIAL_TRAFFIC_LIMIT_GB == 0 else f"{settings.TRIAL_TRAFFIC_LIMIT_GB} ГБ"
+
+ message_lines = [
+ texts.t("PAID_TRIAL_HEADER", "⚡ Пробная подписка"),
+ "",
+ f"📅 {texts.t('PERIOD', 'Период')}: {settings.TRIAL_DURATION_DAYS} {texts.t('DAYS', 'дней')}",
+ f"📊 {texts.t('TRAFFIC', 'Трафик')}: {traffic_label}",
+ f"📱 {texts.t('DEVICES', 'Устройства')}: {settings.TRIAL_DEVICE_LIMIT}",
+ "",
+ f"💰 {texts.t('PRICE', 'Стоимость')}: {settings.format_price(trial_price_kopeks)}",
+ f"💳 {texts.t('YOUR_BALANCE', 'Ваш баланс')}: {settings.format_price(user_balance_kopeks)}",
+ "",
+ ]
+
+ if can_pay_from_balance:
+ message_lines.append(texts.t(
+ "PAID_TRIAL_CAN_PAY_BALANCE",
+ "Вы можете оплатить пробную подписку с баланса или выбрать другой способ оплаты."
+ ))
+ else:
+ message_lines.append(texts.t(
+ "PAID_TRIAL_SELECT_PAYMENT",
+ "Выберите подходящий способ оплаты:"
+ ))
+
+ message_text = "\n".join(message_lines)
+ keyboard = _get_trial_payment_keyboard(db_user.language, can_pay_from_balance)
await callback.message.edit_text(
- message,
- reply_markup=get_insufficient_balance_keyboard(
- db_user.language,
- amount_kopeks=error.required_amount,
- ),
+ message_text,
+ reply_markup=keyboard,
+ parse_mode="HTML"
)
await callback.answer()
return
+ # Бесплатный триал - текущее поведение
charged_amount = 0
subscription: Optional[Subscription] = None
remnawave_user = None
@@ -2732,6 +2830,696 @@ async def clear_saved_cart(
await callback.answer("🗑️ Корзина очищена")
+
+# ============== ХЕНДЛЕРЫ ПЛАТНОГО ТРИАЛА ==============
+
+@error_handler
+async def handle_trial_pay_with_balance(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
+):
+ """Обрабатывает оплату триала с баланса."""
+ from app.services.trial_activation_service import get_trial_activation_charge_amount
+ from app.services.admin_notification_service import AdminNotificationService
+
+ texts = get_texts(db_user.language)
+
+ # Проверяем права на триал
+ if db_user.subscription or db_user.has_had_paid_subscription:
+ await callback.message.edit_text(
+ texts.TRIAL_ALREADY_USED,
+ reply_markup=get_back_keyboard(db_user.language)
+ )
+ await callback.answer()
+ return
+
+ trial_price_kopeks = get_trial_activation_charge_amount()
+ if trial_price_kopeks <= 0:
+ await callback.answer("❌ Ошибка: триал бесплатный", show_alert=True)
+ return
+
+ user_balance_kopeks = getattr(db_user, "balance_kopeks", 0) or 0
+ if user_balance_kopeks < trial_price_kopeks:
+ await callback.answer(
+ texts.t("INSUFFICIENT_BALANCE", "❌ Недостаточно средств на балансе"),
+ show_alert=True
+ )
+ return
+
+ # Списываем с баланса
+ success = await subtract_user_balance(
+ db,
+ db_user,
+ trial_price_kopeks,
+ texts.t("TRIAL_PAYMENT_DESCRIPTION", "Оплата пробной подписки"),
+ )
+
+ if not success:
+ await callback.answer(
+ texts.t("PAYMENT_FAILED", "❌ Не удалось списать средства"),
+ show_alert=True
+ )
+ return
+
+ await db.refresh(db_user)
+
+ # Создаем триальную подписку
+ subscription: Optional[Subscription] = None
+ remnawave_user = None
+
+ try:
+ forced_devices = None
+ if not settings.is_devices_selection_enabled():
+ forced_devices = settings.get_disabled_mode_device_limit()
+
+ subscription = await create_trial_subscription(
+ db,
+ db_user.id,
+ device_limit=forced_devices,
+ )
+
+ await db.refresh(db_user)
+
+ subscription_service = SubscriptionService()
+ try:
+ remnawave_user = await subscription_service.create_remnawave_user(
+ db,
+ subscription,
+ )
+ except RemnaWaveConfigurationError as error:
+ logger.error("RemnaWave update skipped due to configuration error: %s", error)
+ # Откатываем подписку и возвращаем деньги
+ await rollback_trial_subscription_activation(db, subscription)
+ from app.database.crud.user import add_user_balance
+ await add_user_balance(
+ db,
+ db_user,
+ trial_price_kopeks,
+ texts.t("TRIAL_REFUND_DESCRIPTION", "Возврат за неудачную активацию триала"),
+ transaction_type=TransactionType.REFUND,
+ )
+ await db.refresh(db_user)
+
+ await callback.message.edit_text(
+ texts.t(
+ "TRIAL_PROVISIONING_FAILED",
+ "Не удалось завершить активацию триала. Средства возвращены на баланс.",
+ ),
+ reply_markup=get_back_keyboard(db_user.language),
+ )
+ await callback.answer()
+ return
+ except Exception as error:
+ logger.error(
+ "Failed to create RemnaWave user for trial subscription %s: %s",
+ getattr(subscription, "id", ""),
+ error,
+ )
+ # Откатываем подписку и возвращаем деньги
+ await rollback_trial_subscription_activation(db, subscription)
+ from app.database.crud.user import add_user_balance
+ await add_user_balance(
+ db,
+ db_user,
+ trial_price_kopeks,
+ texts.t("TRIAL_REFUND_DESCRIPTION", "Возврат за неудачную активацию триала"),
+ transaction_type=TransactionType.REFUND,
+ )
+ await db.refresh(db_user)
+
+ await callback.message.edit_text(
+ texts.t(
+ "TRIAL_PROVISIONING_FAILED",
+ "Не удалось завершить активацию триала. Средства возвращены на баланс.",
+ ),
+ reply_markup=get_back_keyboard(db_user.language),
+ )
+ await callback.answer()
+ return
+
+ # Отправляем уведомление админам
+ try:
+ notification_service = AdminNotificationService(callback.bot)
+ await notification_service.send_trial_activation_notification(
+ db,
+ db_user,
+ subscription,
+ charged_amount_kopeks=trial_price_kopeks,
+ )
+ except Exception as e:
+ logger.error(f"Ошибка отправки уведомления о триале: {e}")
+
+ # Показываем успешное сообщение с ссылкой
+ subscription_link = get_display_subscription_link(subscription)
+ hide_subscription_link = settings.should_hide_subscription_link()
+
+ payment_note = "\n\n" + texts.t(
+ "TRIAL_PAYMENT_CHARGED_NOTE",
+ "💳 С вашего баланса списано {amount}.",
+ ).format(amount=settings.format_price(trial_price_kopeks))
+
+ if remnawave_user and subscription_link:
+ if settings.is_happ_cryptolink_mode():
+ trial_success_text = (
+ f"{texts.TRIAL_ACTIVATED}\n\n"
+ + texts.t(
+ "SUBSCRIPTION_HAPP_LINK_PROMPT",
+ "🔒 Ссылка на подписку создана. Нажмите кнопку \"Подключиться\" ниже, чтобы открыть её в Happ.",
+ )
+ + "\n\n"
+ + texts.t(
+ "SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT",
+ "📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве",
+ )
+ )
+ elif hide_subscription_link:
+ trial_success_text = (
+ f"{texts.TRIAL_ACTIVATED}\n\n"
+ + texts.t(
+ "SUBSCRIPTION_LINK_HIDDEN_NOTICE",
+ "ℹ️ Ссылка подписки доступна по кнопкам ниже или в разделе \"Моя подписка\".",
+ )
+ + "\n\n"
+ + texts.t(
+ "SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT",
+ "📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве",
+ )
+ )
+ else:
+ subscription_import_link = texts.t(
+ "SUBSCRIPTION_IMPORT_LINK_SECTION",
+ "🔗 Ваша ссылка для импорта в VPN приложение:\n{subscription_url}",
+ ).format(subscription_url=subscription_link)
+
+ trial_success_text = (
+ f"{texts.TRIAL_ACTIVATED}\n\n"
+ f"{subscription_import_link}\n\n"
+ f"{texts.t('SUBSCRIPTION_IMPORT_INSTRUCTION_PROMPT', '📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве')}"
+ )
+
+ trial_success_text += payment_note
+
+ connect_mode = settings.CONNECT_BUTTON_MODE
+ connect_keyboard = _build_trial_success_keyboard(texts, subscription_link, connect_mode)
+
+ await callback.message.edit_text(
+ trial_success_text,
+ reply_markup=connect_keyboard,
+ parse_mode="HTML",
+ )
+ else:
+ trial_success_text = (
+ f"{texts.TRIAL_ACTIVATED}\n\n⚠️ Ссылка генерируется, попробуйте перейти в раздел 'Моя подписка' через несколько секунд."
+ )
+ trial_success_text += payment_note
+
+ await callback.message.edit_text(
+ trial_success_text,
+ reply_markup=get_back_keyboard(db_user.language),
+ parse_mode="HTML",
+ )
+
+ await callback.answer()
+
+ except Exception as error:
+ logger.error(
+ "Unexpected error during paid trial activation for user %s: %s",
+ db_user.id,
+ error,
+ )
+ # Пытаемся откатить и вернуть деньги
+ if subscription:
+ await rollback_trial_subscription_activation(db, subscription)
+ from app.database.crud.user import add_user_balance
+ await add_user_balance(
+ db,
+ db_user,
+ trial_price_kopeks,
+ texts.t("TRIAL_REFUND_DESCRIPTION", "Возврат за неудачную активацию триала"),
+ transaction_type=TransactionType.REFUND,
+ )
+ await db.refresh(db_user)
+
+ await callback.message.edit_text(
+ texts.t(
+ "TRIAL_ACTIVATION_ERROR",
+ "❌ Произошла ошибка при активации триала. Средства возвращены на баланс.",
+ ),
+ reply_markup=get_back_keyboard(db_user.language),
+ )
+ await callback.answer()
+
+
+def _build_trial_success_keyboard(texts, subscription_link: str, connect_mode: str) -> InlineKeyboardMarkup:
+ """Создает клавиатуру успешной активации триала."""
+
+ if connect_mode == "miniapp_subscription":
+ return InlineKeyboardMarkup(inline_keyboard=[
+ [
+ InlineKeyboardButton(
+ text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
+ web_app=types.WebAppInfo(url=subscription_link),
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu",
+ )
+ ],
+ ])
+ elif connect_mode == "miniapp_custom":
+ if not settings.MINIAPP_CUSTOM_URL:
+ return get_back_keyboard(texts.language if hasattr(texts, 'language') else 'ru')
+
+ return InlineKeyboardMarkup(inline_keyboard=[
+ [
+ InlineKeyboardButton(
+ text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
+ web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL),
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu",
+ )
+ ],
+ ])
+ elif connect_mode == "link":
+ rows = [
+ [
+ InlineKeyboardButton(
+ text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
+ url=subscription_link,
+ )
+ ]
+ ]
+ happ_row = get_happ_download_button_row(texts)
+ if happ_row:
+ rows.append(happ_row)
+ rows.append(
+ [
+ InlineKeyboardButton(
+ text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu",
+ )
+ ]
+ )
+ return InlineKeyboardMarkup(inline_keyboard=rows)
+ elif connect_mode == "happ_cryptolink":
+ rows = [
+ [
+ InlineKeyboardButton(
+ text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
+ callback_data="open_subscription_link",
+ )
+ ]
+ ]
+ happ_row = get_happ_download_button_row(texts)
+ if happ_row:
+ rows.append(happ_row)
+ rows.append(
+ [
+ InlineKeyboardButton(
+ text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu",
+ )
+ ]
+ )
+ return InlineKeyboardMarkup(inline_keyboard=rows)
+ else:
+ return InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ InlineKeyboardButton(
+ text=texts.t("CONNECT_BUTTON", "🔗 Подключиться"),
+ callback_data="subscription_connect",
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text=texts.t("BACK_TO_MAIN_MENU_BUTTON", "⬅️ В главное меню"),
+ callback_data="back_to_menu",
+ )
+ ],
+ ]
+ )
+
+
+@error_handler
+async def handle_trial_payment_method(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
+):
+ """Обрабатывает выбор метода оплаты для платного триала."""
+ from app.services.trial_activation_service import get_trial_activation_charge_amount
+ from app.services.payment_service import PaymentService
+
+ texts = get_texts(db_user.language)
+
+ # Проверяем права на триал
+ if db_user.subscription or db_user.has_had_paid_subscription:
+ await callback.message.edit_text(
+ texts.TRIAL_ALREADY_USED,
+ reply_markup=get_back_keyboard(db_user.language)
+ )
+ await callback.answer()
+ return
+
+ trial_price_kopeks = get_trial_activation_charge_amount()
+ if trial_price_kopeks <= 0:
+ await callback.answer("❌ Ошибка: триал бесплатный", show_alert=True)
+ return
+
+ # Определяем метод оплаты
+ payment_method = callback.data.replace("trial_payment_", "")
+
+ try:
+ payment_service = PaymentService(callback.bot)
+
+ # Получаем случайный сквад для триала
+ from app.database.crud.server_squad import get_random_trial_squad_uuid
+ trial_squad_uuid = await get_random_trial_squad_uuid(db)
+
+ # Создаем pending триальную подписку
+ pending_subscription = await create_pending_trial_subscription(
+ db=db,
+ user_id=db_user.id,
+ duration_days=settings.TRIAL_DURATION_DAYS,
+ traffic_limit_gb=settings.TRIAL_TRAFFIC_LIMIT_GB,
+ device_limit=settings.TRIAL_DEVICE_LIMIT,
+ connected_squads=[trial_squad_uuid] if trial_squad_uuid else [],
+ payment_method=f"trial_{payment_method}",
+ total_price_kopeks=trial_price_kopeks,
+ )
+
+ if not pending_subscription:
+ await callback.answer("❌ Не удалось подготовить заказ. Попробуйте позже.", show_alert=True)
+ return
+
+ traffic_label = "Безлимит" if settings.TRIAL_TRAFFIC_LIMIT_GB == 0 else f"{settings.TRIAL_TRAFFIC_LIMIT_GB} ГБ"
+
+ if payment_method == "stars":
+ # Оплата через Telegram Stars
+ stars_count = settings.rubles_to_stars(settings.kopeks_to_rubles(trial_price_kopeks))
+
+ await callback.bot.send_invoice(
+ chat_id=callback.from_user.id,
+ title=texts.t("PAID_TRIAL_INVOICE_TITLE", "Пробная подписка на {days} дней").format(
+ days=settings.TRIAL_DURATION_DAYS
+ ),
+ description=(
+ f"{texts.t('PERIOD', 'Период')}: {settings.TRIAL_DURATION_DAYS} {texts.t('DAYS', 'дней')}\n"
+ f"{texts.t('DEVICES', 'Устройства')}: {settings.TRIAL_DEVICE_LIMIT}\n"
+ f"{texts.t('TRAFFIC', 'Трафик')}: {traffic_label}"
+ ),
+ payload=f"trial_{pending_subscription.id}",
+ provider_token="",
+ currency="XTR",
+ prices=[types.LabeledPrice(
+ label=texts.t("PAID_TRIAL_STARS_LABEL", "Пробная подписка"),
+ amount=stars_count
+ )],
+ )
+
+ await callback.message.edit_text(
+ texts.t(
+ "PAID_TRIAL_STARS_WAITING",
+ "⭐ Для оплаты пробной подписки нажмите кнопку оплаты в сообщении выше.\n\n"
+ "После успешной оплаты подписка будет активирована автоматически."
+ ),
+ reply_markup=get_back_keyboard(db_user.language),
+ parse_mode="HTML",
+ )
+
+ elif payment_method == "yookassa_sbp":
+ # Оплата через YooKassa СБП
+ payment_result = await payment_service.create_yookassa_sbp_payment(
+ amount_kopeks=trial_price_kopeks,
+ description=texts.t("PAID_TRIAL_PAYMENT_DESC", "Пробная подписка на {days} дней").format(
+ days=settings.TRIAL_DURATION_DAYS
+ ),
+ user_id=db_user.id,
+ metadata={
+ "type": "trial",
+ "subscription_id": pending_subscription.id,
+ "user_id": db_user.id,
+ },
+ )
+
+ if not payment_result or not payment_result.get("confirmation_url"):
+ await callback.answer("❌ Не удалось создать платеж. Попробуйте позже.", show_alert=True)
+ return
+
+ qr_url = payment_result.get("qr_code_url") or payment_result.get("confirmation_url")
+
+ await callback.message.edit_text(
+ texts.t(
+ "PAID_TRIAL_YOOKASSA_SBP",
+ "🏦 Оплата через СБП\n\n"
+ "Отсканируйте QR-код или перейдите по ссылке для оплаты.\n\n"
+ "💰 Сумма: {amount}"
+ ).format(amount=settings.format_price(trial_price_kopeks)),
+ reply_markup=InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text="💳 Оплатить", url=qr_url)],
+ [InlineKeyboardButton(text=texts.BACK, callback_data="trial_activate")],
+ ]),
+ parse_mode="HTML",
+ )
+
+ elif payment_method == "yookassa":
+ # Оплата через YooKassa карта
+ payment_result = await payment_service.create_yookassa_payment(
+ amount_kopeks=trial_price_kopeks,
+ description=texts.t("PAID_TRIAL_PAYMENT_DESC", "Пробная подписка на {days} дней").format(
+ days=settings.TRIAL_DURATION_DAYS
+ ),
+ user_id=db_user.id,
+ metadata={
+ "type": "trial",
+ "subscription_id": pending_subscription.id,
+ "user_id": db_user.id,
+ },
+ )
+
+ if not payment_result or not payment_result.get("confirmation_url"):
+ await callback.answer("❌ Не удалось создать платеж. Попробуйте позже.", show_alert=True)
+ return
+
+ await callback.message.edit_text(
+ texts.t(
+ "PAID_TRIAL_YOOKASSA_CARD",
+ "💳 Оплата картой\n\n"
+ "Нажмите кнопку ниже для перехода к оплате.\n\n"
+ "💰 Сумма: {amount}"
+ ).format(amount=settings.format_price(trial_price_kopeks)),
+ reply_markup=InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text="💳 Оплатить", url=payment_result["confirmation_url"])],
+ [InlineKeyboardButton(text=texts.BACK, callback_data="trial_activate")],
+ ]),
+ parse_mode="HTML",
+ )
+
+ elif payment_method == "cryptobot":
+ # Оплата через CryptoBot
+ payment_result = await payment_service.create_cryptobot_payment(
+ amount_kopeks=trial_price_kopeks,
+ description=texts.t("PAID_TRIAL_PAYMENT_DESC", "Пробная подписка на {days} дней").format(
+ days=settings.TRIAL_DURATION_DAYS
+ ),
+ user_id=db_user.id,
+ metadata={
+ "type": "trial",
+ "subscription_id": pending_subscription.id,
+ "user_id": db_user.id,
+ },
+ )
+
+ if not payment_result or not payment_result.get("pay_url"):
+ await callback.answer("❌ Не удалось создать платеж. Попробуйте позже.", show_alert=True)
+ return
+
+ await callback.message.edit_text(
+ texts.t(
+ "PAID_TRIAL_CRYPTOBOT",
+ "🪙 Оплата криптовалютой\n\n"
+ "Нажмите кнопку ниже для перехода к оплате.\n\n"
+ "💰 Сумма: {amount}"
+ ).format(amount=settings.format_price(trial_price_kopeks)),
+ reply_markup=InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text="🪙 Оплатить", url=payment_result["pay_url"])],
+ [InlineKeyboardButton(
+ text=texts.t("CHECK_PAYMENT", "🔄 Проверить оплату"),
+ callback_data=f"check_trial_cryptobot_{pending_subscription.id}"
+ )],
+ [InlineKeyboardButton(text=texts.BACK, callback_data="trial_activate")],
+ ]),
+ parse_mode="HTML",
+ )
+
+ elif payment_method == "heleket":
+ # Оплата через Heleket
+ payment_result = await payment_service.create_heleket_payment(
+ amount_kopeks=trial_price_kopeks,
+ description=texts.t("PAID_TRIAL_PAYMENT_DESC", "Пробная подписка на {days} дней").format(
+ days=settings.TRIAL_DURATION_DAYS
+ ),
+ user_id=db_user.id,
+ metadata={
+ "type": "trial",
+ "subscription_id": pending_subscription.id,
+ "user_id": db_user.id,
+ },
+ )
+
+ if not payment_result or not payment_result.get("pay_url"):
+ await callback.answer("❌ Не удалось создать платеж. Попробуйте позже.", show_alert=True)
+ return
+
+ await callback.message.edit_text(
+ texts.t(
+ "PAID_TRIAL_HELEKET",
+ "🪙 Оплата криптовалютой (Heleket)\n\n"
+ "Нажмите кнопку ниже для перехода к оплате.\n\n"
+ "💰 Сумма: {amount}"
+ ).format(amount=settings.format_price(trial_price_kopeks)),
+ reply_markup=InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text="🪙 Оплатить", url=payment_result["pay_url"])],
+ [InlineKeyboardButton(
+ text=texts.t("CHECK_PAYMENT", "🔄 Проверить оплату"),
+ callback_data=f"check_trial_heleket_{pending_subscription.id}"
+ )],
+ [InlineKeyboardButton(text=texts.BACK, callback_data="trial_activate")],
+ ]),
+ parse_mode="HTML",
+ )
+
+ elif payment_method == "mulenpay":
+ # Оплата через MulenPay
+ payment_result = await payment_service.create_mulenpay_payment(
+ amount_kopeks=trial_price_kopeks,
+ description=texts.t("PAID_TRIAL_PAYMENT_DESC", "Пробная подписка на {days} дней").format(
+ days=settings.TRIAL_DURATION_DAYS
+ ),
+ user_id=db_user.id,
+ metadata={
+ "type": "trial",
+ "subscription_id": pending_subscription.id,
+ "user_id": db_user.id,
+ },
+ )
+
+ if not payment_result or not payment_result.get("pay_url"):
+ await callback.answer("❌ Не удалось создать платеж. Попробуйте позже.", show_alert=True)
+ return
+
+ mulenpay_name = settings.get_mulenpay_display_name()
+ await callback.message.edit_text(
+ texts.t(
+ "PAID_TRIAL_MULENPAY",
+ "💳 Оплата через {name}\n\n"
+ "Нажмите кнопку ниже для перехода к оплате.\n\n"
+ "💰 Сумма: {amount}"
+ ).format(name=mulenpay_name, amount=settings.format_price(trial_price_kopeks)),
+ reply_markup=InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text="💳 Оплатить", url=payment_result["pay_url"])],
+ [InlineKeyboardButton(
+ text=texts.t("CHECK_PAYMENT", "🔄 Проверить оплату"),
+ callback_data=f"check_trial_mulenpay_{pending_subscription.id}"
+ )],
+ [InlineKeyboardButton(text=texts.BACK, callback_data="trial_activate")],
+ ]),
+ parse_mode="HTML",
+ )
+
+ elif payment_method == "pal24":
+ # Оплата через PAL24
+ payment_result = await payment_service.create_pal24_payment(
+ amount_kopeks=trial_price_kopeks,
+ description=texts.t("PAID_TRIAL_PAYMENT_DESC", "Пробная подписка на {days} дней").format(
+ days=settings.TRIAL_DURATION_DAYS
+ ),
+ user_id=db_user.id,
+ metadata={
+ "type": "trial",
+ "subscription_id": pending_subscription.id,
+ "user_id": db_user.id,
+ },
+ )
+
+ if not payment_result or not payment_result.get("pay_url"):
+ await callback.answer("❌ Не удалось создать платеж. Попробуйте позже.", show_alert=True)
+ return
+
+ await callback.message.edit_text(
+ texts.t(
+ "PAID_TRIAL_PAL24",
+ "💳 Оплата через PayPalych\n\n"
+ "Нажмите кнопку ниже для перехода к оплате.\n\n"
+ "💰 Сумма: {amount}"
+ ).format(amount=settings.format_price(trial_price_kopeks)),
+ reply_markup=InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text="💳 Оплатить", url=payment_result["pay_url"])],
+ [InlineKeyboardButton(
+ text=texts.t("CHECK_PAYMENT", "🔄 Проверить оплату"),
+ callback_data=f"check_trial_pal24_{pending_subscription.id}"
+ )],
+ [InlineKeyboardButton(text=texts.BACK, callback_data="trial_activate")],
+ ]),
+ parse_mode="HTML",
+ )
+
+ elif payment_method == "wata":
+ # Оплата через WATA
+ payment_result = await payment_service.create_wata_payment(
+ amount_kopeks=trial_price_kopeks,
+ description=texts.t("PAID_TRIAL_PAYMENT_DESC", "Пробная подписка на {days} дней").format(
+ days=settings.TRIAL_DURATION_DAYS
+ ),
+ user_id=db_user.id,
+ metadata={
+ "type": "trial",
+ "subscription_id": pending_subscription.id,
+ "user_id": db_user.id,
+ },
+ )
+
+ if not payment_result or not payment_result.get("pay_url"):
+ await callback.answer("❌ Не удалось создать платеж. Попробуйте позже.", show_alert=True)
+ return
+
+ await callback.message.edit_text(
+ texts.t(
+ "PAID_TRIAL_WATA",
+ "💳 Оплата через WATA\n\n"
+ "Нажмите кнопку ниже для перехода к оплате.\n\n"
+ "💰 Сумма: {amount}"
+ ).format(amount=settings.format_price(trial_price_kopeks)),
+ reply_markup=InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text="💳 Оплатить", url=payment_result["pay_url"])],
+ [InlineKeyboardButton(
+ text=texts.t("CHECK_PAYMENT", "🔄 Проверить оплату"),
+ callback_data=f"check_trial_wata_{pending_subscription.id}"
+ )],
+ [InlineKeyboardButton(text=texts.BACK, callback_data="trial_activate")],
+ ]),
+ parse_mode="HTML",
+ )
+
+ else:
+ await callback.answer(f"❌ Неизвестный метод оплаты: {payment_method}", show_alert=True)
+ return
+
+ await callback.answer()
+
+ except Exception as error:
+ logger.error(f"Error processing trial payment method {payment_method}: {error}")
+ await callback.answer("❌ Произошла ошибка при создании платежа. Попробуйте позже.", show_alert=True)
+
+
def register_handlers(dp: Dispatcher):
update_traffic_prices()
@@ -2750,6 +3538,17 @@ def register_handlers(dp: Dispatcher):
F.data == "trial_activate"
)
+ # Хендлеры платного триала
+ dp.callback_query.register(
+ handle_trial_pay_with_balance,
+ F.data == "trial_pay_with_balance"
+ )
+
+ dp.callback_query.register(
+ handle_trial_payment_method,
+ F.data.startswith("trial_payment_")
+ )
+
dp.callback_query.register(
start_subscription_purchase,
F.data.in_(["menu_buy", "subscription_upgrade", "subscription_purchase"])