diff --git a/app/handlers/admin/users.py b/app/handlers/admin/users.py
index 9b7a9c2a..b1e0a92a 100644
--- a/app/handlers/admin/users.py
+++ b/app/handlers/admin/users.py
@@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.states import AdminStates
-from app.database.models import User, UserStatus, Subscription
+from app.database.models import User, UserStatus, Subscription, SubscriptionStatus, TransactionType
from app.database.crud.user import get_user_by_id
from app.keyboards.admin import (
get_admin_users_keyboard, get_user_management_keyboard,
@@ -18,6 +18,7 @@ from app.services.user_service import UserService
from app.utils.decorators import admin_required, error_handler
from app.utils.formatters import format_datetime, format_time_ago
from app.services.remnawave_service import RemnaWaveService
+from app.external.remnawave_api import TrafficLimitStrategy
from app.database.crud.server_squad import get_all_server_squads, get_server_squad_by_uuid, get_server_squad_by_id
logger = logging.getLogger(__name__)
@@ -324,6 +325,10 @@ async def show_user_subscription(
types.InlineKeyboardButton(
text="🔄 Тип подписки",
callback_data=f"admin_sub_change_type_{user_id}"
+ ),
+ types.InlineKeyboardButton(
+ text="💳 Купить подписку",
+ callback_data=f"admin_sub_buy_{user_id}"
)
]
]
@@ -2327,6 +2332,289 @@ async def change_subscription_type(
)
await callback.answer()
+@admin_required
+@error_handler
+async def admin_buy_subscription(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
+):
+ user_id = int(callback.data.split('_')[-1])
+
+ user_service = UserService()
+ profile = await user_service.get_user_profile(db, user_id)
+
+ if not profile:
+ await callback.answer("❌ Пользователь не найден", show_alert=True)
+ return
+
+ target_user = profile["user"]
+ subscription = profile["subscription"]
+
+ if not subscription:
+ await callback.answer("❌ У пользователя нет подписки", show_alert=True)
+ return
+
+ available_periods = settings.get_available_subscription_periods()
+
+ period_buttons = []
+ for period in available_periods:
+ price_attr = f"PRICE_{period}_DAYS"
+ if hasattr(settings, price_attr):
+ price_kopeks = getattr(settings, price_attr)
+ price_rubles = price_kopeks // 100
+ period_buttons.append([
+ types.InlineKeyboardButton(
+ text=f"{period} дней ({price_rubles} ₽)",
+ callback_data=f"admin_buy_sub_confirm_{user_id}_{period}_{price_kopeks}"
+ )
+ ])
+
+ period_buttons.append([
+ types.InlineKeyboardButton(
+ text="❌ Отмена",
+ callback_data=f"admin_user_subscription_{user_id}"
+ )
+ ])
+
+ text = f"💳 Покупка подписки для пользователя\n\n"
+ text += f"👤 {target_user.full_name} (ID: {target_user.telegram_id})\n"
+ text += f"💰 Баланс пользователя: {settings.format_price(target_user.balance_kopeks)}\n\n"
+ text += "Выберите период подписки:\n"
+
+ await callback.message.edit_text(
+ text,
+ reply_markup=types.InlineKeyboardMarkup(inline_keyboard=period_buttons)
+ )
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def admin_buy_subscription_confirm(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
+):
+ parts = callback.data.split('_')
+ user_id = int(parts[4])
+ period_days = int(parts[5])
+ price_kopeks = int(parts[6])
+
+ user_service = UserService()
+ profile = await user_service.get_user_profile(db, user_id)
+
+ if not profile:
+ await callback.answer("❌ Пользователь не найден", show_alert=True)
+ return
+
+ target_user = profile["user"]
+ subscription = profile["subscription"]
+
+ if target_user.balance_kopeks < price_kopeks:
+ missing_kopeks = price_kopeks - target_user.balance_kopeks
+ await callback.message.edit_text(
+ f"❌ Недостаточно средств на балансе пользователя\n\n"
+ f"💰 Баланс пользователя: {settings.format_price(target_user.balance_kopeks)}\n"
+ f"💳 Стоимость подписки: {settings.format_price(price_kopeks)}\n"
+ f"📉 Не хватает: {settings.format_price(missing_kopeks)}\n\n"
+ f"Пополните баланс пользователя перед покупкой.",
+ reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
+ [types.InlineKeyboardButton(
+ text="⬅️ Назад к подписке",
+ callback_data=f"admin_user_subscription_{user_id}"
+ )]
+ ])
+ )
+ await callback.answer()
+ return
+
+ price_rubles = price_kopeks // 100
+ text = f"💳 Подтверждение покупки подписки\n\n"
+ text += f"👤 {target_user.full_name} (ID: {target_user.telegram_id})\n"
+ text += f"📅 Период подписки: {period_days} дней\n"
+ text += f"💰 Стоимость: {settings.format_price(price_kopeks)}\n"
+ text += f"💰 Баланс пользователя: {settings.format_price(target_user.balance_kopeks)}\n\n"
+ text += "Вы уверены, что хотите купить подписку для этого пользователя?"
+
+ keyboard = [
+ [
+ types.InlineKeyboardButton(
+ text="✅ Подтвердить",
+ callback_data=f"admin_buy_sub_execute_{user_id}_{period_days}_{price_kopeks}"
+ )
+ ],
+ [
+ types.InlineKeyboardButton(
+ text="❌ Отмена",
+ callback_data=f"admin_sub_buy_{user_id}"
+ )
+ ]
+ ]
+
+ await callback.message.edit_text(
+ text,
+ reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
+ )
+ await callback.answer()
+
+
+@admin_required
+@error_handler
+async def admin_buy_subscription_execute(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
+):
+ parts = callback.data.split('_')
+ user_id = int(parts[4])
+ period_days = int(parts[5])
+ price_kopeks = int(parts[6])
+
+ user_service = UserService()
+ profile = await user_service.get_user_profile(db, user_id)
+
+ if not profile:
+ await callback.answer("❌ Пользователь не найден", show_alert=True)
+ return
+
+ target_user = profile["user"]
+ subscription = profile["subscription"]
+
+ if target_user.balance_kopeks < price_kopeks:
+ await callback.answer("❌ Недостаточно средств на балансе пользователя", show_alert=True)
+ return
+
+ try:
+ from app.database.crud.user import subtract_user_balance
+ success = await subtract_user_balance(
+ db, target_user, price_kopeks,
+ f"Покупка подписки на {period_days} дней (администратор)"
+ )
+
+ if not success:
+ await callback.answer("❌ Ошибка списания средств", show_alert=True)
+ return
+
+ if subscription:
+ current_time = datetime.utcnow()
+
+ if subscription.end_date <= current_time:
+ subscription.start_date = current_time
+
+ subscription.end_date = current_time + timedelta(days=period_days)
+ subscription.status = SubscriptionStatus.ACTIVE.value
+ subscription.updated_at = current_time
+
+ if subscription.is_trial or not subscription.is_active:
+ subscription.is_trial = False
+ if subscription.traffic_limit_gb != 0:
+ subscription.traffic_limit_gb = 0
+ subscription.device_limit = settings.DEFAULT_DEVICE_LIMIT
+ if subscription.is_trial:
+ subscription.traffic_used_gb = 0.0
+
+ await db.commit()
+ await db.refresh(subscription)
+
+ from app.database.crud.transaction import create_transaction
+ transaction = await create_transaction(
+ db=db,
+ user_id=target_user.id,
+ type=TransactionType.SUBSCRIPTION_PAYMENT,
+ amount_kopeks=price_kopeks,
+ description=f"Продление подписки на {period_days} дней (администратор)"
+ )
+
+ try:
+ from app.services.remnawave_service import RemnaWaveService
+ from app.external.remnawave_api import UserStatus, TrafficLimitStrategy
+ remnawave_service = RemnaWaveService()
+
+ if target_user.remnawave_uuid:
+ async with remnawave_service.api as api:
+ remnawave_user = await api.update_user(
+ uuid=target_user.remnawave_uuid,
+ status=UserStatus.ACTIVE if subscription.is_active else UserStatus.EXPIRED,
+ expire_at=subscription.end_date,
+ traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0,
+ traffic_limit_strategy=TrafficLimitStrategy.MONTH,
+ hwid_device_limit=subscription.device_limit,
+ description=settings.format_remnawave_user_description(
+ full_name=target_user.full_name,
+ username=target_user.username,
+ telegram_id=target_user.telegram_id
+ ),
+ active_internal_squads=subscription.connected_squads
+ )
+ else:
+ username = f"user_{target_user.telegram_id}"
+ async with remnawave_service.api as api:
+ remnawave_user = await api.create_user(
+ username=username,
+ expire_at=subscription.end_date,
+ status=UserStatus.ACTIVE if subscription.is_active else UserStatus.EXPIRED,
+ traffic_limit_bytes=subscription.traffic_limit_gb * (1024**3) if subscription.traffic_limit_gb > 0 else 0,
+ traffic_limit_strategy=TrafficLimitStrategy.MONTH,
+ telegram_id=target_user.telegram_id,
+ hwid_device_limit=subscription.device_limit,
+ description=settings.format_remnawave_user_description(
+ full_name=target_user.full_name,
+ username=target_user.username,
+ telegram_id=target_user.telegram_id
+ ),
+ active_internal_squads=subscription.connected_squads
+ )
+
+ if remnawave_user and hasattr(remnawave_user, 'uuid'):
+ target_user.remnawave_uuid = remnawave_user.uuid
+ await db.commit()
+
+ if remnawave_user:
+ logger.info(f"Пользователь {target_user.telegram_id} успешно обновлен в RemnaWave")
+ else:
+ logger.error(f"Ошибка обновления пользователя {target_user.telegram_id} в RemnaWave")
+ except Exception as e:
+ logger.error(f"Ошибка работы с RemnaWave для пользователя {target_user.telegram_id}: {e}")
+
+ message = f"✅ Подписка пользователя продлена на {period_days} дней"
+ else:
+ message = "❌ Ошибка: у пользователя нет существующей подписки"
+
+ await callback.message.edit_text(
+ f"{message}\n\n"
+ f"👤 {target_user.full_name} (ID: {target_user.telegram_id})\n"
+ f"💰 Списано: {settings.format_price(price_kopeks)}\n"
+ f"📅 Подписка действительна до: {format_datetime(subscription.end_date)}",
+ reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
+ [types.InlineKeyboardButton(
+ text="⬅️ Назад к подписке",
+ callback_data=f"admin_user_subscription_{user_id}"
+ )]
+ ])
+ )
+
+ try:
+ if callback.bot:
+ await callback.bot.send_message(
+ chat_id=target_user.telegram_id,
+ text=f"💳 Администратор продлил вашу подписку\n\n"
+ f"📅 Подписка продлена на {period_days} дней\n"
+ f"💰 Списано с баланса: {settings.format_price(price_kopeks)}\n"
+ f"📅 Подписка действительна до: {format_datetime(subscription.end_date)}",
+ parse_mode="HTML"
+ )
+ except Exception as e:
+ logger.error(f"Ошибка отправки уведомления пользователю {target_user.telegram_id}: {e}")
+
+ await callback.answer()
+
+ except Exception as e:
+ logger.error(f"Ошибка покупки подписки администратором: {e}")
+ await callback.answer("❌ Ошибка при покупке подписки", show_alert=True)
+
+ await db.rollback()
+
@admin_required
@error_handler
@@ -2633,3 +2921,23 @@ def register_handlers(dp: Dispatcher):
change_subscription_type_confirm,
F.data.startswith("admin_sub_type_")
)
+
+ # Регистрация обработчика покупки подписки администратором
+ dp.callback_query.register(
+ admin_buy_subscription,
+ F.data.startswith("admin_sub_buy_")
+ )
+
+ # Регистрация дополнительных обработчиков для покупки подписки
+ dp.callback_query.register(
+ admin_buy_subscription_confirm,
+ F.data.startswith("admin_buy_sub_confirm_")
+ )
+
+ dp.callback_query.register(
+ admin_buy_subscription_execute,
+ F.data.startswith("admin_buy_sub_execute_")
+ )
+
+
+