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"])