diff --git a/.env.example b/.env.example
index 9ada6fc5..9a4bd047 100644
--- a/.env.example
+++ b/.env.example
@@ -838,15 +838,9 @@ CONTESTS_BUTTON_VISIBLE=false
# Реферальные конкурсы (турниры среди рефералов)
REFERRAL_CONTESTS_ENABLED=false
-# ===== АВТОАКТИВАЦИЯ ПОСЛЕ ПОПОЛНЕНИЯ =====
+# ===== АВТОПОКУПКА ПОСЛЕ ПОПОЛНЕНИЯ =====
# Автоматическая покупка из сохранённой корзины после пополнения баланса
AUTO_PURCHASE_AFTER_TOPUP_ENABLED=false
-# Умная автоактивация: система сама решает — продлить или создать подписку
-# Работает даже без сохранённой корзины. Выбирает максимальный период <= баланса
-AUTO_ACTIVATE_AFTER_TOPUP_ENABLED=false
-# Показывать предупреждение об активации подписки после пополнения баланса
-# Если true - после пополнения показывает сообщение с кнопками: "Активировать", "Продлить", "Добавить устройства"
-SHOW_ACTIVATION_PROMPT_AFTER_TOPUP=false
# ===== КНОПКА АКТИВАЦИИ =====
ACTIVATE_BUTTON_VISIBLE=false
diff --git a/README.md b/README.md
index 15301c60..a846714e 100644
--- a/README.md
+++ b/README.md
@@ -1342,7 +1342,6 @@ CONTEST_BUTTON_VISIBLE=true
- 🔄 Автоплатёж с настройкой дня списания
- 🎁 Реферальные и промо-бонусы
- ⚡ **Быстрое пополнение** с кнопками быстрых сумм
-- 🔄 **Умная автоактивация** подписки после пополнения баланса
📱 **Управление подписками**
@@ -1529,7 +1528,6 @@ CONTEST_BUTTON_VISIBLE=true
- 🔄 **Миграция сквадов** - массовый перенос пользователей между сквадами
- 🧾 **История операций** - хранение всех транзакций и действий для аудита
- 💸 **Сервис автопроверки транзакций** - автоматическая проверка транзакций в статусе "В ожидании оплаты" за последние 24ч
-- 🔄 **Умная автоактивация** - автоматическая активация подписки после пополнения баланса
- 📝 **Ротация логов** - автоматическая очистка и архивация старых логов
- 🎮 **Система конкурсов** - ежедневные игры и реферальные конкурсы с призами
diff --git a/app/config.py b/app/config.py
index 260b8def..392a64ae 100644
--- a/app/config.py
+++ b/app/config.py
@@ -339,12 +339,6 @@ class Settings(BaseSettings):
NALOGO_STORAGE_PATH: str = './nalogo_tokens.json'
AUTO_PURCHASE_AFTER_TOPUP_ENABLED: bool = False
- AUTO_ACTIVATE_AFTER_TOPUP_ENABLED: bool = False
-
- # Показывать предупреждение об активации подписки после пополнения баланса
- # Если True - после пополнения показывает большое сообщение с кнопками:
- # "Активировать", "Продлить", "Добавить устройства"
- SHOW_ACTIVATION_PROMPT_AFTER_TOPUP: bool = False
# Отключение превью ссылок в сообщениях бота
DISABLE_WEB_PAGE_PREVIEW: bool = False
@@ -1183,16 +1177,6 @@ class Settings(BaseSettings):
return bool(value)
- def is_auto_activate_after_topup_enabled(self) -> bool:
- """Умная автоактивация после пополнения баланса (без корзины)."""
- value = getattr(self, 'AUTO_ACTIVATE_AFTER_TOPUP_ENABLED', False)
-
- if isinstance(value, str):
- normalized = value.strip().lower()
- return normalized in {'1', 'true', 'yes', 'on'}
-
- return bool(value)
-
def is_quick_amount_buttons_enabled(self) -> bool:
"""Показывать ли кнопки быстрого выбора суммы пополнения."""
return self.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not self.DISABLE_TOPUP_BUTTONS
diff --git a/app/database/crud/subscription.py b/app/database/crud/subscription.py
index 6b94c8b4..06dd6c64 100644
--- a/app/database/crud/subscription.py
+++ b/app/database/crud/subscription.py
@@ -95,6 +95,25 @@ async def create_trial_subscription(
end_date = datetime.utcnow() + timedelta(days=duration_days)
+ # Check for existing PENDING trial subscription (retry after failed payment)
+ existing = await get_subscription_by_user_id(db, user_id)
+ if existing and existing.is_trial and existing.status == SubscriptionStatus.PENDING.value:
+ existing.status = SubscriptionStatus.ACTIVE.value
+ existing.start_date = datetime.utcnow()
+ existing.end_date = end_date
+ existing.traffic_limit_gb = traffic_limit_gb
+ existing.device_limit = device_limit
+ existing.connected_squads = final_squads
+ existing.tariff_id = tariff_id
+ await db.commit()
+ await db.refresh(existing)
+ logger.info(
+ '🎁 Обновлена PENDING триальная подписка %s для пользователя %s',
+ existing.id,
+ user_id,
+ )
+ return existing
+
subscription = Subscription(
user_id=user_id,
status=SubscriptionStatus.ACTIVE.value,
diff --git a/app/handlers/start.py b/app/handlers/start.py
index 4cf25d11..3118a703 100644
--- a/app/handlers/start.py
+++ b/app/handlers/start.py
@@ -2048,9 +2048,22 @@ async def required_sub_channel_check(
)
await state.set_state(RegistrationStates.waiting_for_rules_accept)
+ except TelegramBadRequest as e:
+ error_msg = str(e).lower()
+ if 'query is too old' in error_msg or 'query id is invalid' in error_msg:
+ logger.debug('Устаревший callback в required_sub_channel_check, игнорируем')
+ else:
+ logger.error(f'Ошибка Telegram API в required_sub_channel_check: {e}')
+ try:
+ await query.answer(f'{texts.ERROR}!', show_alert=True)
+ except Exception:
+ pass
except Exception as e:
logger.error(f'Ошибка в required_sub_channel_check: {e}')
- await query.answer(f'{texts.ERROR}!', show_alert=True)
+ try:
+ await query.answer(f'{texts.ERROR}!', show_alert=True)
+ except Exception:
+ pass
def register_handlers(dp: Dispatcher):
diff --git a/app/handlers/subscription/devices.py b/app/handlers/subscription/devices.py
index 9101c022..396a8b11 100644
--- a/app/handlers/subscription/devices.py
+++ b/app/handlers/subscription/devices.py
@@ -416,6 +416,7 @@ async def confirm_change_devices(callback: types.CallbackQuery, db_user: User, d
).format(
amount=texts.format_price(price),
period=period_label,
+ months=period_label,
)
if total_discount > 0:
cost_text += texts.t(
diff --git a/app/handlers/subscription/purchase.py b/app/handlers/subscription/purchase.py
index 431ea2d9..c8ffc466 100644
--- a/app/handlers/subscription/purchase.py
+++ b/app/handlers/subscription/purchase.py
@@ -3227,6 +3227,9 @@ async def handle_trial_pay_with_balance(callback: types.CallbackQuery, db_user:
await db.refresh(db_user)
+ # Сохраняем ID до начала транзакции (на случай detached session)
+ user_id_snapshot = db_user.id
+
# Создаем триальную подписку
subscription: Subscription | None = None
remnawave_user = None
@@ -3388,22 +3391,33 @@ async def handle_trial_pay_with_balance(callback: types.CallbackQuery, db_user:
except Exception as error:
logger.error(
'Unexpected error during paid trial activation for user %s: %s',
- db_user.id,
+ user_id_snapshot,
error,
)
- # Пытаемся откатить и вернуть деньги
- if subscription:
- await rollback_trial_subscription_activation(db, subscription)
- from app.database.crud.user import add_user_balance
+ # Откатываем сессию чтобы очистить PendingRollbackError
+ try:
+ await db.rollback()
+ except Exception:
+ pass
- await add_user_balance(
- db,
- db_user,
- trial_price_kopeks,
- texts.t('TRIAL_REFUND_DESCRIPTION', 'Возврат за неудачную активацию триала'),
- transaction_type=TransactionType.REFUND,
- )
- await db.refresh(db_user)
+ # Пытаемся вернуть деньги
+ try:
+ 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)
+ except Exception as refund_error:
+ logger.error(
+ 'Failed to refund trial payment for user %s: %s',
+ user_id_snapshot,
+ refund_error,
+ )
await callback.message.edit_text(
texts.t(
diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json
index d870c0ad..9ae174d0 100644
--- a/app/localization/locales/en.json
+++ b/app/localization/locales/en.json
@@ -1548,6 +1548,9 @@
"TRIAL_PROVISIONING_FAILED": "We couldn't finish setting up the trial. Any charge has been refunded. Please try again later.",
"TRIAL_ROLLBACK_FAILED": "We couldn't cancel the trial activation after a payment error. Please contact support and try again later.",
"TRIAL_REFUND_FAILED": "We couldn't refund the trial activation charge. Please contact support immediately.",
+ "TRIAL_PAYMENT_DESCRIPTION": "Trial subscription payment",
+ "TRIAL_REFUND_DESCRIPTION": "Refund for failed trial activation",
+ "TRIAL_ACTIVATION_ERROR": "❌ An error occurred during trial activation. Funds have been returned to your balance.",
"TRIAL_PAYMENT_CHARGED_NOTE": "💳 {amount} has been deducted from your balance.",
"TRIAL_CHANNEL_UNSUBSCRIBED": "\n🚫 Access paused\n\nWe couldn't find your subscription to our channel, so the trial plan has been disabled.\n\nJoin the channel and tap “{check_button}” to restore access.",
"TRIAL_ENDING_SOON": "\n🎁 The trial subscription is ending soon!\n\nYour trial expires in a few hours.\n\n💎 Don't want to lose VPN access?\nSwitch to the full subscription!\n\n🔥 Special offer:\n• 30 days for {price}\n• Unlimited traffic\n• All servers available\n• Speeds up to 1 Gbit/s\n\n⚡️ Activate before the trial ends!\n",
diff --git a/app/localization/locales/fa.json b/app/localization/locales/fa.json
index d77ae92b..49f549f3 100644
--- a/app/localization/locales/fa.json
+++ b/app/localization/locales/fa.json
@@ -1569,6 +1569,9 @@
"TRIAL_PROVISIONING_FAILED": "فعالسازی آزمایشی ناموفق. وجه بازگشت داده شد. بعداً تلاش کنید.",
"TRIAL_ROLLBACK_FAILED": "لغو آزمایشی پس از خطا ناموفق. با پشتیبانی تماس بگیرید.",
"TRIAL_REFUND_FAILED": "بازگشت وجه آزمایشی ناموفق. فوراً با پشتیبانی تماس بگیرید.",
+ "TRIAL_PAYMENT_DESCRIPTION": "پرداخت اشتراک آزمایشی",
+ "TRIAL_REFUND_DESCRIPTION": "بازگشت وجه به دلیل خطا در فعالسازی آزمایشی",
+ "TRIAL_ACTIVATION_ERROR": "❌ خطا در فعالسازی آزمایشی. وجه به موجودی بازگردانده شد.",
"TRIAL_PAYMENT_CHARGED_NOTE": "💳 از موجودی شما {amount} کسر شد.",
"TRIAL_CHANNEL_UNSUBSCRIBED": "\n🚫 دسترسی متوقف شد\n\nعضویت شما در کانال یافت نشد، آزمایشی غیرفعال شد.\n\nعضو شوید و «{check_button}» بزنید.",
"TRIAL_ENDING_SOON": "\n🎁 آزمایشی بهزودی تمام میشود!\n\nچند ساعت تا انقضا مانده.\n\n💎 بدون VPN نمانید!\n\n🔥 پیشنهاد ویژه:\n• ۳۰ روز فقط {price}\n• ترافیک نامحدود\n• همه سرورها\n• سرعت تا ۱ گیگ\n\n⚡️ قبل از پایان اقدام کنید!\n",
diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json
index 8292133f..5121051d 100644
--- a/app/localization/locales/ru.json
+++ b/app/localization/locales/ru.json
@@ -1569,6 +1569,9 @@
"TRIAL_PROVISIONING_FAILED": "Не удалось завершить активацию триала. Средства возвращены на баланс. Попробуйте позже.",
"TRIAL_ROLLBACK_FAILED": "Не удалось отменить активацию триала после ошибки списания. Свяжитесь с поддержкой и попробуйте позже.",
"TRIAL_REFUND_FAILED": "Не удалось вернуть оплату за активацию триала. Немедленно свяжитесь с поддержкой.",
+ "TRIAL_PAYMENT_DESCRIPTION": "Оплата пробной подписки",
+ "TRIAL_REFUND_DESCRIPTION": "Возврат за неудачную активацию триала",
+ "TRIAL_ACTIVATION_ERROR": "❌ Произошла ошибка при активации триала. Средства возвращены на баланс.",
"TRIAL_PAYMENT_CHARGED_NOTE": "💳 С вашего баланса списано {amount}.",
"TRIAL_CHANNEL_UNSUBSCRIBED": "\n🚫 Доступ приостановлен\n\nМы не нашли вашу подписку на наш канал, поэтому тестовая подписка отключена.\n\nПодпишитесь на канал и нажмите «{check_button}», чтобы вернуть доступ.",
"TRIAL_ENDING_SOON": "\n🎁 Тестовая подписка скоро закончится!\n\nВаша тестовая подписка истекает через несколько часов.\n\n💎 Не хотите остаться без VPN?\nПереходите на полную подписку!\n\n🔥 Специальное предложение:\n• 30 дней всего за {price}\n• Безлимитный трафик \n• Все серверы доступны\n• Скорость до 1ГБит/сек\n\n⚡️ Успейте оформить до окончания тестового периода!\n",
diff --git a/app/localization/locales/ua.json b/app/localization/locales/ua.json
index f77943d1..7959a00a 100644
--- a/app/localization/locales/ua.json
+++ b/app/localization/locales/ua.json
@@ -1479,6 +1479,9 @@
"TRIAL_PROVISIONING_FAILED": "Не вдалося завершити активацію тріалу. Кошти повернуто на баланс. Спробуйте пізніше.",
"TRIAL_ROLLBACK_FAILED": "Не вдалося скасувати активацію тріалу після помилки списання. Зв'яжіться з підтримкою і спробуйте пізніше.",
"TRIAL_REFUND_FAILED": "Не вдалося повернути оплату за активацію тріалу. Негайно зв'яжіться з підтримкою.",
+ "TRIAL_PAYMENT_DESCRIPTION": "Оплата пробної підписки",
+ "TRIAL_REFUND_DESCRIPTION": "Повернення за невдалу активацію тріалу",
+ "TRIAL_ACTIVATION_ERROR": "❌ Виникла помилка при активації тріалу. Кошти повернуто на баланс.",
"TRIAL_PAYMENT_CHARGED_NOTE": "💳 З вашого балансу списано {amount}.",
"TRIAL_CHANNEL_UNSUBSCRIBED": "\n🚫 Доступ призупинено\n\nМи не знайшли вашу підписку на наш канал, тому тестову підписку вимкнено.\n\nПідпишіться на канал і натисніть «{check_button}», щоб повернути доступ.",
"TRIAL_ENDING_SOON": "\n🎁 Тестова підписка скоро закінчиться!\n\nВаша тестова підписка закінчується через декілька годин.\n\n💎 Не хочете залишитися без VPN?\nПереходьте на повну підписку!\n\n🔥 Спеціальна пропозиція:\n• 30 днів усього за {price}\n• Безлімітний трафік \n• Всі сервери доступні\n• Швидкість до 1ГБіт/сек\n\n⚡️ Встигніть оформити до закінчення тестового періоду!\n",
diff --git a/app/localization/locales/zh.json b/app/localization/locales/zh.json
index 6f8fc731..bc16170f 100644
--- a/app/localization/locales/zh.json
+++ b/app/localization/locales/zh.json
@@ -1477,6 +1477,9 @@
"TRIAL_PROVISIONING_FAILED":"未能完成试用激活。资金已退回余额。请稍后再试。",
"TRIAL_ROLLBACK_FAILED":"扣款失败后未能取消试用激活。请联系支持并稍后再试。",
"TRIAL_REFUND_FAILED":"未能退还试用激活付款。请立即联系支持。",
+"TRIAL_PAYMENT_DESCRIPTION":"试用订阅付款",
+"TRIAL_REFUND_DESCRIPTION":"试用激活失败退款",
+"TRIAL_ACTIVATION_ERROR":"❌ 试用激活出错。资金已退回余额。",
"TRIAL_PAYMENT_CHARGED_NOTE":"💳已从您的余额中扣除{amount}。",
"TRIAL_CHANNEL_UNSUBSCRIBED":"\n🚫访问已暂停\n\n我们未找到您对我们频道的订阅,因此试用订阅已禁用。\n\n请订阅频道并点击“{check_button}”以恢复访问。",
"TRIAL_ENDING_SOON":"\n🎁试用订阅即将结束!\n\n您的试用订阅将在几小时后过期。\n\n💎不想没有VPN吗?\n升级到完整订阅!\n\n🔥特别优惠:\n•30天仅需{price}\n•无限流量 \n•所有服务器可用\n•速度高达1Gbit/s\n\n⚡️赶在试用期结束前订购吧!\n",
@@ -1807,6 +1810,9 @@
"TRIAL_PROVISIONING_FAILED":"未能完成试用激活。资金已退回余额。请稍后再试。",
"TRIAL_ROLLBACK_FAILED":"扣款失败后未能取消试用激活。请联系支持并稍后再试。",
"TRIAL_REFUND_FAILED":"未能退还试用激活付款。请立即联系支持。",
+"TRIAL_PAYMENT_DESCRIPTION":"试用订阅付款",
+"TRIAL_REFUND_DESCRIPTION":"试用激活失败退款",
+"TRIAL_ACTIVATION_ERROR":"❌ 试用激活出错。资金已退回余额。",
"TRIAL_PAYMENT_CHARGED_NOTE":"💳已从您的余额中扣除{amount}。",
"TRIAL_CHANNEL_UNSUBSCRIBED":"\n🚫访问已暂停\n\n我们未找到您对我们频道的订阅,因此试用订阅已禁用。\n\n请订阅频道并点击“{check_button}”以恢复访问。",
"TRIAL_ENDING_SOON":"\n🎁试用订阅即将结束!\n\n您的试用订阅将在几小时后过期。\n\n💎不想没有VPN吗?\n升级到完整订阅!\n\n🔥特别优惠:\n•30天仅需{price}\n•无限流量 \n•所有服务器可用\n•速度高达1Gbit/s\n\n⚡️赶在试用期结束前订购吧!\n",
diff --git a/app/services/payment/cloudpayments.py b/app/services/payment/cloudpayments.py
index 5cff5337..93451982 100644
--- a/app/services/payment/cloudpayments.py
+++ b/app/services/payment/cloudpayments.py
@@ -12,7 +12,6 @@ from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.cloudpayments_service import CloudPaymentsAPIError
from app.services.subscription_auto_purchase_service import (
- auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -262,22 +261,11 @@ class CloudPaymentsPaymentMixin:
logger.exception('Ошибка отправки уведомления CloudPayments: %s', error)
# Auto-purchase if enabled
- auto_purchase_success = False
try:
- auto_purchase_success = await auto_purchase_saved_cart_after_topup(db, user, bot=getattr(self, 'bot', None))
+ await auto_purchase_saved_cart_after_topup(db, user, bot=getattr(self, 'bot', None))
except Exception as error:
logger.exception('Ошибка автопокупки после CloudPayments: %s', error)
- # Умная автоактивация если автопокупка не сработала
- if not auto_purchase_success:
- try:
- # Игнорируем notification_sent т.к. здесь нет дополнительных уведомлений
- await auto_activate_subscription_after_topup(
- db, user, bot=getattr(self, 'bot', None), topup_amount=amount_kopeks
- )
- except Exception as error:
- logger.exception('Ошибка умной автоактивации после CloudPayments: %s', error)
-
return True
async def process_cloudpayments_fail_webhook(
diff --git a/app/services/payment/common.py b/app/services/payment/common.py
index a4a2d3d7..3e97a631 100644
--- a/app/services/payment/common.py
+++ b/app/services/payment/common.py
@@ -171,79 +171,18 @@ class PaymentCommonMixin:
try:
payment_method = payment_method_title or 'Банковская карта (YooKassa)'
- # Проверяем, нужно ли показывать яркое предупреждение об активации
- if settings.SHOW_ACTIVATION_PROMPT_AFTER_TOPUP:
- # Определяем статус подписки для выбора правильной кнопки
- has_active_subscription = False
- if user_snapshot:
- try:
- subscription = user_snapshot.subscription
- has_active_subscription = bool(
- subscription
- and not getattr(subscription, 'is_trial', False)
- and getattr(subscription, 'is_active', False)
- )
- except Exception:
- pass
-
- # Яркое сообщение с восклицательными знаками
- message = (
- '✅ Платеж успешно завершен!\n\n'
- f'💰 Сумма: {settings.format_price(amount_kopeks)}\n'
- f'💳 Способ: {payment_method}\n\n'
- '💎 Средства зачислены на ваш баланс!\n\n'
- '‼️ ВНИМАНИЕ! ОБЯЗАТЕЛЬНО АКТИВИРУЙТЕ ПОДПИСКУ! ‼️\n\n'
- '⚠️ Пополнение баланса НЕ АКТИВИРУЕТ подписку автоматически!\n\n'
- '👇 НАЖМИТЕ КНОПКУ НИЖЕ ДЛЯ АКТИВАЦИИ 👇'
- )
-
- # Формируем клавиатуру с кнопками действий
- keyboard_rows: list[list[InlineKeyboardButton]] = []
-
- # Кнопка активации или продления в зависимости от статуса
- if has_active_subscription:
- # Активная платная подписка - показываем продление и изменение устройств
- keyboard_rows.append(
- [
- build_miniapp_or_callback_button(
- text='🔄 ПРОДЛИТЬ ПОДПИСКУ',
- callback_data='subscription_extend',
- )
- ]
- )
- keyboard_rows.append(
- [
- build_miniapp_or_callback_button(
- text='📱 Изменить количество устройств',
- callback_data='subscription_change_devices',
- )
- ]
- )
- else:
- # Нет подписки или истекла - показываем только активацию
- keyboard_rows.append(
- [
- build_miniapp_or_callback_button(
- text='🔥 АКТИВИРОВАТЬ ПОДПИСКУ',
- callback_data='menu_buy',
- )
- ]
- )
-
- keyboard = InlineKeyboardMarkup(inline_keyboard=keyboard_rows)
- else:
- # Стандартное сообщение с полной клавиатурой
- keyboard = await self.build_topup_success_keyboard(user_snapshot)
- message = (
- '✅ Платеж успешно завершен!\n\n'
- f'💰 Сумма: {settings.format_price(amount_kopeks)}\n'
- f'💳 Способ: {payment_method}\n\n'
- 'Средства зачислены на ваш баланс!\n\n'
- '⚠️ Важно: Пополнение баланса не активирует подписку автоматически. '
- 'Обязательно активируйте подписку отдельно!\n\n'
- f'🔄 При наличии сохранённой корзины подписки и включенной автопокупке, '
- f'подписка будет приобретена автоматически после пополнения баланса.'
- )
+ # Стандартное сообщение с полной клавиатурой
+ keyboard = await self.build_topup_success_keyboard(user_snapshot)
+ message = (
+ '✅ Платеж успешно завершен!\n\n'
+ f'💰 Сумма: {settings.format_price(amount_kopeks)}\n'
+ f'💳 Способ: {payment_method}\n\n'
+ 'Средства зачислены на ваш баланс!\n\n'
+ '⚠️ Важно: Пополнение баланса не активирует подписку автоматически. '
+ 'Обязательно активируйте подписку отдельно!\n\n'
+ f'🔄 При наличии сохранённой корзины подписки и включенной автопокупке, '
+ f'подписка будет приобретена автоматически после пополнения баланса.'
+ )
await self.bot.send_message(
chat_id=telegram_id,
diff --git a/app/services/payment/cryptobot.py b/app/services/payment/cryptobot.py
index 93975cf7..129d5729 100644
--- a/app/services/payment/cryptobot.py
+++ b/app/services/payment/cryptobot.py
@@ -14,7 +14,6 @@ from app.config import settings
from app.database.database import AsyncSessionLocal
from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import (
- auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.services.subscription_renewal_service import (
@@ -361,26 +360,7 @@ class CryptoBotPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
- # Умная автоактивация если автопокупка не сработала
- activation_notification_sent = False
- if not auto_purchase_success:
- try:
- _, activation_notification_sent = await auto_activate_subscription_after_topup(
- db,
- user,
- bot=bot_instance,
- topup_amount=amount_kopeks,
- )
- except Exception as auto_activate_error:
- logger.error(
- 'Ошибка умной автоактивации для пользователя %s: %s',
- user.id,
- auto_activate_error,
- exc_info=True,
- )
-
- # Отправляем уведомление только если его ещё не отправили
- if has_saved_cart and bot_instance and not activation_notification_sent:
+ if has_saved_cart and bot_instance:
from app.localization.texts import get_texts
texts = get_texts(user.language)
diff --git a/app/services/payment/freekassa.py b/app/services/payment/freekassa.py
index 4533cce7..1d64c936 100644
--- a/app/services/payment/freekassa.py
+++ b/app/services/payment/freekassa.py
@@ -14,7 +14,6 @@ from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.freekassa_service import freekassa_service
from app.services.subscription_auto_purchase_service import (
- auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -388,23 +387,7 @@ class FreekassaPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
- # Умная автоактивация если автопокупка не сработала
- activation_notification_sent = False
- if not auto_purchase_success:
- try:
- _, activation_notification_sent = await auto_activate_subscription_after_topup(
- db, user, bot=getattr(self, 'bot', None), topup_amount=payment.amount_kopeks
- )
- except Exception as auto_activate_error:
- logger.error(
- 'Ошибка умной автоактивации для пользователя %s: %s',
- user.id,
- auto_activate_error,
- exc_info=True,
- )
-
- # Отправляем уведомление только если его ещё не отправили
- if has_saved_cart and getattr(self, 'bot', None) and not activation_notification_sent and user.telegram_id:
+ if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
from app.localization.texts import get_texts
texts = get_texts(user.language)
diff --git a/app/services/payment/heleket.py b/app/services/payment/heleket.py
index 310e75f2..6b7f39c3 100644
--- a/app/services/payment/heleket.py
+++ b/app/services/payment/heleket.py
@@ -13,7 +13,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import (
- auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -452,22 +451,6 @@ class HeleketPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
- # Умная автоактивация если автопокупка не сработала
- if not auto_purchase_success:
- try:
- await auto_activate_subscription_after_topup(
- db,
- user,
- bot=getattr(self, 'bot', None),
- topup_amount=amount_kopeks,
- )
- except Exception as auto_activate_error:
- logger.error(
- 'Ошибка умной автоактивации для пользователя %s: %s',
- user.id,
- auto_activate_error,
- exc_info=True,
- )
except Exception as error:
logger.error(
'Ошибка при работе с автоактивацией для пользователя %s: %s',
diff --git a/app/services/payment/kassa_ai.py b/app/services/payment/kassa_ai.py
index 1cc3818d..09d7d449 100644
--- a/app/services/payment/kassa_ai.py
+++ b/app/services/payment/kassa_ai.py
@@ -14,7 +14,6 @@ from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.kassa_ai_service import kassa_ai_service
from app.services.subscription_auto_purchase_service import (
- auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -339,34 +338,14 @@ class KassaAiPaymentMixin:
try:
display_name = settings.get_kassa_ai_display_name()
- if settings.SHOW_ACTIVATION_PROMPT_AFTER_TOPUP:
- # Яркое сообщение для тупых
- from aiogram import types
-
- message = (
- '✅ Платеж успешно завершен!\n\n'
- f'💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n'
- f'💳 Способ: {display_name}\n\n'
- '💎 Средства зачислены на ваш баланс!\n\n'
- '‼️ ВНИМАНИЕ! ОБЯЗАТЕЛЬНО АКТИВИРУЙТЕ ПОДПИСКУ! ‼️\n\n'
- '⚠️ Пополнение баланса НЕ АКТИВИРУЕТ подписку автоматически!\n\n'
- '👇 НАЖМИТЕ КНОПКУ НИЖЕ ДЛЯ АКТИВАЦИИ 👇'
- )
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [types.InlineKeyboardButton(text='🔥 АКТИВИРОВАТЬ ПОДПИСКУ', callback_data='menu_buy')],
- ]
- )
- else:
- # Стандартное сообщение (как было раньше)
- keyboard = await self.build_topup_success_keyboard(user)
- message = (
- '✅ Пополнение успешно!\n\n'
- f'💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n'
- f'💳 Способ: {display_name}\n'
- f'🆔 Транзакция: {transaction.id}\n\n'
- 'Баланс пополнен автоматически!'
- )
+ keyboard = await self.build_topup_success_keyboard(user)
+ message = (
+ '✅ Пополнение успешно!\n\n'
+ f'💰 Сумма: {settings.format_price(payment.amount_kopeks)}\n'
+ f'💳 Способ: {display_name}\n'
+ f'🆔 Транзакция: {transaction.id}\n\n'
+ 'Баланс пополнен автоматически!'
+ )
await self.bot.send_message(
user.telegram_id,
@@ -404,23 +383,7 @@ class KassaAiPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
- # Умная автоактивация если автопокупка не сработала
- activation_notification_sent = False
- if not auto_purchase_success:
- try:
- _, activation_notification_sent = await auto_activate_subscription_after_topup(
- db, user, bot=getattr(self, 'bot', None), topup_amount=payment.amount_kopeks
- )
- except Exception as auto_activate_error:
- logger.error(
- 'Ошибка умной автоактивации для пользователя %s: %s',
- user.id,
- auto_activate_error,
- exc_info=True,
- )
-
- # Отправляем уведомление только если его ещё не отправили
- if has_saved_cart and getattr(self, 'bot', None) and not activation_notification_sent and user.telegram_id:
+ if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
from app.localization.texts import get_texts
texts = get_texts(user.language)
diff --git a/app/services/payment/mulenpay.py b/app/services/payment/mulenpay.py
index f41befcf..bfa2e577 100644
--- a/app/services/payment/mulenpay.py
+++ b/app/services/payment/mulenpay.py
@@ -11,7 +11,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import (
- auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -390,28 +389,7 @@ class MulenPayPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
- # Умная автоактивация если автопокупка не сработала
- activation_notification_sent = False
- if not auto_purchase_success:
- try:
- _, activation_notification_sent = await auto_activate_subscription_after_topup(
- db, user, bot=getattr(self, 'bot', None), topup_amount=payment.amount_kopeks
- )
- except Exception as auto_activate_error:
- logger.error(
- 'Ошибка умной автоактивации для пользователя %s: %s',
- user.id,
- auto_activate_error,
- exc_info=True,
- )
-
- # Отправляем уведомление только если его ещё не отправили и есть telegram_id
- if (
- has_saved_cart
- and getattr(self, 'bot', None)
- and not activation_notification_sent
- and user.telegram_id
- ):
+ if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
# Если у пользователя есть сохраненная корзина,
# отправляем ему уведомление с кнопкой вернуться к оформлению
from app.localization.texts import get_texts
diff --git a/app/services/payment/pal24.py b/app/services/payment/pal24.py
index 9834bb8a..8725de65 100644
--- a/app/services/payment/pal24.py
+++ b/app/services/payment/pal24.py
@@ -13,7 +13,6 @@ from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.pal24_service import Pal24APIError
from app.services.subscription_auto_purchase_service import (
- auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -489,23 +488,7 @@ class Pal24PaymentMixin:
if auto_purchase_success:
has_saved_cart = False
- # Умная автоактивация если автопокупка не сработала
- activation_notification_sent = False
- if not auto_purchase_success:
- try:
- _, activation_notification_sent = await auto_activate_subscription_after_topup(
- db, user, bot=getattr(self, 'bot', None), topup_amount=payment.amount_kopeks
- )
- except Exception as auto_activate_error:
- logger.error(
- 'Ошибка умной автоактивации для пользователя %s: %s',
- user.id,
- auto_activate_error,
- exc_info=True,
- )
-
- # Отправляем уведомление только если его ещё не отправили
- if has_saved_cart and getattr(self, 'bot', None) and not activation_notification_sent and user.telegram_id:
+ if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
from app.localization.texts import get_texts
texts = get_texts(user.language)
diff --git a/app/services/payment/platega.py b/app/services/payment/platega.py
index e83bd31b..0675fb29 100644
--- a/app/services/payment/platega.py
+++ b/app/services/payment/platega.py
@@ -13,7 +13,6 @@ from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.platega_service import PlategaService
from app.services.subscription_auto_purchase_service import (
- auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -470,23 +469,7 @@ class PlategaPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
- # Умная автоактивация если автопокупка не сработала
- activation_notification_sent = False
- if not auto_purchase_success:
- try:
- _, activation_notification_sent = await auto_activate_subscription_after_topup(
- db, user, bot=getattr(self, 'bot', None), topup_amount=payment.amount_kopeks
- )
- except Exception as auto_activate_error:
- logger.error(
- 'Ошибка умной автоактивации для пользователя %s: %s',
- user.id,
- auto_activate_error,
- exc_info=True,
- )
-
- # Отправляем уведомление только если его ещё не отправили
- if has_saved_cart and getattr(self, 'bot', None) and not activation_notification_sent and user.telegram_id:
+ if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
from app.localization.texts import get_texts
texts = get_texts(user.language)
diff --git a/app/services/payment/stars.py b/app/services/payment/stars.py
index e68e02c5..c4630e39 100644
--- a/app/services/payment/stars.py
+++ b/app/services/payment/stars.py
@@ -19,7 +19,6 @@ from app.database.crud.user import get_user_by_id
from app.database.models import PaymentMethod, TransactionType
from app.external.telegram_stars import TelegramStarsService
from app.services.subscription_auto_purchase_service import (
- auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -534,26 +533,7 @@ class TelegramStarsMixin:
if auto_purchase_success:
has_saved_cart = False
- # Умная автоактивация если автопокупка не сработала
- activation_notification_sent = False
- if not auto_purchase_success:
- try:
- _, activation_notification_sent = await auto_activate_subscription_after_topup(
- db,
- user,
- bot=getattr(self, 'bot', None),
- topup_amount=amount_kopeks,
- )
- except Exception as auto_activate_error:
- logger.error(
- 'Ошибка умной автоактивации для пользователя %s: %s',
- user.id,
- auto_activate_error,
- exc_info=True,
- )
-
- # Отправляем уведомление только если его ещё не отправили
- if has_saved_cart and getattr(self, 'bot', None) and not activation_notification_sent and user.telegram_id:
+ if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
texts = get_texts(user.language)
cart_message = texts.t(
'BALANCE_TOPUP_CART_REMINDER_DETAILED',
diff --git a/app/services/payment/wata.py b/app/services/payment/wata.py
index 3f9326af..6402a145 100644
--- a/app/services/payment/wata.py
+++ b/app/services/payment/wata.py
@@ -12,7 +12,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import (
- auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.services.wata_service import WataAPIError, WataService
@@ -575,23 +574,7 @@ class WataPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
- # Умная автоактивация если автопокупка не сработала
- activation_notification_sent = False
- if not auto_purchase_success:
- try:
- _, activation_notification_sent = await auto_activate_subscription_after_topup(
- db, user, bot=getattr(self, 'bot', None), topup_amount=payment.amount_kopeks
- )
- except Exception as auto_activate_error:
- logger.error(
- 'Ошибка умной автоактивации для пользователя %s: %s',
- user.id,
- auto_activate_error,
- exc_info=True,
- )
-
- # Отправляем уведомление только если его ещё не отправили
- if has_saved_cart and getattr(self, 'bot', None) and not activation_notification_sent and user.telegram_id:
+ if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
from app.localization.texts import get_texts
texts = get_texts(user.language)
diff --git a/app/services/payment/yookassa.py b/app/services/payment/yookassa.py
index 254ea8ed..b96ed6c4 100644
--- a/app/services/payment/yookassa.py
+++ b/app/services/payment/yookassa.py
@@ -16,7 +16,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import PaymentMethod, TransactionType
from app.services.subscription_auto_purchase_service import (
- auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.payment_logger import payment_logger as logger
@@ -847,78 +846,59 @@ class YooKassaPaymentMixin:
if auto_purchase_success:
has_saved_cart = False
- # Умная автоактивация если автопокупка не сработала
- if not auto_purchase_success:
- try:
- await auto_activate_subscription_after_topup(
- db,
- user,
- bot=getattr(self, 'bot', None),
- )
- except Exception as auto_activate_error:
- logger.error(
- 'Ошибка умной автоактивации для пользователя %s: %s',
- user.id,
- auto_activate_error,
- exc_info=True,
- )
+ if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
+ # Если у пользователя есть сохраненная корзина,
+ # отправляем ему уведомление с кнопкой вернуться к оформлению
+ from aiogram import types
- # Если включен яркий промпт активации, пропускаем старое уведомление
- # т.к. оно будет отправлено через _send_payment_success_notification
- if not settings.SHOW_ACTIVATION_PROMPT_AFTER_TOPUP:
- if has_saved_cart and getattr(self, 'bot', None) and user.telegram_id:
- # Если у пользователя есть сохраненная корзина,
- # отправляем ему уведомление с кнопкой вернуться к оформлению
- from aiogram import types
+ from app.localization.texts import get_texts
- from app.localization.texts import get_texts
+ texts = get_texts(user.language)
+ cart_message = texts.BALANCE_TOPUP_CART_REMINDER_DETAILED.format(
+ total_amount=settings.format_price(payment.amount_kopeks)
+ )
- texts = get_texts(user.language)
- cart_message = texts.BALANCE_TOPUP_CART_REMINDER_DETAILED.format(
- total_amount=settings.format_price(payment.amount_kopeks)
- )
+ # Создаем клавиатуру с кнопками
+ keyboard = types.InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ types.InlineKeyboardButton(
+ text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
+ callback_data='return_to_saved_cart',
+ )
+ ],
+ [
+ types.InlineKeyboardButton(
+ text='💰 Мой баланс',
+ callback_data='menu_balance',
+ )
+ ],
+ [
+ types.InlineKeyboardButton(
+ text='🏠 Главное меню',
+ callback_data='back_to_menu',
+ )
+ ],
+ ]
+ )
- # Создаем клавиатуру с кнопками
- keyboard = types.InlineKeyboardMarkup(
- inline_keyboard=[
- [
- types.InlineKeyboardButton(
- text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
- callback_data='return_to_saved_cart',
- )
- ],
- [
- types.InlineKeyboardButton(
- text='💰 Мой баланс',
- callback_data='menu_balance',
- )
- ],
- [
- types.InlineKeyboardButton(
- text='🏠 Главное меню',
- callback_data='back_to_menu',
- )
- ],
- ]
- )
-
- await self.bot.send_message(
- chat_id=user.telegram_id,
- text=f'✅ Баланс пополнен на {settings.format_price(payment.amount_kopeks)}!\n\n'
- f'⚠️ Важно: Пополнение баланса не активирует подписку автоматически. '
- f'Обязательно активируйте подписку отдельно!\n\n'
- f'🔄 При наличии сохранённой корзины подписки и включенной автопокупке, '
- f'подписка будет приобретена автоматически после пополнения баланса.\n\n{cart_message}',
- reply_markup=keyboard,
- )
- logger.info(
- f'Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю {user.id}'
- )
- else:
- logger.info(
- 'У пользователя %s нет сохраненной корзины, бот недоступен или покупка уже выполнена',
- user.id,
- )
+ await self.bot.send_message(
+ chat_id=user.telegram_id,
+ text=f'✅ Баланс пополнен на {settings.format_price(payment.amount_kopeks)}!\n\n'
+ f'⚠️ Важно: Пополнение баланса не активирует подписку автоматически. '
+ f'Обязательно активируйте подписку отдельно!\n\n'
+ f'🔄 При наличии сохранённой корзины подписки и включенной автопокупке, '
+ f'подписка будет приобретена автоматически после пополнения баланса.\n\n{cart_message}',
+ reply_markup=keyboard,
+ )
+ logger.info(
+ f'Отправлено уведомление с кнопкой возврата к оформлению подписки пользователю {user.id}'
+ )
+ else:
+ logger.info(
+ 'У пользователя %s нет сохраненной корзины, бот недоступен или покупка уже выполнена',
+ user.id,
+ )
except Exception as e:
logger.error(
f'Критическая ошибка при работе с сохраненной корзиной для пользователя {user.id}: {e}',
diff --git a/app/services/subscription_auto_purchase_service.py b/app/services/subscription_auto_purchase_service.py
index b03a69d2..9808cf0c 100644
--- a/app/services/subscription_auto_purchase_service.py
+++ b/app/services/subscription_auto_purchase_service.py
@@ -1814,340 +1814,4 @@ async def auto_purchase_saved_cart_after_topup(
return True
-async def auto_activate_subscription_after_topup(
- db: AsyncSession,
- user: User,
- *,
- bot: Bot | None = None,
- topup_amount: int | None = None,
-) -> tuple[bool, bool]:
- """
- Умная автоактивация после пополнения баланса.
-
- Работает БЕЗ сохранённой корзины:
- - Если подписка активна — ничего не делает
- - Если подписка истекла — продлевает с теми же параметрами
- - Если подписки нет — создаёт новую с дефолтными параметрами
-
- Выбирает максимальный период, который можно оплатить из баланса.
-
- Args:
- topup_amount: Сумма пополнения в копейках (для отображения в уведомлении)
-
- Returns:
- tuple[bool, bool]: (success, notification_sent)
- - success: True если подписка активирована
- - notification_sent: True если уведомление отправлено пользователю
- """
- from datetime import datetime
-
- # Lazy imports to avoid circular dependency
- from app.cabinet.routes.websocket import (
- notify_user_subscription_activated,
- notify_user_subscription_renewed,
- )
- from app.database.crud.server_squad import get_available_server_squads, get_server_ids_by_uuids
- from app.database.crud.subscription import create_paid_subscription, get_subscription_by_user_id
- from app.database.crud.transaction import create_transaction
- from app.database.crud.user import subtract_user_balance
- from app.database.models import PaymentMethod, TransactionType
- from app.services.admin_notification_service import AdminNotificationService
- from app.services.subscription_renewal_service import SubscriptionRenewalService
- from app.services.subscription_service import SubscriptionService
-
- if not user or not getattr(user, 'id', None):
- return (False, False)
-
- subscription = await get_subscription_by_user_id(db, user.id)
-
- # Если автоактивация отключена - уведомление отправится из _send_payment_success_notification
- if not settings.is_auto_activate_after_topup_enabled():
- logger.info(
- '⚠️ Автоактивация отключена для пользователя %s, уведомление будет отправлено из payment service',
- _format_user_id(user),
- )
- return (False, False)
-
- # Если подписка активна — ничего не делаем (автоактивация включена, но подписка уже есть)
- if subscription and subscription.status == 'ACTIVE' and subscription.end_date > datetime.utcnow():
- logger.info(
- '🔁 Автоактивация: у пользователя %s уже активная подписка, пропускаем',
- _format_user_id(user),
- )
- return (False, False)
-
- # Определяем параметры подписки
- if subscription:
- device_limit = subscription.device_limit or settings.DEFAULT_DEVICE_LIMIT
- # В режиме fixed_with_topup при автоактивации используем фиксированный лимит
- if settings.is_traffic_fixed():
- traffic_limit_gb = settings.get_fixed_traffic_limit()
- else:
- traffic_limit_gb = subscription.traffic_limit_gb or 0
- connected_squads = subscription.connected_squads or []
- else:
- device_limit = settings.DEFAULT_DEVICE_LIMIT
- # В режиме fixed_with_topup при автоактивации используем фиксированный лимит
- if settings.is_traffic_fixed():
- traffic_limit_gb = settings.get_fixed_traffic_limit()
- else:
- traffic_limit_gb = 0
- connected_squads = []
-
- # Если серверы не выбраны — берём бесплатные по умолчанию
- if not connected_squads:
- available_servers = await get_available_server_squads(db, promo_group_id=user.promo_group_id)
- connected_squads = [s.squad_uuid for s in available_servers if s.is_available and s.price_kopeks == 0]
- if not connected_squads and available_servers:
- connected_squads = [available_servers[0].squad_uuid]
-
- server_ids = await get_server_ids_by_uuids(db, connected_squads) if connected_squads else []
-
- balance = user.balance_kopeks
- available_periods = sorted(settings.get_available_subscription_periods(), reverse=True)
-
- if not available_periods:
- logger.warning('🔁 Автоактивация: нет доступных периодов подписки')
- return (False, False)
-
- subscription_service = SubscriptionService()
-
- # Найти максимальный период <= баланса
- best_period = None
- best_price = 0
-
- for period in available_periods:
- try:
- price, _ = await subscription_service.calculate_subscription_price_with_months(
- period, traffic_limit_gb, server_ids, device_limit, db, user=user
- )
- if price <= balance:
- best_period = period
- best_price = price
- break
- except Exception as calc_error:
- logger.warning(
- '🔁 Автоактивация: ошибка расчёта цены для периода %s: %s',
- period,
- calc_error,
- )
- continue
-
- if not best_period:
- logger.info(
- '🔁 Автоактивация: у пользователя %s недостаточно средств (%s) для любого периода',
- _format_user_id(user),
- balance,
- )
- # Уведомление отправится из _send_payment_success_notification
- logger.info(
- '⚠️ Недостаточно средств для автоактивации пользователя %s, уведомление будет отправлено из payment service',
- _format_user_id(user),
- )
- return (False, False)
-
- texts = get_texts(getattr(user, 'language', 'ru'))
-
- try:
- if subscription:
- # Продление существующей подписки
- renewal_service = SubscriptionRenewalService()
- pricing = await renewal_service.calculate_pricing(db, user, subscription, best_period)
-
- result = await renewal_service.finalize(
- db,
- user,
- subscription,
- pricing,
- description=f'Автоматическое продление на {best_period} дней',
- payment_method=PaymentMethod.BALANCE,
- )
-
- logger.info(
- '✅ Автоактивация: подписка пользователя %s продлена на %s дней за %s коп.',
- _format_user_id(user),
- best_period,
- best_price,
- )
-
- # Send WebSocket notification to cabinet frontend
- try:
- await notify_user_subscription_renewed(
- user_id=user.id,
- new_expires_at=result.subscription.end_date.isoformat() if result.subscription.end_date else '',
- amount_kopeks=best_price,
- )
- except Exception as ws_error:
- logger.warning(
- '⚠️ Автоактивация: не удалось отправить WS уведомление о продлении для %s: %s',
- _format_user_id(user),
- ws_error,
- )
-
- # Уведомление пользователю (только для Telegram-пользователей)
- if bot and user.telegram_id:
- try:
- period_label = format_period_description(best_period, getattr(user, 'language', 'ru'))
- new_end_date = result.subscription.end_date
- end_date_str = new_end_date.strftime('%d.%m.%Y') if new_end_date else '—'
-
- message = texts.t(
- 'AUTO_PURCHASE_SUBSCRIPTION_EXTENDED',
- '✅ Подписка автоматически продлена на {period}.',
- ).format(period=period_label)
-
- details = texts.t(
- 'AUTO_PURCHASE_SUBSCRIPTION_EXTENDED_DETAILS',
- '⏰ Новая дата окончания: {date}.',
- ).format(date=end_date_str)
-
- hint = texts.t(
- 'AUTO_PURCHASE_SUBSCRIPTION_HINT',
- 'Перейдите в раздел «Моя подписка», чтобы получить ссылку.',
- )
-
- keyboard = InlineKeyboardMarkup(
- inline_keyboard=[
- [
- InlineKeyboardButton(
- text=texts.t('MY_SUBSCRIPTION_BUTTON', '📱 Моя подписка'),
- callback_data='menu_subscription',
- )
- ],
- ]
- )
-
- await bot.send_message(
- chat_id=user.telegram_id,
- text=f'{message}\n{details}\n\n{hint}',
- reply_markup=keyboard,
- parse_mode='HTML',
- )
- except Exception as notify_error:
- logger.warning(
- '⚠️ Автоактивация: не удалось уведомить пользователя %s: %s',
- user.telegram_id or user.id,
- notify_error,
- )
-
- else:
- # Создание новой подписки
- new_subscription = await create_paid_subscription(
- db,
- user.id,
- best_period,
- traffic_limit_gb=traffic_limit_gb,
- device_limit=device_limit,
- connected_squads=connected_squads,
- update_server_counters=True,
- )
-
- await subtract_user_balance(db, user, best_price, f'Активация подписки на {best_period} дней')
-
- await subscription_service.create_remnawave_user(db, new_subscription)
-
- await create_transaction(
- db=db,
- user_id=user.id,
- type=TransactionType.SUBSCRIPTION_PAYMENT,
- amount_kopeks=best_price,
- description=f'Активация подписки на {best_period} дней',
- payment_method=PaymentMethod.BALANCE,
- )
-
- logger.info(
- '✅ Автоактивация: новая подписка на %s дней создана для пользователя %s за %s коп.',
- best_period,
- _format_user_id(user),
- best_price,
- )
-
- # Send WebSocket notification to cabinet frontend
- try:
- await notify_user_subscription_activated(
- user_id=user.id,
- expires_at=new_subscription.end_date.isoformat() if new_subscription.end_date else '',
- tariff_name='',
- )
- except Exception as ws_error:
- logger.warning(
- '⚠️ Автоактивация: не удалось отправить WS уведомление об активации для %s: %s',
- _format_user_id(user),
- ws_error,
- )
-
- # Уведомление пользователю (только для Telegram-пользователей)
- if bot and user.telegram_id:
- try:
- period_label = format_period_description(best_period, getattr(user, 'language', 'ru'))
-
- message = texts.t(
- 'AUTO_PURCHASE_SUBSCRIPTION_SUCCESS',
- '✅ Подписка на {period} автоматически оформлена после пополнения баланса.',
- ).format(period=period_label)
-
- hint = texts.t(
- 'AUTO_PURCHASE_SUBSCRIPTION_HINT',
- 'Перейдите в раздел «Моя подписка», чтобы получить ссылку.',
- )
-
- keyboard = InlineKeyboardMarkup(
- inline_keyboard=[
- [
- InlineKeyboardButton(
- text=texts.t('MY_SUBSCRIPTION_BUTTON', '📱 Моя подписка'),
- callback_data='menu_subscription',
- )
- ],
- ]
- )
-
- await bot.send_message(
- chat_id=user.telegram_id,
- text=f'{message}\n\n{hint}',
- reply_markup=keyboard,
- parse_mode='HTML',
- )
-
- except Exception as notify_error:
- logger.warning(
- '⚠️ Автоактивация: не удалось уведомить пользователя %s: %s',
- user.telegram_id or user.id,
- notify_error,
- )
-
- # Уведомление админам (независимо от telegram_id)
- if bot:
- try:
- notification_service = AdminNotificationService(bot)
- await notification_service.send_subscription_purchase_notification(
- db,
- user,
- new_subscription,
- None, # transaction
- best_period,
- False, # was_trial_conversion
- )
- except Exception as admin_error:
- logger.warning(
- '⚠️ Автоактивация: не удалось уведомить админов: %s',
- admin_error,
- )
-
- return (True, True) # success=True, notification_sent=True (об активации)
-
- except Exception as e:
- logger.error(
- '❌ Автоактивация: ошибка для пользователя %s: %s',
- _format_user_id(user),
- e,
- exc_info=True,
- )
- try:
- await db.rollback()
- except Exception:
- pass
- return (False, False)
-
-
-__all__ = ['auto_activate_subscription_after_topup', 'auto_purchase_saved_cart_after_topup']
+__all__ = ['auto_purchase_saved_cart_after_topup']
diff --git a/app/services/system_settings_service.py b/app/services/system_settings_service.py
index 28c4d307..717bcf34 100644
--- a/app/services/system_settings_service.py
+++ b/app/services/system_settings_service.py
@@ -260,7 +260,6 @@ class BotConfigurationService:
'PAYMENT_BALANCE_TEMPLATE': 'PAYMENT',
'PAYMENT_SUBSCRIPTION_TEMPLATE': 'PAYMENT',
'AUTO_PURCHASE_AFTER_TOPUP_ENABLED': 'PAYMENT',
- 'SHOW_ACTIVATION_PROMPT_AFTER_TOPUP': 'PAYMENT',
'SIMPLE_SUBSCRIPTION_ENABLED': 'SIMPLE_SUBSCRIPTION',
'SIMPLE_SUBSCRIPTION_PERIOD_DAYS': 'SIMPLE_SUBSCRIPTION',
'SIMPLE_SUBSCRIPTION_DEVICE_LIMIT': 'SIMPLE_SUBSCRIPTION',
@@ -585,19 +584,6 @@ class BotConfigurationService:
'example': 'true',
'warning': ('Используйте с осторожностью: средства будут списаны мгновенно, если корзина найдена.'),
},
- 'SHOW_ACTIVATION_PROMPT_AFTER_TOPUP': {
- 'description': (
- 'Включает режим яркого промпта активации подписки после пополнения баланса. '
- 'Вместо обычного уведомления пользователь получит яркое сообщение с восклицательными знаками '
- 'и кнопками для активации/продления подписки или изменения количества устройств.'
- ),
- 'format': 'Булево значение.',
- 'example': 'true',
- 'warning': (
- 'При включении пользователи будут получать только яркое уведомление без кнопок баланса и главного меню. '
- 'Эти кнопки появятся после выполнения действия (активация/продление/изменение устройств).'
- ),
- },
'SUPPORT_TICKET_SLA_MINUTES': {
'description': 'Лимит времени для ответа модераторов на тикет в минутах.',
'format': 'Целое число от 1 до 1440.',
diff --git a/app/services/tribute_service.py b/app/services/tribute_service.py
index 17e5a6c5..c45508dc 100644
--- a/app/services/tribute_service.py
+++ b/app/services/tribute_service.py
@@ -14,7 +14,6 @@ from app.database.models import PaymentMethod, TransactionType
from app.external.tribute import TributeService as TributeAPI
from app.services.payment_service import PaymentService
from app.services.subscription_auto_purchase_service import (
- auto_activate_subscription_after_topup,
auto_purchase_saved_cart_after_topup,
)
from app.utils.user_utils import format_referrer_info
@@ -307,23 +306,8 @@ class TributeService:
if auto_purchase_success:
has_saved_cart = False
- # Умная автоактивация если автопокупка не сработала
- activation_notification_sent = False
- if not auto_purchase_success:
- try:
- _, activation_notification_sent = await auto_activate_subscription_after_topup(
- session, user, bot=self.bot, topup_amount=amount_kopeks
- )
- except Exception as auto_activate_error:
- logger.error(
- 'Ошибка умной автоактивации для пользователя %s: %s',
- user.id,
- auto_activate_error,
- exc_info=True,
- )
-
- # Отправляем уведомление только если его ещё не отправили и есть telegram_id
- if has_saved_cart and self.bot and not activation_notification_sent and user_id:
+ # Отправляем уведомление только если есть сохранённая корзина и telegram_id
+ if has_saved_cart and self.bot and user_id:
# Если у пользователя есть сохраненная корзина,
# отправляем ему уведомление с кнопкой вернуться к оформлению
from aiogram import types
diff --git a/app/utils/photo_message.py b/app/utils/photo_message.py
index e7a5efc3..7ca4a1a6 100644
--- a/app/utils/photo_message.py
+++ b/app/utils/photo_message.py
@@ -2,7 +2,7 @@ import asyncio
import logging
from aiogram import types
-from aiogram.exceptions import TelegramBadRequest, TelegramNetworkError
+from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramNetworkError
from aiogram.types import InaccessibleMessage, InputMediaPhoto
from app.config import settings
@@ -126,6 +126,8 @@ async def edit_or_answer_photo(
reply_markup=keyboard,
parse_mode=resolved_parse_mode,
)
+ except TelegramForbiddenError:
+ logger.debug('Пользователь заблокировал бота, пропускаем')
except TelegramBadRequest as error:
try:
await callback.message.delete()
@@ -140,6 +142,8 @@ async def edit_or_answer_photo(
if callback.message.photo:
await callback.message.delete()
await _answer_text(callback, caption, keyboard, resolved_parse_mode)
+ except TelegramForbiddenError:
+ logger.debug('Пользователь заблокировал бота, пропускаем')
except TelegramBadRequest as error:
await _answer_text(callback, caption, keyboard, resolved_parse_mode, error)
return
@@ -167,6 +171,10 @@ async def edit_or_answer_photo(
pass
await _answer_text(callback, caption, keyboard, resolved_parse_mode)
return
+ except TelegramForbiddenError:
+ # Пользователь заблокировал бота — молча игнорируем
+ logger.debug('Пользователь заблокировал бота, пропускаем edit_media')
+ return
except TelegramBadRequest as error:
if is_privacy_restricted_error(error):
try:
@@ -189,7 +197,7 @@ async def edit_or_answer_photo(
parse_mode=resolved_parse_mode,
)
_cache_logo_file_id(result)
- except TelegramBadRequest as photo_error:
+ except (TelegramBadRequest, TelegramForbiddenError) as photo_error:
await _answer_text(callback, caption, keyboard, resolved_parse_mode, photo_error)
except Exception:
# Последний фоллбек — обычный текст
diff --git a/tests/services/test_kassa_ai_notifications.py b/tests/services/test_kassa_ai_notifications.py
index 3912a2e1..ea720655 100644
--- a/tests/services/test_kassa_ai_notifications.py
+++ b/tests/services/test_kassa_ai_notifications.py
@@ -5,71 +5,6 @@
from unittest.mock import MagicMock
-def test_notification_message_bright_prompt():
- """
- Тест: проверяем что формируется ЯРКОЕ сообщение с SHOW_ACTIVATION_PROMPT_AFTER_TOPUP=true.
- """
-
- # Эмулируем код из kassa_ai.py
- SHOW_ACTIVATION_PROMPT_AFTER_TOPUP = True
- display_name = 'Kassa AI'
- amount_formatted = '10₽'
-
- if SHOW_ACTIVATION_PROMPT_AFTER_TOPUP:
- message = (
- '✅ Платеж успешно завершен!\n\n'
- f'💰 Сумма: {amount_formatted}\n'
- f'💳 Способ: {display_name}\n\n'
- '💎 Средства зачислены на ваш баланс!\n\n'
- '‼️ ВНИМАНИЕ! ОБЯЗАТЕЛЬНО АКТИВИРУЙТЕ ПОДПИСКУ! ‼️\n\n'
- '⚠️ Пополнение баланса НЕ АКТИВИРУЕТ подписку автоматически!\n\n'
- '👇 НАЖМИТЕ КНОПКУ НИЖЕ ДЛЯ АКТИВАЦИИ 👇'
- )
- else:
- message = ''
-
- # Проверки
- assert '‼️' in message
- assert 'ВНИМАНИЕ' in message
- assert 'ОБЯЗАТЕЛЬНО АКТИВИРУЙТЕ ПОДПИСКУ' in message
- assert '👇' in message
- assert display_name in message
- assert amount_formatted in message
- print(f'\n✅ ЯРКОЕ сообщение сформировано правильно:\n{message}')
-
-
-def test_notification_message_standard():
- """
- Тест: проверяем что формируется обычное сообщение с SHOW_ACTIVATION_PROMPT_AFTER_TOPUP=false.
- """
- # Эмулируем код из kassa_ai.py
- SHOW_ACTIVATION_PROMPT_AFTER_TOPUP = False
- display_name = 'Kassa AI'
- amount_formatted = '10₽'
-
- if SHOW_ACTIVATION_PROMPT_AFTER_TOPUP:
- message = ''
- else:
- message = (
- '✅ Платеж успешно завершен!\n\n'
- f'💰 Сумма: {amount_formatted}\n'
- f'💳 Способ: {display_name}\n\n'
- 'Средства зачислены на ваш баланс!\n\n'
- '⚠️ Важно: Пополнение баланса не активирует подписку автоматически. '
- 'Обязательно активируйте подписку отдельно!\n\n'
- f'🔄 При наличии сохранённой корзины подписки и включенной автопокупке, '
- f'подписка будет приобретена автоматически после пополнения баланса.'
- )
-
- # Проверки
- assert '‼️' not in message
- assert 'ОБЯЗАТЕЛЬНО АКТИВИРУЙТЕ ПОДПИСКУ' not in message
- assert 'Платеж успешно завершен' in message
- assert display_name in message
- assert amount_formatted in message
- print(f'\n✅ Обычное сообщение сформировано правильно:\n{message}')
-
-
def test_telegram_id_saved_before_commit():
"""
Тест: проверяем что telegram_id сохраняется в локальную переменную ДО commit.
diff --git a/uv.lock b/uv.lock
index 6bacc96f..378d6208 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1149,7 +1149,7 @@ wheels = [
[[package]]
name = "remnawave-bedolaga-telegram-bot"
-version = "3.6.0"
+version = "3.8.0"
source = { virtual = "." }
dependencies = [
{ name = "aiogram" },