diff --git a/app/handlers/subscription/purchase.py b/app/handlers/subscription/purchase.py
index b9518499..fe410d8b 100644
--- a/app/handlers/subscription/purchase.py
+++ b/app/handlers/subscription/purchase.py
@@ -3190,12 +3190,27 @@ async def handle_toggle_daily_subscription_pause(
"DAILY_SUBSCRIPTION_RESUMED",
"▶️ Подписка возобновлена!"
)
+ # Синхронизируем с Remnawave - активируем пользователя
+ try:
+ from app.services.subscription_service import SubscriptionService
+ subscription_service = SubscriptionService()
+ await subscription_service.create_remnawave_user(
+ db,
+ subscription,
+ reset_traffic=False,
+ reset_reason=None,
+ )
+ logger.info(f"✅ Синхронизировано с Remnawave после возобновления суточной подписки {subscription.id}")
+ except Exception as e:
+ logger.error(f"Ошибка синхронизации с Remnawave при возобновлении: {e}")
else:
# Была активна, теперь на паузе
message = texts.t(
"DAILY_SUBSCRIPTION_PAUSED",
"⏸️ Подписка приостановлена!"
)
+ # При паузе можно отключить пользователя в Remnawave (опционально)
+ # Пока оставляем активным, т.к. пауза - это только остановка списания
await callback.answer(message, show_alert=True)
diff --git a/app/handlers/subscription/tariff_purchase.py b/app/handlers/subscription/tariff_purchase.py
index 266821ab..dfbf8352 100644
--- a/app/handlers/subscription/tariff_purchase.py
+++ b/app/handlers/subscription/tariff_purchase.py
@@ -318,6 +318,151 @@ def get_daily_tariff_insufficient_balance_keyboard(
])
+# ==================== Кастомные дни/трафик ====================
+
+
+def get_custom_tariff_keyboard(
+ tariff_id: int,
+ language: str,
+ days: int,
+ traffic_gb: int,
+ can_custom_days: bool,
+ can_custom_traffic: bool,
+ min_days: int = 1,
+ max_days: int = 365,
+ min_traffic: int = 1,
+ max_traffic: int = 1000,
+) -> InlineKeyboardMarkup:
+ """Создает клавиатуру для настройки кастомных дней и трафика."""
+ texts = get_texts(language)
+ buttons = []
+
+ # Кнопки изменения дней
+ if can_custom_days:
+ days_row = []
+ # -30 / -7 / -1
+ if days > min_days:
+ if days - 30 >= min_days:
+ days_row.append(InlineKeyboardButton(text="-30", callback_data=f"custom_days:{tariff_id}:-30"))
+ if days - 7 >= min_days:
+ days_row.append(InlineKeyboardButton(text="-7", callback_data=f"custom_days:{tariff_id}:-7"))
+ days_row.append(InlineKeyboardButton(text="-1", callback_data=f"custom_days:{tariff_id}:-1"))
+
+ # Текущее значение
+ days_row.append(InlineKeyboardButton(text=f"📅 {days} дн.", callback_data="noop"))
+
+ # +1 / +7 / +30
+ if days < max_days:
+ days_row.append(InlineKeyboardButton(text="+1", callback_data=f"custom_days:{tariff_id}:1"))
+ if days + 7 <= max_days:
+ days_row.append(InlineKeyboardButton(text="+7", callback_data=f"custom_days:{tariff_id}:7"))
+ if days + 30 <= max_days:
+ days_row.append(InlineKeyboardButton(text="+30", callback_data=f"custom_days:{tariff_id}:30"))
+
+ if days_row:
+ buttons.append(days_row)
+
+ # Кнопки изменения трафика
+ if can_custom_traffic:
+ traffic_row = []
+ # -100 / -10 / -1
+ if traffic_gb > min_traffic:
+ if traffic_gb - 100 >= min_traffic:
+ traffic_row.append(InlineKeyboardButton(text="-100", callback_data=f"custom_traffic:{tariff_id}:-100"))
+ if traffic_gb - 10 >= min_traffic:
+ traffic_row.append(InlineKeyboardButton(text="-10", callback_data=f"custom_traffic:{tariff_id}:-10"))
+ traffic_row.append(InlineKeyboardButton(text="-1", callback_data=f"custom_traffic:{tariff_id}:-1"))
+
+ # Текущее значение
+ traffic_row.append(InlineKeyboardButton(text=f"📊 {traffic_gb} ГБ", callback_data="noop"))
+
+ # +1 / +10 / +100
+ if traffic_gb < max_traffic:
+ traffic_row.append(InlineKeyboardButton(text="+1", callback_data=f"custom_traffic:{tariff_id}:1"))
+ if traffic_gb + 10 <= max_traffic:
+ traffic_row.append(InlineKeyboardButton(text="+10", callback_data=f"custom_traffic:{tariff_id}:10"))
+ if traffic_gb + 100 <= max_traffic:
+ traffic_row.append(InlineKeyboardButton(text="+100", callback_data=f"custom_traffic:{tariff_id}:100"))
+
+ if traffic_row:
+ buttons.append(traffic_row)
+
+ # Кнопка подтверждения
+ buttons.append([
+ InlineKeyboardButton(
+ text="✅ Подтвердить покупку",
+ callback_data=f"custom_confirm:{tariff_id}"
+ )
+ ])
+
+ # Кнопка назад
+ buttons.append([
+ InlineKeyboardButton(text=texts.BACK, callback_data="tariff_list")
+ ])
+
+ return InlineKeyboardMarkup(inline_keyboard=buttons)
+
+
+def format_custom_tariff_preview(
+ tariff: Tariff,
+ days: int,
+ traffic_gb: int,
+ user_balance: int,
+ discount_percent: int = 0,
+) -> str:
+ """Форматирует предпросмотр покупки с кастомными параметрами."""
+ # Рассчитываем цену
+ days_price = 0
+ traffic_price = 0
+
+ if tariff.can_purchase_custom_days():
+ days_price = tariff.get_price_for_custom_days(days) or 0
+
+ if tariff.can_purchase_custom_traffic():
+ traffic_price = tariff.get_price_for_custom_traffic(traffic_gb) or 0
+
+ total_price = days_price + traffic_price
+
+ # Применяем скидку
+ if discount_percent > 0:
+ total_price = _apply_promo_discount(total_price, discount_percent)
+
+ traffic_display = f"{traffic_gb} ГБ" if traffic_gb > 0 else _format_traffic(tariff.traffic_limit_gb)
+
+ text = f"""📦 {tariff.name}
+
+Настройте параметры:
+"""
+
+ if tariff.can_purchase_custom_days():
+ text += f"📅 Дней: {days} (от {tariff.min_days} до {tariff.max_days})\n"
+ text += f" 💰 {_format_price_kopeks(days_price)}\n"
+
+ if tariff.can_purchase_custom_traffic():
+ text += f"📊 Трафик: {traffic_gb} ГБ (от {tariff.min_traffic_gb} до {tariff.max_traffic_gb})\n"
+ text += f" 💰 {_format_price_kopeks(traffic_price)}\n"
+ else:
+ text += f"📊 Трафик: {traffic_display}\n"
+
+ text += f"📱 Устройств: {tariff.device_limit}\n"
+
+ if discount_percent > 0:
+ text += f"\n🎁 Скидка: {discount_percent}%\n"
+
+ text += f"""
+💰 Итого: {_format_price_kopeks(total_price)}
+
+💳 Ваш баланс: {_format_price_kopeks(user_balance)}"""
+
+ if user_balance < total_price:
+ missing = total_price - user_balance
+ text += f"\n⚠️ Не хватает: {_format_price_kopeks(missing)}"
+ else:
+ text += f"\nПосле оплаты: {_format_price_kopeks(user_balance - total_price)}"
+
+ return text
+
+
@error_handler
async def show_tariffs_list(
callback: types.CallbackQuery,
@@ -416,17 +561,317 @@ async def select_tariff(
parse_mode="HTML"
)
else:
- # Для обычного тарифа показываем выбор периода
- await callback.message.edit_text(
- format_tariff_info_for_user(tariff, db_user.language),
- reply_markup=get_tariff_periods_keyboard(tariff, db_user.language, db_user=db_user),
- parse_mode="HTML"
- )
+ # Проверяем, есть ли кастомные дни или трафик
+ can_custom_days = tariff.can_purchase_custom_days()
+ can_custom_traffic = tariff.can_purchase_custom_traffic()
+
+ if can_custom_days or can_custom_traffic:
+ # Показываем экран настройки кастомных параметров
+ user_balance = db_user.balance_kopeks or 0
+
+ # Начальные значения - минимальные
+ initial_days = tariff.min_days if can_custom_days else 30
+ initial_traffic = tariff.min_traffic_gb if can_custom_traffic else tariff.traffic_limit_gb
+
+ # Сохраняем в состояние
+ await state.update_data(
+ selected_tariff_id=tariff_id,
+ custom_days=initial_days,
+ custom_traffic_gb=initial_traffic,
+ )
+
+ preview_text = format_custom_tariff_preview(
+ tariff=tariff,
+ days=initial_days,
+ traffic_gb=initial_traffic,
+ user_balance=user_balance,
+ )
+
+ await callback.message.edit_text(
+ preview_text,
+ reply_markup=get_custom_tariff_keyboard(
+ tariff_id=tariff_id,
+ language=db_user.language,
+ days=initial_days,
+ traffic_gb=initial_traffic,
+ can_custom_days=can_custom_days,
+ can_custom_traffic=can_custom_traffic,
+ min_days=tariff.min_days,
+ max_days=tariff.max_days,
+ min_traffic=tariff.min_traffic_gb,
+ max_traffic=tariff.max_traffic_gb,
+ ),
+ parse_mode="HTML"
+ )
+ else:
+ # Для обычного тарифа показываем выбор периода
+ await callback.message.edit_text(
+ format_tariff_info_for_user(tariff, db_user.language),
+ reply_markup=get_tariff_periods_keyboard(tariff, db_user.language, db_user=db_user),
+ parse_mode="HTML"
+ )
await state.update_data(selected_tariff_id=tariff_id)
await callback.answer()
+@error_handler
+async def handle_custom_days_change(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession,
+ state: FSMContext,
+):
+ """Обрабатывает изменение количества дней."""
+ parts = callback.data.split(":")
+ tariff_id = int(parts[1])
+ delta = int(parts[2])
+
+ tariff = await get_tariff_by_id(db, tariff_id)
+ if not tariff or not tariff.is_active:
+ await callback.answer("Тариф недоступен", show_alert=True)
+ return
+
+ state_data = await state.get_data()
+ current_days = state_data.get('custom_days', tariff.min_days)
+ current_traffic = state_data.get('custom_traffic_gb', tariff.min_traffic_gb)
+
+ # Применяем изменение
+ new_days = current_days + delta
+ new_days = max(tariff.min_days, min(tariff.max_days, new_days))
+
+ await state.update_data(custom_days=new_days)
+
+ user_balance = db_user.balance_kopeks or 0
+
+ preview_text = format_custom_tariff_preview(
+ tariff=tariff,
+ days=new_days,
+ traffic_gb=current_traffic,
+ user_balance=user_balance,
+ )
+
+ await callback.message.edit_text(
+ preview_text,
+ reply_markup=get_custom_tariff_keyboard(
+ tariff_id=tariff_id,
+ language=db_user.language,
+ days=new_days,
+ traffic_gb=current_traffic,
+ can_custom_days=tariff.can_purchase_custom_days(),
+ can_custom_traffic=tariff.can_purchase_custom_traffic(),
+ min_days=tariff.min_days,
+ max_days=tariff.max_days,
+ min_traffic=tariff.min_traffic_gb,
+ max_traffic=tariff.max_traffic_gb,
+ ),
+ parse_mode="HTML"
+ )
+ await callback.answer()
+
+
+@error_handler
+async def handle_custom_traffic_change(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession,
+ state: FSMContext,
+):
+ """Обрабатывает изменение количества трафика."""
+ parts = callback.data.split(":")
+ tariff_id = int(parts[1])
+ delta = int(parts[2])
+
+ tariff = await get_tariff_by_id(db, tariff_id)
+ if not tariff or not tariff.is_active:
+ await callback.answer("Тариф недоступен", show_alert=True)
+ return
+
+ state_data = await state.get_data()
+ current_days = state_data.get('custom_days', tariff.min_days)
+ current_traffic = state_data.get('custom_traffic_gb', tariff.min_traffic_gb)
+
+ # Применяем изменение
+ new_traffic = current_traffic + delta
+ new_traffic = max(tariff.min_traffic_gb, min(tariff.max_traffic_gb, new_traffic))
+
+ await state.update_data(custom_traffic_gb=new_traffic)
+
+ user_balance = db_user.balance_kopeks or 0
+
+ preview_text = format_custom_tariff_preview(
+ tariff=tariff,
+ days=current_days,
+ traffic_gb=new_traffic,
+ user_balance=user_balance,
+ )
+
+ await callback.message.edit_text(
+ preview_text,
+ reply_markup=get_custom_tariff_keyboard(
+ tariff_id=tariff_id,
+ language=db_user.language,
+ days=current_days,
+ traffic_gb=new_traffic,
+ can_custom_days=tariff.can_purchase_custom_days(),
+ can_custom_traffic=tariff.can_purchase_custom_traffic(),
+ min_days=tariff.min_days,
+ max_days=tariff.max_days,
+ min_traffic=tariff.min_traffic_gb,
+ max_traffic=tariff.max_traffic_gb,
+ ),
+ parse_mode="HTML"
+ )
+ await callback.answer()
+
+
+@error_handler
+async def handle_custom_confirm(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession,
+ state: FSMContext,
+):
+ """Подтверждает покупку тарифа с кастомными параметрами."""
+ tariff_id = int(callback.data.split(":")[1])
+
+ tariff = await get_tariff_by_id(db, tariff_id)
+ if not tariff or not tariff.is_active:
+ await callback.answer("Тариф недоступен", show_alert=True)
+ return
+
+ state_data = await state.get_data()
+ custom_days = state_data.get('custom_days', tariff.min_days)
+ custom_traffic = state_data.get('custom_traffic_gb', tariff.min_traffic_gb)
+
+ # Рассчитываем цену
+ days_price = tariff.get_price_for_custom_days(custom_days) or 0
+ traffic_price = tariff.get_price_for_custom_traffic(custom_traffic) or 0
+ total_price = days_price + traffic_price
+
+ # Проверяем баланс
+ user_balance = db_user.balance_kopeks or 0
+ if user_balance < total_price:
+ await callback.answer("Недостаточно средств на балансе", show_alert=True)
+ return
+
+ texts = get_texts(db_user.language)
+
+ try:
+ # Списываем баланс
+ success = await subtract_user_balance(
+ db, db_user, total_price,
+ f"Покупка тарифа {tariff.name} на {custom_days} дней"
+ )
+ if not success:
+ await callback.answer("Ошибка списания баланса", show_alert=True)
+ return
+
+ # Получаем список серверов из тарифа
+ squads = tariff.allowed_squads or []
+
+ # Если allowed_squads пустой - значит "все серверы", получаем их
+ if not squads:
+ from app.database.crud.server_squad import get_all_server_squads
+ all_servers, _ = await get_all_server_squads(db, available_only=True)
+ squads = [s.squad_uuid for s in all_servers if s.squad_uuid]
+
+ # Определяем трафик
+ traffic_limit = custom_traffic if tariff.can_purchase_custom_traffic() else tariff.traffic_limit_gb
+
+ # Проверяем есть ли уже подписка
+ existing_subscription = await get_subscription_by_user_id(db, db_user.id)
+
+ if existing_subscription:
+ # Продлеваем существующую подписку и обновляем параметры тарифа
+ subscription = await extend_subscription(
+ db,
+ existing_subscription,
+ days=custom_days,
+ tariff_id=tariff.id,
+ traffic_limit_gb=traffic_limit,
+ device_limit=tariff.device_limit,
+ connected_squads=squads,
+ )
+ else:
+ # Создаем новую подписку
+ subscription = await create_paid_subscription(
+ db=db,
+ user_id=db_user.id,
+ duration_days=custom_days,
+ traffic_limit_gb=traffic_limit,
+ device_limit=tariff.device_limit,
+ connected_squads=squads,
+ tariff_id=tariff.id,
+ )
+
+ # Обновляем пользователя в Remnawave
+ try:
+ subscription_service = SubscriptionService()
+ await subscription_service.create_remnawave_user(
+ db,
+ subscription,
+ reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT,
+ reset_reason="покупка тарифа",
+ )
+ except Exception as e:
+ logger.error(f"Ошибка обновления Remnawave: {e}")
+
+ # Создаем транзакцию
+ await create_transaction(
+ db,
+ user_id=db_user.id,
+ type=TransactionType.SUBSCRIPTION_PAYMENT,
+ amount_kopeks=-total_price,
+ description=f"Покупка тарифа {tariff.name} на {custom_days} дней",
+ )
+
+ # Отправляем уведомление админу
+ try:
+ admin_notification_service = AdminNotificationService(callback.bot)
+ await admin_notification_service.send_subscription_purchase_notification(
+ db,
+ db_user,
+ subscription,
+ None,
+ custom_days,
+ was_trial_conversion=False,
+ amount_kopeks=total_price,
+ )
+ except Exception as e:
+ logger.error(f"Ошибка отправки уведомления админу: {e}")
+
+ # Очищаем корзину после успешной покупки
+ try:
+ await user_cart_service.delete_user_cart(db_user.id)
+ except Exception as e:
+ logger.error(f"Ошибка очистки корзины: {e}")
+
+ await state.clear()
+
+ traffic_display = _format_traffic(traffic_limit)
+
+ await callback.message.edit_text(
+ f"🎉 Подписка успешно оформлена!\n\n"
+ f"📦 Тариф: {tariff.name}\n"
+ f"📊 Трафик: {traffic_display}\n"
+ f"📱 Устройств: {tariff.device_limit}\n"
+ f"📅 Период: {_format_period(custom_days)}\n"
+ f"💰 Списано: {_format_price_kopeks(total_price)}\n\n"
+ f"Перейдите в раздел «Подписка» для подключения.",
+ reply_markup=InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")],
+ [InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")]
+ ]),
+ parse_mode="HTML"
+ )
+ await callback.answer("Подписка оформлена!", show_alert=True)
+
+ except Exception as e:
+ logger.error(f"Ошибка при покупке тарифа с кастомными параметрами: {e}", exc_info=True)
+ await callback.answer("Произошла ошибка при оформлении подписки", show_alert=True)
+
+
@error_handler
async def select_tariff_period(
callback: types.CallbackQuery,
@@ -2460,6 +2905,11 @@ def register_tariff_purchase_handlers(dp: Dispatcher):
# Подтверждение покупки суточного тарифа
dp.callback_query.register(confirm_daily_tariff_purchase, F.data.startswith("daily_tariff_confirm:"))
+ # Кастомные дни/трафик
+ dp.callback_query.register(handle_custom_days_change, F.data.startswith("custom_days:"))
+ dp.callback_query.register(handle_custom_traffic_change, F.data.startswith("custom_traffic:"))
+ dp.callback_query.register(handle_custom_confirm, F.data.startswith("custom_confirm:"))
+
# Продление по тарифу
dp.callback_query.register(select_tariff_extend_period, F.data.startswith("tariff_extend:"))
dp.callback_query.register(confirm_tariff_extend, F.data.startswith("tariff_ext_confirm:"))
diff --git a/app/services/ban_notification_service.py b/app/services/ban_notification_service.py
index c3c8f8e9..1e521282 100644
--- a/app/services/ban_notification_service.py
+++ b/app/services/ban_notification_service.py
@@ -229,6 +229,65 @@ class BanNotificationService:
)
return False, f"Ошибка Telegram API: {str(e)}", user.telegram_id
+ async def send_network_wifi_notification(
+ self,
+ db: AsyncSession,
+ user_identifier: str,
+ username: str,
+ ban_minutes: int,
+ network_type: Optional[str] = None,
+ node_name: Optional[str] = None
+ ) -> Tuple[bool, str, Optional[int]]:
+ """
+ Отправить уведомление о блокировке за использование WiFi сети
+
+ Returns:
+ (success, message, telegram_id)
+ """
+ if not self._bot:
+ return False, "Бот не инициализирован", None
+
+ # Находим пользователя
+ user = await self._find_user_by_identifier(db, user_identifier)
+ if not user:
+ logger.warning(f"Пользователь {user_identifier} не найден в базе данных")
+ return False, f"Пользователь не найден: {user_identifier}", None
+
+ # Формируем сообщение
+ network_info = f"Тип сети: {network_type}\n" if network_type else ""
+ node_info = f"Сервер: {node_name}\n" if node_name else ""
+
+ message_text = (
+ "📶 Блокировка за использование WiFi сети\n\n"
+ f"Ваш аккаунт временно заблокирован из-за использования WiFi сети.\n\n"
+ f"{network_info}"
+ f"{node_info}"
+ f"⏱ Блокировка на: {ban_minutes} мин\n\n"
+ f"ℹ️ Использование VPN через WiFi запрещено правилами сервиса.\n"
+ f"Пожалуйста, используйте мобильную сеть для подключения к VPN.\n\n"
+ f"После окончания блокировки ваш доступ будет восстановлен автоматически."
+ )
+
+ # Отправляем сообщение
+ try:
+ await self._bot.send_message(
+ chat_id=user.telegram_id,
+ text=message_text,
+ parse_mode="HTML"
+ )
+ logger.info(
+ f"Уведомление о WiFi бане отправлено пользователю {username} "
+ f"(telegram_id: {user.telegram_id})"
+ )
+ return True, "Уведомление отправлено", user.telegram_id
+
+ except TelegramAPIError as e:
+ logger.error(
+ f"Ошибка отправки WiFi уведомления пользователю {username} "
+ f"(telegram_id: {user.telegram_id}): {e}"
+ )
+ return False, f"Ошибка Telegram API: {str(e)}", user.telegram_id
+
# Глобальный экземпляр сервиса
ban_notification_service = BanNotificationService()
diff --git a/app/services/remnawave_service.py b/app/services/remnawave_service.py
index 50cc3e0f..f5eaef5d 100644
--- a/app/services/remnawave_service.py
+++ b/app/services/remnawave_service.py
@@ -1893,7 +1893,60 @@ class RemnaWaveService:
except Exception as e:
logger.error(f"Ошибка получения статистики трафика для пользователя {telegram_id}: {e}")
return None
-
+
+ async def get_telegram_id_by_email(self, user_identifier: str) -> Optional[int]:
+ """
+ Получить telegram_id пользователя по email или username из панели RemnaWave.
+
+ Args:
+ user_identifier: Email или username пользователя
+
+ Returns:
+ telegram_id если найден, иначе None
+ """
+ if not self.is_configured:
+ logger.warning("RemnaWave API не настроен для поиска пользователя")
+ return None
+
+ try:
+ async with self.get_api_client() as api:
+ # Сначала пробуем найти по username (часто username == email)
+ try:
+ user = await api.get_user_by_username(user_identifier)
+ if user and user.telegram_id:
+ logger.info(
+ f"Найден пользователь по username '{user_identifier}': "
+ f"telegram_id={user.telegram_id}"
+ )
+ return user.telegram_id
+ except Exception as e:
+ logger.debug(f"Пользователь не найден по username '{user_identifier}': {e}")
+
+ # Если не нашли по username, ищем по email среди всех пользователей
+ try:
+ all_users_response = await api.get_all_users(start=0, size=10000)
+ users_list = all_users_response.get('users', [])
+
+ for panel_user in users_list:
+ panel_email = panel_user.email if hasattr(panel_user, 'email') else None
+ if panel_email and panel_email.lower() == user_identifier.lower():
+ panel_telegram_id = panel_user.telegram_id if hasattr(panel_user, 'telegram_id') else None
+ if panel_telegram_id:
+ logger.info(
+ f"Найден пользователь по email '{user_identifier}': "
+ f"telegram_id={panel_telegram_id}"
+ )
+ return panel_telegram_id
+ except Exception as e:
+ logger.warning(f"Ошибка поиска пользователя по email '{user_identifier}': {e}")
+
+ logger.warning(f"Пользователь с идентификатором '{user_identifier}' не найден в панели")
+ return None
+
+ except Exception as e:
+ logger.error(f"Ошибка получения telegram_id для '{user_identifier}': {e}")
+ return None
+
async def test_api_connection(self) -> Dict[str, Any]:
if not self.is_configured:
return {
diff --git a/app/states.py b/app/states.py
index 7e608051..98b5d4b7 100644
--- a/app/states.py
+++ b/app/states.py
@@ -12,17 +12,22 @@ class SubscriptionStates(StatesGroup):
selecting_countries = State()
selecting_devices = State()
confirming_purchase = State()
-
+
adding_countries = State()
adding_traffic = State()
adding_devices = State()
extending_subscription = State()
confirming_traffic_reset = State()
cart_saved_for_topup = State()
-
+
# Состояния для простой подписки
waiting_for_simple_subscription_payment_method = State()
+ # Состояния для кастомных дней/трафика при покупке тарифа
+ selecting_custom_days = State()
+ selecting_custom_traffic = State()
+ confirming_custom_purchase = State()
+
class BalanceStates(StatesGroup):
waiting_for_amount = State()
waiting_for_pal24_method = State()
diff --git a/app/webapi/routes/ban_notifications.py b/app/webapi/routes/ban_notifications.py
index 69cb08f0..69e45eea 100644
--- a/app/webapi/routes/ban_notifications.py
+++ b/app/webapi/routes/ban_notifications.py
@@ -87,6 +87,22 @@ async def send_ban_notification(
warning_message=request.warning_message,
)
+ elif request.notification_type == "network_wifi":
+ if request.ban_minutes is None:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Для типа 'network_wifi' требуется поле: ban_minutes"
+ )
+
+ success, message, telegram_id = await ban_notification_service.send_network_wifi_notification(
+ db=db,
+ user_identifier=request.user_identifier,
+ username=request.username,
+ ban_minutes=request.ban_minutes,
+ network_type=request.network_type,
+ node_name=request.node_name,
+ )
+
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
diff --git a/app/webapi/schemas/ban_notifications.py b/app/webapi/schemas/ban_notifications.py
index 1e3800de..5d88253a 100644
--- a/app/webapi/schemas/ban_notifications.py
+++ b/app/webapi/schemas/ban_notifications.py
@@ -7,8 +7,8 @@ from pydantic import BaseModel, Field
class BanNotificationRequest(BaseModel):
"""Запрос на отправку уведомления о бане пользователю"""
- notification_type: Literal["punishment", "enabled", "warning"] = Field(
- description="Тип уведомления: punishment (бан), enabled (разбан), warning (предупреждение)"
+ notification_type: Literal["punishment", "enabled", "warning", "network_wifi"] = Field(
+ description="Тип уведомления: punishment (бан за устройства), enabled (разбан), warning (предупреждение), network_wifi (бан за WiFi)"
)
user_identifier: str = Field(
description="Email или user_id пользователя из Remnawave Panel"
@@ -25,6 +25,10 @@ class BanNotificationRequest(BaseModel):
# Данные для warning
warning_message: Optional[str] = Field(None, description="Текст предупреждения")
+ # Данные для network_wifi
+ network_type: Optional[str] = Field(None, description="Тип сети (WiFi/Mobile)")
+ node_name: Optional[str] = Field(None, description="Название ноды")
+
class Config:
json_schema_extra = {
"example": {