diff --git a/app/services/subscription_auto_purchase_service.py b/app/services/subscription_auto_purchase_service.py
index a3aac239..57ae9316 100644
--- a/app/services/subscription_auto_purchase_service.py
+++ b/app/services/subscription_auto_purchase_service.py
@@ -752,95 +752,13 @@ async def auto_activate_subscription_after_topup(
subscription = await get_subscription_by_user_id(db, user.id)
- # Если автоактивация отключена - только отправляем уведомление
+ # Если автоактивация отключена - уведомление отправится из _send_payment_success_notification
if not settings.is_auto_activate_after_topup_enabled():
- # Если включен яркий промпт активации, НЕ отправляем уведомление здесь
- # т.к. оно будет отправлено через _send_payment_success_notification
- if settings.SHOW_ACTIVATION_PROMPT_AFTER_TOPUP:
- logger.info(
- "⚠️ Пропущена отправка уведомления пользователю %s (SHOW_ACTIVATION_PROMPT_AFTER_TOPUP=true, уведомление будет из payment service)",
- user.telegram_id,
- )
- return (False, False)
-
- # Старая логика уведомлений для режима без яркого промпта
- notification_sent = False
- if bot:
- try:
- texts = get_texts(getattr(user, "language", "ru"))
- has_active_subscription = (
- subscription
- and subscription.status in ("active", "ACTIVE")
- )
-
- # Формируем строку с суммой пополнения
- topup_line = ""
- if topup_amount:
- topup_line = f"➕ Пополнено: {settings.format_price(topup_amount)}\n"
-
- # Определяем состояние подписки
- is_trial = subscription and getattr(subscription, 'is_trial', False)
-
- if has_active_subscription and not is_trial:
- # Активная платная подписка — 2 кнопки
- warning_message = (
- f"✅ Баланс пополнен!\n\n"
- f"{topup_line}"
- f"💳 Текущий баланс: {settings.format_price(user.balance_kopeks)}\n\n"
- f"👇 Выберите действие:"
- )
- keyboard = InlineKeyboardMarkup(
- inline_keyboard=[
- [InlineKeyboardButton(
- text="💎 Продлить подписку",
- callback_data="subscription_extend",
- )],
- [InlineKeyboardButton(
- text="📱 Изменить устройства",
- callback_data="subscription_change_devices",
- )],
- ]
- )
- else:
- # Триал или подписка закончилась — 1 кнопка
- warning_message = (
- f"✅ Баланс пополнен!\n\n"
- f"{topup_line}"
- f"💳 Текущий баланс: {settings.format_price(user.balance_kopeks)}\n\n"
- f"{'━' * 20}\n\n"
- f"🚨🚨🚨 ВНИМАНИЕ! 🚨🚨🚨\n\n"
- f"🔴 ПОДПИСКА НЕ АКТИВНА!\n\n"
- f"⚠️ Пополнение баланса НЕ активирует подписку автоматически!\n\n"
- f"👇 Обязательно оформите подписку:"
- )
- keyboard = InlineKeyboardMarkup(
- inline_keyboard=[
- [InlineKeyboardButton(
- text="🚀 КУПИТЬ ПОДПИСКУ",
- callback_data="menu_buy",
- )],
- ]
- )
-
- await bot.send_message(
- chat_id=user.telegram_id,
- text=warning_message,
- reply_markup=keyboard,
- parse_mode="HTML",
- )
- notification_sent = True
- logger.info(
- "⚠️ Отправлено уведомление о пополнении баланса пользователю %s (автоактивация выключена, подписка %s)",
- user.telegram_id,
- "активна" if has_active_subscription else "неактивна",
- )
- except Exception as notify_error:
- logger.warning(
- "⚠️ Не удалось отправить уведомление пользователю %s: %s",
- user.telegram_id,
- notify_error,
- )
- return (False, notification_sent)
+ logger.info(
+ "⚠️ Автоактивация отключена для пользователя %s, уведомление будет отправлено из payment service",
+ user.telegram_id,
+ )
+ return (False, False)
# Если подписка активна — ничего не делаем (автоактивация включена, но подписка уже есть)
if subscription and subscription.status == "ACTIVE" and subscription.end_date > datetime.utcnow():
@@ -921,93 +839,12 @@ async def auto_activate_subscription_after_topup(
user.telegram_id,
balance,
)
- # Если включен яркий промпт активации, НЕ отправляем уведомление здесь
- # т.к. оно будет отправлено через _send_payment_success_notification
- if settings.SHOW_ACTIVATION_PROMPT_AFTER_TOPUP:
- logger.info(
- "⚠️ Пропущена отправка уведомления пользователю %s (SHOW_ACTIVATION_PROMPT_AFTER_TOPUP=true, уведомление будет из payment service)",
- user.telegram_id,
- )
- return (False, False)
-
- # Старая логика уведомлений для режима без яркого промпта
- notification_sent = False
- if bot:
- try:
- texts = get_texts(getattr(user, "language", "ru"))
- has_active_subscription = (
- subscription
- and subscription.status in ("active", "ACTIVE")
- )
-
- # Формируем строку с суммой пополнения
- topup_line2 = ""
- if topup_amount:
- topup_line2 = f"➕ Пополнено: {settings.format_price(topup_amount)}\n"
-
- # Определяем состояние подписки
- is_trial2 = subscription and getattr(subscription, 'is_trial', False)
-
- if has_active_subscription and not is_trial2:
- # Активная платная подписка — 2 кнопки
- warning_message = (
- f"✅ Баланс пополнен!\n\n"
- f"{topup_line2}"
- f"💳 Текущий баланс: {settings.format_price(balance)}\n\n"
- f"👇 Выберите действие:"
- )
- keyboard = InlineKeyboardMarkup(
- inline_keyboard=[
- [InlineKeyboardButton(
- text="💎 Продлить подписку",
- callback_data="subscription_extend",
- )],
- [InlineKeyboardButton(
- text="📱 Изменить устройства",
- callback_data="subscription_change_devices",
- )],
- ]
- )
- else:
- # Триал или подписка закончилась — 1 кнопка
- warning_message = (
- f"✅ Баланс пополнен!\n\n"
- f"{topup_line2}"
- f"💳 Текущий баланс: {settings.format_price(balance)}\n\n"
- f"{'━' * 20}\n\n"
- f"🚨🚨🚨 ВНИМАНИЕ! 🚨🚨🚨\n\n"
- f"🔴 ПОДПИСКА НЕ АКТИВНА!\n\n"
- f"⚠️ Пополнение баланса НЕ активирует подписку автоматически!\n\n"
- f"👇 Обязательно оформите подписку:"
- )
- keyboard = InlineKeyboardMarkup(
- inline_keyboard=[
- [InlineKeyboardButton(
- text="🚀 КУПИТЬ ПОДПИСКУ",
- callback_data="menu_buy",
- )],
- ]
- )
-
- await bot.send_message(
- chat_id=user.telegram_id,
- text=warning_message,
- reply_markup=keyboard,
- parse_mode="HTML",
- )
- notification_sent = True
- logger.info(
- "⚠️ Отправлено уведомление о пополнении баланса пользователю %s (недостаточно средств, подписка %s)",
- user.telegram_id,
- "активна" if has_active_subscription else "неактивна",
- )
- except Exception as notify_error:
- logger.warning(
- "⚠️ Не удалось отправить уведомление пользователю %s: %s",
- user.telegram_id,
- notify_error,
- )
- return (False, notification_sent)
+ # Уведомление отправится из _send_payment_success_notification
+ logger.info(
+ "⚠️ Недостаточно средств для автоактивации пользователя %s, уведомление будет отправлено из payment service",
+ user.telegram_id,
+ )
+ return (False, False)
texts = get_texts(getattr(user, "language", "ru"))
diff --git a/app/services/system_settings_service.py b/app/services/system_settings_service.py
index 31d39da3..1f1187b0 100644
--- a/app/services/system_settings_service.py
+++ b/app/services/system_settings_service.py
@@ -259,6 +259,7 @@ 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",
@@ -271,6 +272,10 @@ class BotConfigurationService:
"NOTIFICATION_CACHE_HOURS": "NOTIFICATIONS",
"MONITORING_LOGS_RETENTION_DAYS": "MONITORING",
"MONITORING_INTERVAL": "MONITORING",
+ "TRAFFIC_MONITORING_ENABLED": "MONITORING",
+ "TRAFFIC_MONITORING_INTERVAL_HOURS": "MONITORING",
+ "TRAFFIC_MONITORED_NODES": "MONITORING",
+ "TRAFFIC_SNAPSHOT_TTL_HOURS": "MONITORING",
"ENABLE_LOGO_MODE": "INTERFACE_BRANDING",
"LOGO_FILE": "INTERFACE_BRANDING",
"HIDE_SUBSCRIPTION_LINK": "INTERFACE_SUBSCRIPTION",
@@ -570,6 +575,19 @@ class BotConfigurationService:
"Используйте с осторожностью: средства будут списаны мгновенно, если корзина найдена."
),
},
+ "SHOW_ACTIVATION_PROMPT_AFTER_TOPUP": {
+ "description": (
+ "Включает режим яркого промпта активации подписки после пополнения баланса. "
+ "Вместо обычного уведомления пользователь получит яркое сообщение с восклицательными знаками "
+ "и кнопками для активации/продления подписки или изменения количества устройств."
+ ),
+ "format": "Булево значение.",
+ "example": "true",
+ "warning": (
+ "При включении пользователи будут получать только яркое уведомление без кнопок баланса и главного меню. "
+ "Эти кнопки появятся после выполнения действия (активация/продление/изменение устройств)."
+ ),
+ },
"SUPPORT_TICKET_SLA_MINUTES": {
"description": "Лимит времени для ответа модераторов на тикет в минутах.",
"format": "Целое число от 1 до 1440.",
@@ -710,6 +728,58 @@ class BotConfigurationService:
"warning": "Убедитесь, что конфигурация существует в панели и содержит нужные приложения.",
"dependencies": "Настроенное подключение к RemnaWave API",
},
+ "TRAFFIC_MONITORING_ENABLED": {
+ "description": (
+ "Включает автоматический мониторинг трафика пользователей. "
+ "Система отслеживает изменения трафика (дельту) и сохраняет snapshot в Redis. "
+ "При превышении порогов отправляются уведомления пользователям и админам."
+ ),
+ "format": "Булево значение.",
+ "example": "true",
+ "warning": (
+ "Требует настроенного подключения к Redis. "
+ "При включении будет запущен фоновый мониторинг трафика по расписанию."
+ ),
+ "dependencies": "Redis, TRAFFIC_MONITORING_INTERVAL_HOURS, TRAFFIC_SNAPSHOT_TTL_HOURS",
+ },
+ "TRAFFIC_MONITORING_INTERVAL_HOURS": {
+ "description": (
+ "Интервал проверки трафика в часах. "
+ "Каждые N часов система проверяет трафик всех активных пользователей и сравнивает с предыдущим snapshot."
+ ),
+ "format": "Целое число часов (минимум 1).",
+ "example": "24",
+ "warning": (
+ "Слишком маленький интервал может создать большую нагрузку на RemnaWave API. "
+ "Рекомендуется 24 часа для ежедневного мониторинга."
+ ),
+ "dependencies": "TRAFFIC_MONITORING_ENABLED",
+ },
+ "TRAFFIC_MONITORED_NODES": {
+ "description": (
+ "Список UUID нод для мониторинга трафика через запятую. "
+ "Если пусто - мониторятся все ноды. "
+ "Позволяет ограничить мониторинг только определенными серверами."
+ ),
+ "format": "UUID через запятую или пусто для всех нод.",
+ "example": "d4aa2b8c-9a36-4f31-93a2-6f07dad05fba, a1b2c3d4-5678-90ab-cdef-1234567890ab",
+ "warning": "UUID должны существовать в RemnaWave, иначе мониторинг не будет работать.",
+ "dependencies": "TRAFFIC_MONITORING_ENABLED",
+ },
+ "TRAFFIC_SNAPSHOT_TTL_HOURS": {
+ "description": (
+ "Время жизни (TTL) snapshot трафика в Redis в часах. "
+ "Snapshot используется для вычисления дельты (изменения трафика) между проверками. "
+ "После истечения TTL snapshot удаляется и создается новый."
+ ),
+ "format": "Целое число часов (минимум 1).",
+ "example": "24",
+ "warning": (
+ "TTL должен быть >= интервала мониторинга. "
+ "Если TTL меньше интервала, snapshot будет удален до следующей проверки."
+ ),
+ "dependencies": "TRAFFIC_MONITORING_ENABLED, Redis",
+ },
}
@classmethod