diff --git a/app/cabinet/routes/subscription_modules/devices.py b/app/cabinet/routes/subscription_modules/devices.py index de686392..b1a98d36 100644 --- a/app/cabinet/routes/subscription_modules/devices.py +++ b/app/cabinet/routes/subscription_modules/devices.py @@ -18,7 +18,7 @@ from typing import Any import structlog from fastapi import APIRouter, Depends, HTTPException, Query as QueryParam, status -from sqlalchemy import select +from sqlalchemy import and_, select from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings @@ -37,6 +37,13 @@ logger = structlog.get_logger(__name__) router = APIRouter() +def _resolve_panel_uuid(subscription: Subscription | None, user: User) -> str | None: + """Resolve RemnaWave panel UUID: per-subscription in multi-tariff, user-level otherwise.""" + if settings.is_multi_tariff_enabled() and subscription and subscription.remnawave_uuid: + return subscription.remnawave_uuid + return user.remnawave_uuid + + @router.post('/devices') async def purchase_devices_legacy( request: DevicePurchaseRequest, @@ -57,7 +64,7 @@ async def purchase_devices_legacy( # Lock subscription row to prevent concurrent device purchases exceeding the limit _sub_filter = ( - Subscription.id == subscription_id + and_(Subscription.id == subscription_id, Subscription.user_id == user.id) if subscription_id and settings.is_multi_tariff_enabled() else Subscription.user_id == user.id ) @@ -282,7 +289,7 @@ async def purchase_devices( try: # Lock subscription row to prevent concurrent device purchases exceeding the limit _sub_filter = ( - Subscription.id == subscription_id + and_(Subscription.id == subscription_id, Subscription.user_id == user.id) if subscription_id and settings.is_multi_tariff_enabled() else Subscription.user_id == user.id ) @@ -748,7 +755,8 @@ async def get_devices( detail='No subscription found', ) - if not user.remnawave_uuid: + _puuid = _resolve_panel_uuid(subscription, user) + if not _puuid: return { 'devices': [], 'total': 0, @@ -758,7 +766,7 @@ async def get_devices( try: service = RemnaWaveService() async with service.get_api_client() as api: - response = await api.get_user_devices(user.remnawave_uuid) + response = await api.get_user_devices(_puuid) devices_list = response.get('devices', []) formatted_devices = [] @@ -810,7 +818,8 @@ async def delete_device( detail='No subscription found', ) - if not user.remnawave_uuid: + _puuid = _resolve_panel_uuid(subscription, user) + if not _puuid: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail='User UUID not found', @@ -819,7 +828,7 @@ async def delete_device( try: service = RemnaWaveService() async with service.get_api_client() as api: - delete_data = {'userUuid': user.remnawave_uuid, 'hwid': hwid} + delete_data = {'userUuid': _puuid, 'hwid': hwid} await api._make_request('POST', '/api/hwid/devices/delete', data=delete_data) return { @@ -853,7 +862,8 @@ async def delete_all_devices( detail='No subscription found', ) - if not user.remnawave_uuid: + _puuid = _resolve_panel_uuid(subscription, user) + if not _puuid: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail='User UUID not found', @@ -863,7 +873,7 @@ async def delete_all_devices( service = RemnaWaveService() async with service.get_api_client() as api: # Get all devices first - response = await api._make_request('GET', f'/api/hwid/devices/{user.remnawave_uuid}') + response = await api._make_request('GET', f'/api/hwid/devices/{_puuid}') if not response or 'response' not in response: return { @@ -885,7 +895,7 @@ async def delete_all_devices( device_hwid = device.get('hwid') if device_hwid: try: - delete_data = {'userUuid': user.remnawave_uuid, 'hwid': device_hwid} + delete_data = {'userUuid': _puuid, 'hwid': device_hwid} await api._make_request('POST', '/api/hwid/devices/delete', data=delete_data) deleted_count += 1 except Exception as device_error: @@ -963,11 +973,12 @@ async def get_device_reduction_info( # Get connected devices count connected_devices_count = 0 - if user.remnawave_uuid: + _puuid = _resolve_panel_uuid(subscription, user) + if _puuid: try: service = RemnaWaveService() async with service.get_api_client() as api: - response = await api._make_request('GET', f'/api/hwid/devices/{user.remnawave_uuid}') + response = await api._make_request('GET', f'/api/hwid/devices/{_puuid}') if response and 'response' in response: connected_devices_count = response['response'].get('total', 0) except Exception as e: @@ -1003,7 +1014,7 @@ async def reduce_devices( # Lock subscription to prevent concurrent device modifications _sub_filter = ( - Subscription.id == subscription_id + and_(Subscription.id == subscription_id, Subscription.user_id == user.id) if subscription_id and settings.is_multi_tariff_enabled() else Subscription.user_id == user.id ) @@ -1050,11 +1061,12 @@ async def reduce_devices( # Get connected devices and remove excess (last connected ones) connected_devices_count = 0 devices_removed_count = 0 - if user.remnawave_uuid: + _puuid = _resolve_panel_uuid(subscription, user) + if _puuid: try: service = RemnaWaveService() async with service.get_api_client() as api: - response = await api._make_request('GET', f'/api/hwid/devices/{user.remnawave_uuid}') + response = await api._make_request('GET', f'/api/hwid/devices/{_puuid}') if response and 'response' in response: devices_list = response['response'].get('devices', []) connected_devices_count = len(devices_list) @@ -1081,7 +1093,7 @@ async def reduce_devices( device_hwid = device.get('hwid') if device_hwid: try: - delete_data = {'userUuid': user.remnawave_uuid, 'hwid': device_hwid} + delete_data = {'userUuid': _puuid, 'hwid': device_hwid} await api._make_request('POST', '/api/hwid/devices/delete', data=delete_data) devices_removed_count += 1 logger.info('Removed device for user', device_hwid=device_hwid, user_id=user.id) diff --git a/app/cabinet/routes/subscription_modules/purchase.py b/app/cabinet/routes/subscription_modules/purchase.py index f6ff2697..5cec27e9 100644 --- a/app/cabinet/routes/subscription_modules/purchase.py +++ b/app/cabinet/routes/subscription_modules/purchase.py @@ -968,35 +968,33 @@ async def get_trial_info( logger.error('Error getting trial tariff for info', error=e) # Check if user already has an active subscription - if user.subscription: - now = datetime.now(UTC) - is_active = ( - user.subscription.status == 'active' and user.subscription.end_date and user.subscription.end_date > now - ) - if is_active: - return TrialInfoResponse( - is_available=False, - duration_days=duration_days, - traffic_limit_gb=traffic_limit_gb, - device_limit=device_limit, - requires_payment=requires_payment, - price_kopeks=price_kopeks, - price_rubles=price_kopeks / 100, - reason_unavailable='You already have an active subscription', - ) + subs = getattr(user, 'subscriptions', None) or [] + has_active = any(s.status == 'active' and s.end_date and s.end_date > datetime.now(UTC) for s in subs) + has_used_trial = any(s.is_trial for s in subs) or user.has_had_paid_subscription - # Check if user already used trial - if user.subscription.is_trial or user.has_had_paid_subscription: - return TrialInfoResponse( - is_available=False, - duration_days=duration_days, - traffic_limit_gb=traffic_limit_gb, - device_limit=device_limit, - requires_payment=requires_payment, - price_kopeks=price_kopeks, - price_rubles=price_kopeks / 100, - reason_unavailable='Trial already used', - ) + if has_active: + return TrialInfoResponse( + is_available=False, + duration_days=duration_days, + traffic_limit_gb=traffic_limit_gb, + device_limit=device_limit, + requires_payment=requires_payment, + price_kopeks=price_kopeks, + price_rubles=price_kopeks / 100, + reason_unavailable='You already have an active subscription', + ) + + if has_used_trial: + return TrialInfoResponse( + is_available=False, + duration_days=duration_days, + traffic_limit_gb=traffic_limit_gb, + device_limit=device_limit, + requires_payment=requires_payment, + price_kopeks=price_kopeks, + price_rubles=price_kopeks / 100, + reason_unavailable='Trial already used', + ) return TrialInfoResponse( is_available=True, @@ -1025,23 +1023,20 @@ async def activate_trial( ) # Check if user already has an active subscription - if user.subscription: - now = datetime.now(UTC) - is_active = ( - user.subscription.status == 'active' and user.subscription.end_date and user.subscription.end_date > now + subs = getattr(user, 'subscriptions', None) or [] + has_active = any(s.status == 'active' and s.end_date and s.end_date > datetime.now(UTC) for s in subs) + if has_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='You already have an active subscription', ) - if is_active: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='You already have an active subscription', - ) - # Check if user already used trial - if user.subscription.is_trial or user.has_had_paid_subscription: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail='Trial already used', - ) + # Check if user already used trial + if any(s.is_trial for s in subs) or user.has_had_paid_subscription: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Trial already used', + ) # Check if trial requires payment requires_payment = bool(settings.TRIAL_PAYMENT_ENABLED) diff --git a/app/cabinet/routes/subscription_modules/tariff_switch.py b/app/cabinet/routes/subscription_modules/tariff_switch.py index 56784b97..b11ccb50 100644 --- a/app/cabinet/routes/subscription_modules/tariff_switch.py +++ b/app/cabinet/routes/subscription_modules/tariff_switch.py @@ -415,11 +415,16 @@ async def switch_tariff( # Reset all devices on tariff switch devices_reset = False - if user.remnawave_uuid: + _switch_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else user.remnawave_uuid + ) + if _switch_uuid: try: service = RemnaWaveService() async with service.get_api_client() as api: - await api.reset_user_devices(user.remnawave_uuid) + await api.reset_user_devices(_switch_uuid) devices_reset = True logger.info('Reset all devices for user on tariff switch', user_id=user.id) except Exception as e: diff --git a/app/cabinet/routes/subscription_modules/traffic.py b/app/cabinet/routes/subscription_modules/traffic.py index e64377c1..21d1f9e9 100644 --- a/app/cabinet/routes/subscription_modules/traffic.py +++ b/app/cabinet/routes/subscription_modules/traffic.py @@ -316,11 +316,15 @@ async def purchase_traffic( # Синхронизируем с RemnaWave try: subscription_service = SubscriptionService() - if getattr(user, 'remnawave_uuid', None): + _panel_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else getattr(user, 'remnawave_uuid', None) + ) + if _panel_uuid: await subscription_service.update_remnawave_user(db, subscription) - # Явно включаем пользователя на панели (PATCH может не снять LIMITED-статус) if subscription.status == 'active': - await subscription_service.enable_remnawave_user(user.remnawave_uuid) + await subscription_service.enable_remnawave_user(_panel_uuid) else: await subscription_service.create_remnawave_user(db, subscription) except Exception as e: @@ -600,7 +604,12 @@ async def switch_traffic_package( # Sync with RemnaWave try: subscription_service = SubscriptionService() - if getattr(user, 'remnawave_uuid', None): + _panel_uuid2 = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else getattr(user, 'remnawave_uuid', None) + ) + if _panel_uuid2: await subscription_service.update_remnawave_user(db, subscription) else: await subscription_service.create_remnawave_user(db, subscription) @@ -682,11 +691,16 @@ async def refresh_traffic( try: remnawave_service = RemnaWaveService() - # Для email-пользователей (без telegram_id) используем UUID - if user.telegram_id: + # Resolve panel UUID for traffic lookup + _traffic_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else user.remnawave_uuid + ) + if user.telegram_id and not settings.is_multi_tariff_enabled(): traffic_stats = await remnawave_service.get_user_traffic_stats(user.telegram_id) - elif user.remnawave_uuid: - traffic_stats = await remnawave_service.get_user_traffic_stats_by_uuid(user.remnawave_uuid) + elif _traffic_uuid: + traffic_stats = await remnawave_service.get_user_traffic_stats_by_uuid(_traffic_uuid) else: traffic_stats = None diff --git a/app/cabinet/routes/wheel.py b/app/cabinet/routes/wheel.py index 8142956a..9192d569 100644 --- a/app/cabinet/routes/wheel.py +++ b/app/cabinet/routes/wheel.py @@ -20,6 +20,7 @@ from app.cabinet.schemas.wheel import ( WheelConfigResponse, WheelPrizeDisplay, ) +from app.config import settings from app.database.crud.wheel import ( get_or_create_wheel_config, get_user_spin_history, diff --git a/app/database/crud/subscription.py b/app/database/crud/subscription.py index f47fa302..467f299b 100644 --- a/app/database/crud/subscription.py +++ b/app/database/crud/subscription.py @@ -1405,6 +1405,7 @@ async def create_subscription_no_commit( if connected_squads is None: connected_squads = [] + short_id = await generate_unique_short_id(db) subscription = Subscription( user_id=user_id, status=status, @@ -1415,6 +1416,7 @@ async def create_subscription_no_commit( device_limit=device_limit, connected_squads=connected_squads, remnawave_short_uuid=remnawave_short_uuid, + remnawave_short_id=short_id, subscription_url=subscription_url, subscription_crypto_link=subscription_crypto_link, autopay_enabled=(settings.is_autopay_enabled_by_default() if autopay_enabled is None else autopay_enabled), @@ -1455,6 +1457,7 @@ async def create_subscription( if connected_squads is None: connected_squads = [] + short_id = await generate_unique_short_id(db) subscription = Subscription( user_id=user_id, status=status, @@ -1465,6 +1468,7 @@ async def create_subscription( device_limit=device_limit, connected_squads=connected_squads, remnawave_short_uuid=remnawave_short_uuid, + remnawave_short_id=short_id, subscription_url=subscription_url, subscription_crypto_link=subscription_crypto_link, autopay_enabled=(settings.is_autopay_enabled_by_default() if autopay_enabled is None else autopay_enabled), @@ -1502,7 +1506,23 @@ async def create_pending_subscription( current_time = datetime.now(UTC) end_date = current_time + timedelta(days=duration_days) - existing_subscription = await get_subscription_by_user_id(db, user_id) + if settings.is_multi_tariff_enabled() and tariff_id: + active_subs = await get_active_subscriptions_by_user_id(db, user_id) + existing_subscription = next((s for s in active_subs if s.tariff_id == tariff_id), None) + if not existing_subscription: + # Also check non-active subs for this tariff + result = await db.execute( + select(Subscription) + .where( + Subscription.user_id == user_id, + Subscription.tariff_id == tariff_id, + ) + .order_by(Subscription.created_at.desc()) + .limit(1) + ) + existing_subscription = result.scalar_one_or_none() + else: + existing_subscription = await get_subscription_by_user_id(db, user_id) if existing_subscription: if ( @@ -1540,6 +1560,7 @@ async def create_pending_subscription( ) return existing_subscription + short_id = await generate_unique_short_id(db) subscription = Subscription( user_id=user_id, status=SubscriptionStatus.PENDING.value, @@ -1552,6 +1573,7 @@ async def create_pending_subscription( tariff_id=tariff_id, autopay_enabled=settings.is_autopay_enabled_by_default(), autopay_days_before=settings.DEFAULT_AUTOPAY_DAYS_BEFORE, + remnawave_short_id=short_id, ) db.add(subscription) diff --git a/app/database/models.py b/app/database/models.py index f97c4cb9..7a71f09c 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -1351,6 +1351,13 @@ class Subscription(Base): Index('ix_subscriptions_user_id', 'user_id'), Index('ix_subscriptions_user_status', 'user_id', 'status'), Index('ix_subscriptions_user_tariff_status', 'user_id', 'tariff_id', 'status'), + Index( + 'uq_subscriptions_user_tariff_active', + 'user_id', + 'tariff_id', + unique=True, + postgresql_where=text("tariff_id IS NOT NULL AND status IN ('active', 'trial')"), + ), ) id = Column(Integer, primary_key=True, index=True) diff --git a/app/handlers/channel_member.py b/app/handlers/channel_member.py index b88c4788..cfeec1e2 100644 --- a/app/handlers/channel_member.py +++ b/app/handlers/channel_member.py @@ -85,12 +85,18 @@ async def on_user_joined_channel(event: ChatMemberUpdated, bot: Bot) -> None: ) # Re-enable in RemnaWave panel - if db_user.remnawave_uuid: - service = SubscriptionService() - try: - await service.enable_remnawave_user(db_user.remnawave_uuid) - except Exception as api_error: - logger.error('Failed to enable RemnaWave user', error=api_error) + service = SubscriptionService() + for subscription in disabled_subs: + _uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else db_user.remnawave_uuid + ) + if _uuid: + try: + await service.enable_remnawave_user(_uuid) + except Exception as api_error: + logger.error('Failed to enable RemnaWave user', error=api_error) # Notify the user try: @@ -158,12 +164,18 @@ async def on_user_left_channel(event: ChatMemberUpdated, bot: Bot) -> None: ) # Disable in RemnaWave panel - if db_user.remnawave_uuid: - service = SubscriptionService() - try: - await service.disable_remnawave_user(db_user.remnawave_uuid) - except Exception as api_error: - logger.error('Failed to disable RemnaWave user', error=api_error) + service = SubscriptionService() + for subscription in active_subs: + _uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else db_user.remnawave_uuid + ) + if _uuid: + try: + await service.disable_remnawave_user(_uuid) + except Exception as api_error: + logger.error('Failed to disable RemnaWave user', error=api_error) # Notify the user with channel subscription keyboard try: diff --git a/app/handlers/simple_subscription.py b/app/handlers/simple_subscription.py index c21ce508..201f9a6f 100644 --- a/app/handlers/simple_subscription.py +++ b/app/handlers/simple_subscription.py @@ -42,6 +42,10 @@ async def start_simple_subscription_purchase( await callback.answer('❌ Простая покупка подписки временно недоступна', show_alert=True) return + if settings.is_multi_tariff_enabled(): + await callback.answer('Используйте выбор тарифа для управления подписками', show_alert=True) + return + # Проверка ограничения на покупку/продление подписки if getattr(db_user, 'restriction_subscription', False): reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором') diff --git a/app/handlers/subscription/devices.py b/app/handlers/subscription/devices.py index 6b062f9e..4a8f5b03 100644 --- a/app/handlers/subscription/devices.py +++ b/app/handlers/subscription/devices.py @@ -73,9 +73,10 @@ def _get_remnawave_uuid(subscription, db_user): return getattr(subscription, 'remnawave_uuid', None) or db_user.remnawave_uuid -async def get_current_devices_detailed(db_user: User) -> dict: +async def get_current_devices_detailed(db_user: User, subscription=None) -> dict: try: - if not db_user.remnawave_uuid: + uuid = _get_remnawave_uuid(subscription, db_user) if subscription else db_user.remnawave_uuid + if not uuid: return {'count': 0, 'devices': []} from app.services.remnawave_service import RemnaWaveService @@ -83,7 +84,7 @@ async def get_current_devices_detailed(db_user: User) -> dict: service = RemnaWaveService() async with service.get_api_client() as api: - response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}') + response = await api._make_request('GET', f'/api/hwid/devices/{uuid}') if response and 'response' in response: devices_info = response['response'] @@ -144,9 +145,10 @@ async def get_servers_display_names(squad_uuids: list[str]) -> str: return f'{len(squad_uuids)} стран' -async def get_current_devices_count(db_user: User) -> str: +async def get_current_devices_count(db_user: User, subscription=None) -> str: try: - if not db_user.remnawave_uuid: + uuid = _get_remnawave_uuid(subscription, db_user) if subscription else db_user.remnawave_uuid + if not uuid: return '—' from app.services.remnawave_service import RemnaWaveService @@ -154,7 +156,7 @@ async def get_current_devices_count(db_user: User) -> str: service = RemnaWaveService() async with service.get_api_client() as api: - response = await api._make_request('GET', f'/api/hwid/devices/{db_user.remnawave_uuid}') + response = await api._make_request('GET', f'/api/hwid/devices/{uuid}') if response and 'response' in response: total_devices = response['response'].get('total', 0) diff --git a/app/handlers/subscription/purchase.py b/app/handlers/subscription/purchase.py index 7124254e..494fc090 100644 --- a/app/handlers/subscription/purchase.py +++ b/app/handlers/subscription/purchase.py @@ -2504,7 +2504,12 @@ async def confirm_purchase(callback: types.CallbackQuery, state: FSMContext, db_ subscription_service = SubscriptionService() # При покупке подписки ВСЕГДА сбрасываем трафик в панели - if db_user.remnawave_uuid: + _purchase_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else db_user.remnawave_uuid + ) + if _purchase_uuid: remnawave_user = await subscription_service.update_remnawave_user( db, subscription, diff --git a/app/handlers/subscription/tariff_purchase.py b/app/handlers/subscription/tariff_purchase.py index f5779e03..8c567eb3 100644 --- a/app/handlers/subscription/tariff_purchase.py +++ b/app/handlers/subscription/tariff_purchase.py @@ -898,9 +898,7 @@ async def handle_custom_confirm( # Проверяем есть ли уже подписка if settings.is_multi_tariff_enabled(): active_subs = await get_active_subscriptions_by_user_id(db, db_user.id) - existing_subscription = next( - (s for s in active_subs if s.tariff_id == tariff.id), active_subs[0] if active_subs else None - ) + existing_subscription = next((s for s in active_subs if s.tariff_id == tariff.id), None) else: existing_subscription = await get_subscription_by_user_id(db, db_user.id) @@ -1553,9 +1551,7 @@ async def confirm_daily_tariff_purchase( # Проверяем есть ли уже подписка if settings.is_multi_tariff_enabled(): active_subs = await get_active_subscriptions_by_user_id(db, db_user.id) - existing_subscription = next( - (s for s in active_subs if s.tariff_id == tariff.id), active_subs[0] if active_subs else None - ) + existing_subscription = next((s for s in active_subs if s.tariff_id == tariff.id), None) else: existing_subscription = await get_subscription_by_user_id(db, db_user.id) @@ -2690,13 +2686,18 @@ async def confirm_tariff_switch( # Гарантированный сброс устройств при смене тарифа await db.refresh(db_user) - if db_user.remnawave_uuid: + _reset_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else db_user.remnawave_uuid + ) + if _reset_uuid: try: from app.services.remnawave_service import RemnaWaveService service = RemnaWaveService() async with service.get_api_client() as api: - await api.reset_user_devices(db_user.remnawave_uuid) + await api.reset_user_devices(_reset_uuid) logger.info('🔧 Сброшены устройства при смене тарифа для user_id', db_user_id=db_user.id) except Exception as e: logger.error('Ошибка сброса устройств при смене тарифа', error=e) diff --git a/app/handlers/subscription/traffic.py b/app/handlers/subscription/traffic.py index 1c0f7a75..3d46325b 100644 --- a/app/handlers/subscription/traffic.py +++ b/app/handlers/subscription/traffic.py @@ -632,8 +632,13 @@ async def add_traffic(callback: types.CallbackQuery, db_user: User, db: AsyncSes await subscription_service.update_remnawave_user(db, subscription) # Явно включаем пользователя на панели (PATCH может не снять LIMITED-статус) - if db_user.remnawave_uuid and subscription.status == 'active': - await subscription_service.enable_remnawave_user(db_user.remnawave_uuid) + _en_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else db_user.remnawave_uuid + ) + if _en_uuid and subscription.status == 'active': + await subscription_service.enable_remnawave_user(_en_uuid) await create_transaction( db=db, @@ -923,8 +928,13 @@ async def execute_switch_traffic(callback: types.CallbackQuery, db_user: User, d await subscription_service.update_remnawave_user(db, subscription) # Явно включаем пользователя на панели (PATCH может не снять LIMITED-статус) - if db_user.remnawave_uuid and subscription.status == 'active': - await subscription_service.enable_remnawave_user(db_user.remnawave_uuid) + _en_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else db_user.remnawave_uuid + ) + if _en_uuid and subscription.status == 'active': + await subscription_service.enable_remnawave_user(_en_uuid) await db.refresh(db_user) await db.refresh(subscription) diff --git a/app/middlewares/channel_checker.py b/app/middlewares/channel_checker.py index 5770abc3..18fbcc4b 100644 --- a/app/middlewares/channel_checker.py +++ b/app/middlewares/channel_checker.py @@ -386,16 +386,22 @@ class ChannelCheckerMiddleware(BaseMiddleware): telegram_id=telegram_id, ) - if user.remnawave_uuid: - service = SubscriptionService() - try: - await service.disable_remnawave_user(user.remnawave_uuid) - except Exception as api_error: - logger.error( - 'Failed to disable RemnaWave user', - remnawave_uuid=user.remnawave_uuid, - api_error=api_error, - ) + service = SubscriptionService() + for subscription in active_subs: + panel_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else user.remnawave_uuid + ) + if panel_uuid: + try: + await service.disable_remnawave_user(panel_uuid) + except Exception as api_error: + logger.error( + 'Failed to disable RemnaWave user', + remnawave_uuid=panel_uuid, + api_error=api_error, + ) # Notify user about deactivation try: @@ -458,16 +464,22 @@ class ChannelCheckerMiddleware(BaseMiddleware): ) # Enable in RemnaWave - if user.remnawave_uuid: - service = SubscriptionService() - try: - await service.enable_remnawave_user(user.remnawave_uuid) - except Exception as api_error: - logger.error( - 'Failed to enable RemnaWave user', - remnawave_uuid=user.remnawave_uuid, - api_error=api_error, - ) + service = SubscriptionService() + for subscription in disabled_subs: + panel_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else user.remnawave_uuid + ) + if panel_uuid: + try: + await service.enable_remnawave_user(panel_uuid) + except Exception as api_error: + logger.error( + 'Failed to enable RemnaWave user', + remnawave_uuid=panel_uuid, + api_error=api_error, + ) # Notify user about reactivation try: diff --git a/app/services/monitoring_service.py b/app/services/monitoring_service.py index 82856b7d..5047bdbb 100644 --- a/app/services/monitoring_service.py +++ b/app/services/monitoring_service.py @@ -734,13 +734,18 @@ class MonitoringService: is_trial=subscription.is_trial, ) - if user.remnawave_uuid: + panel_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else user.remnawave_uuid + ) + if panel_uuid: try: - await self.subscription_service.disable_remnawave_user(user.remnawave_uuid) + await self.subscription_service.disable_remnawave_user(panel_uuid) except Exception as api_error: logger.error( 'Failed to disable RemnaWave user', - remnawave_uuid=user.remnawave_uuid, + remnawave_uuid=panel_uuid, api_error=api_error, ) @@ -806,8 +811,13 @@ class MonitoringService: ) try: - if user.remnawave_uuid: - await self.subscription_service.enable_remnawave_user(user.remnawave_uuid) + panel_uuid_restore = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else user.remnawave_uuid + ) + if panel_uuid_restore: + await self.subscription_service.enable_remnawave_user(panel_uuid_restore) else: # create_remnawave_user calls db.commit() internally -- # flush accumulated batch state first to preserve atomicity. diff --git a/app/services/remnawave_webhook_service.py b/app/services/remnawave_webhook_service.py index 05514580..b50b66c4 100644 --- a/app/services/remnawave_webhook_service.py +++ b/app/services/remnawave_webhook_service.py @@ -367,6 +367,9 @@ class RemnaWaveWebhookService: user_id=user.id, ) + if settings.is_multi_tariff_enabled(): + # In multi-tariff mode, don't fall back to arbitrary subscription + return user, None subscription = await get_subscription_by_user_id(db, user.id) return user, subscription diff --git a/app/services/subscription_auto_purchase_service.py b/app/services/subscription_auto_purchase_service.py index 23da27e7..d68b23de 100644 --- a/app/services/subscription_auto_purchase_service.py +++ b/app/services/subscription_auto_purchase_service.py @@ -1529,8 +1529,13 @@ async def _auto_add_devices( subscription_service = SubscriptionService() await subscription_service.update_remnawave_user(db, subscription) # Явно включаем пользователя на панели (PATCH может не снять LIMITED-статус) - if getattr(user, 'remnawave_uuid', None) and subscription.status == 'active': - await subscription_service.enable_remnawave_user(user.remnawave_uuid) + _panel_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else getattr(user, 'remnawave_uuid', None) + ) + if _panel_uuid and subscription.status == 'active': + await subscription_service.enable_remnawave_user(_panel_uuid) except Exception as error: logger.warning( '⚠️ Автопокупка устройств: не удалось обновить Remnawave для пользователя', @@ -1849,8 +1854,13 @@ async def _auto_add_traffic( subscription_service = SubscriptionService() await subscription_service.update_remnawave_user(db, subscription) # Явно включаем пользователя на панели (PATCH может не снять LIMITED-статус) - if getattr(user, 'remnawave_uuid', None) and subscription.status == 'active': - await subscription_service.enable_remnawave_user(user.remnawave_uuid) + _panel_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else getattr(user, 'remnawave_uuid', None) + ) + if _panel_uuid and subscription.status == 'active': + await subscription_service.enable_remnawave_user(_panel_uuid) except Exception as error: logger.warning( '⚠️ Автопокупка трафика: не удалось обновить Remnawave для пользователя', diff --git a/app/services/subscription_purchase_service.py b/app/services/subscription_purchase_service.py index 344116a9..232f52fc 100644 --- a/app/services/subscription_purchase_service.py +++ b/app/services/subscription_purchase_service.py @@ -1112,7 +1112,12 @@ class MiniAppSubscriptionPurchaseService: subscription_service = SubscriptionService() # При покупке подписки ВСЕГДА сбрасываем трафик в панели try: - if getattr(user, 'remnawave_uuid', None): + _purch_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else getattr(user, 'remnawave_uuid', None) + ) + if _purch_uuid: await subscription_service.update_remnawave_user( db, subscription, diff --git a/app/services/subscription_renewal_service.py b/app/services/subscription_renewal_service.py index 25396204..dec7c311 100644 --- a/app/services/subscription_renewal_service.py +++ b/app/services/subscription_renewal_service.py @@ -491,7 +491,12 @@ class SubscriptionRenewalService: subscription_service = SubscriptionService() try: await db.refresh(user) - if getattr(user, 'remnawave_uuid', None): + _renew_uuid = ( + subscription_after.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription_after.remnawave_uuid + else getattr(user, 'remnawave_uuid', None) + ) + if _renew_uuid: await subscription_service.update_remnawave_user( db, subscription_after, diff --git a/app/services/subscription_service.py b/app/services/subscription_service.py index 53a40e74..76e3b18a 100644 --- a/app/services/subscription_service.py +++ b/app/services/subscription_service.py @@ -508,7 +508,7 @@ class SubscriptionService: status_text = 'активным' if is_actually_active else 'истёкшим' logger.info( '✅ Обновлен RemnaWave пользователь со статусом', - remnawave_uuid=user.remnawave_uuid, + remnawave_uuid=remnawave_uuid, status_text=status_text, ) strategy_name = settings.DEFAULT_TRAFFIC_RESET_STRATEGY @@ -698,12 +698,12 @@ class SubscriptionService: # Проверяем, существует ли пользователь в RemnaWave try: async with self.get_api_client() as api: - remnawave_user = await api.get_user_by_uuid(user.remnawave_uuid) + remnawave_user = await api.get_user_by_uuid(sub_uuid) if not remnawave_user: needs_sync = True logger.warning( 'Пользователь не найден в RemnaWave, требуется синхронизация', - remnawave_uuid=user.remnawave_uuid, + remnawave_uuid=sub_uuid, ) except Exception as check_error: logger.warning('Не удалось проверить пользователя в RemnaWave', check_error=check_error) @@ -716,12 +716,12 @@ class SubscriptionService: 'Синхронизация подписки с RemnaWave (subscription_url=, remnawave_uuid=)', subscription_id=subscription.id, subscription_url=bool(subscription.subscription_url), - remnawave_uuid=bool(user.remnawave_uuid), + remnawave_uuid=bool(sub_uuid), ) # Пытаемся синхронизировать result = None - if user.remnawave_uuid: + if sub_uuid: # Пробуем обновить существующего пользователя result = await self.update_remnawave_user( db, @@ -732,10 +732,13 @@ class SubscriptionService: if not result: logger.warning( 'Не удалось обновить пользователя в RemnaWave, пробуем создать заново', - remnawave_uuid=user.remnawave_uuid, + remnawave_uuid=sub_uuid, ) # Сбрасываем старый UUID, create_remnawave_user установит новый - user.remnawave_uuid = None + if settings.is_multi_tariff_enabled(): + subscription.remnawave_uuid = None + else: + user.remnawave_uuid = None result = await self.create_remnawave_user( db, subscription, @@ -938,7 +941,12 @@ class SubscriptionService: async with semaphore: try: user = users_map.get(sub.user_id) - if not user or not user.remnawave_uuid: + remnawave_uuid = ( + sub.remnawave_uuid + if settings.is_multi_tariff_enabled() and sub.remnawave_uuid + else (user.remnawave_uuid if user else None) + ) + if not user or not remnawave_uuid: return False current_time = datetime.now(UTC) @@ -952,7 +960,7 @@ class SubscriptionService: hwid_limit = resolve_hwid_device_limit_for_payload(sub) update_kwargs = dict( - uuid=user.remnawave_uuid, + uuid=remnawave_uuid, status=UserStatus.ACTIVE if is_actually_active else UserStatus.DISABLED, expire_at=sub.end_date if is_actually_active diff --git a/app/services/user_service.py b/app/services/user_service.py index 5518d714..2da22d59 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -683,11 +683,30 @@ class UserService: remnawave_uuid=user.remnawave_uuid, ) else: - if user.remnawave_uuid: - try: - from app.services.subscription_service import SubscriptionService + from app.services.subscription_service import SubscriptionService - subscription_service = SubscriptionService() + subscription_service = SubscriptionService() + + if settings.is_multi_tariff_enabled(): + # In multi-tariff mode, disable each subscription's panel user individually + for sub in subs: + panel_uuid = sub.remnawave_uuid + if panel_uuid: + try: + await subscription_service.disable_remnawave_user(panel_uuid) + logger.info( + '✅ RemnaWave пользователь деактивирован при блокировке', + remnawave_uuid=panel_uuid, + subscription_id=sub.id, + ) + except Exception as e: + logger.error( + '❌ Ошибка деактивации RemnaWave при блокировке', + error=e, + subscription_id=sub.id, + ) + elif user.remnawave_uuid: + try: await subscription_service.disable_remnawave_user(user.remnawave_uuid) logger.info( '✅ RemnaWave пользователь деактивирован при блокировке', @@ -769,91 +788,89 @@ class UserService: '🗑️ Начинаем полное удаление пользователя (ID: )', user_id=user_id, user_id_display=user_id_display ) - if user.remnawave_uuid: - from app.config import settings - from app.database.crud.subscription import is_active_paid_subscription + from app.config import settings + from app.database.crud.subscription import is_active_paid_subscription - if not force_panel_delete and any( - is_active_paid_subscription(sub) for sub in (getattr(user, 'subscriptions', None) or []) - ): + # Collect all panel UUIDs to process + subs = getattr(user, 'subscriptions', None) or [] + if settings.is_multi_tariff_enabled(): + panel_uuids = [sub.remnawave_uuid for sub in subs if sub.remnawave_uuid] + else: + panel_uuids = [user.remnawave_uuid] if user.remnawave_uuid else [] + + if panel_uuids: + if not force_panel_delete and any(is_active_paid_subscription(sub) for sub in subs): logger.info( '⏭️ Пропуск отключения RemnaWave при удалении: у пользователя активная оплаченная подписка', user_id=user_id, - remnawave_uuid=user.remnawave_uuid, ) else: delete_mode = 'delete' if force_panel_delete else settings.get_remnawave_user_delete_mode() - try: - from app.services.remnawave_service import RemnaWaveService + for panel_uuid in panel_uuids: + try: + from app.services.remnawave_service import RemnaWaveService - remnawave_service = RemnaWaveService() + remnawave_service = RemnaWaveService() - if delete_mode == 'delete': - # Удаляем пользователя из панели Remnawave - async with remnawave_service.get_api_client() as api: - delete_success = await api.delete_user(user.remnawave_uuid) - if delete_success: - result.panel_deleted = True - logger.info( - '✅ RemnaWave пользователь удален из панели', - remnawave_uuid=user.remnawave_uuid, - ) - else: - result.panel_error = 'Remnawave API вернул ошибку удаления' - logger.warning( - '⚠️ Не удалось удалить пользователя из панели Remnawave', - remnawave_uuid=user.remnawave_uuid, - ) - else: - # Деактивируем пользователя в панели Remnawave - from app.services.subscription_service import SubscriptionService - - subscription_service = SubscriptionService() - disabled = await subscription_service.disable_remnawave_user(user.remnawave_uuid) - result.panel_deleted = disabled - if disabled: - logger.info( - '✅ RemnaWave пользователь деактивирован', - remnawave_uuid=user.remnawave_uuid, - delete_mode=delete_mode, - ) + if delete_mode == 'delete': + async with remnawave_service.get_api_client() as api: + delete_success = await api.delete_user(panel_uuid) + if delete_success: + result.panel_deleted = True + logger.info( + '✅ RemnaWave пользователь удален из панели', + remnawave_uuid=panel_uuid, + ) + else: + result.panel_error = 'Remnawave API вернул ошибку удаления' + logger.warning( + '⚠️ Не удалось удалить пользователя из панели Remnawave', + remnawave_uuid=panel_uuid, + ) else: - result.panel_error = 'disable_remnawave_user вернул False' - logger.warning( - '⚠️ Не удалось деактивировать пользователя в RemnaWave', - remnawave_uuid=user.remnawave_uuid, - delete_mode=delete_mode, - ) - - except Exception as e: - result.panel_error = 'Ошибка обработки пользователя в Remnawave' - logger.warning( - '⚠️ Ошибка обработки пользователя в Remnawave (режим: )', - delete_mode=delete_mode, - error=e, - ) - # Если основное действие не удалось, попытаемся хотя бы деактивировать - if delete_mode == 'delete': - try: from app.services.subscription_service import SubscriptionService subscription_service = SubscriptionService() - disabled = await subscription_service.disable_remnawave_user(user.remnawave_uuid) + disabled = await subscription_service.disable_remnawave_user(panel_uuid) + result.panel_deleted = disabled if disabled: - result.panel_deleted = True - result.panel_error = 'Удаление не удалось, пользователь деактивирован' logger.info( - '✅ RemnaWave пользователь деактивирован как fallback', - remnawave_uuid=user.remnawave_uuid, + '✅ RemnaWave пользователь деактивирован', + remnawave_uuid=panel_uuid, + delete_mode=delete_mode, ) else: + result.panel_error = 'disable_remnawave_user вернул False' logger.warning( - '⚠️ Fallback деактивация RemnaWave тоже не удалась', - remnawave_uuid=user.remnawave_uuid, + '⚠️ Не удалось деактивировать пользователя в RemnaWave', + remnawave_uuid=panel_uuid, + delete_mode=delete_mode, ) - except Exception as fallback_e: - logger.error('❌ Ошибка деактивации RemnaWave как fallback', fallback_e=fallback_e) + + except Exception as e: + result.panel_error = 'Ошибка обработки пользователя в Remnawave' + logger.warning( + '⚠️ Ошибка обработки пользователя в Remnawave', + delete_mode=delete_mode, + remnawave_uuid=panel_uuid, + error=e, + ) + if delete_mode == 'delete': + try: + from app.services.subscription_service import SubscriptionService + + subscription_service = SubscriptionService() + disabled = await subscription_service.disable_remnawave_user(panel_uuid) + if disabled: + result.panel_deleted = True + result.panel_error = 'Удаление не удалось, пользователь деактивирован' + logger.info( + '✅ RemnaWave пользователь деактивирован как fallback', + remnawave_uuid=panel_uuid, + ) + except Exception as fallback_e: + logger.error('❌ Ошибка деактивации RemnaWave как fallback', fallback_e=fallback_e) try: async with db.begin_nested(): diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index e4a02976..9ee65885 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -3025,8 +3025,8 @@ def _is_trial_available_for_user(user: User) -> bool: if getattr(user, 'has_had_paid_subscription', False): return False - subscription = getattr(user, 'subscription', None) - if subscription is not None: + subs = getattr(user, 'subscriptions', None) or [] + if any(s.is_active for s in subs): return False return True @@ -7167,8 +7167,13 @@ async def purchase_traffic_topup_endpoint( service = SubscriptionService() await service.update_remnawave_user(db, subscription) # Явно включаем пользователя на панели (PATCH может не снять LIMITED-статус) - if getattr(user, 'remnawave_uuid', None) and subscription.status == 'active': - await service.enable_remnawave_user(user.remnawave_uuid) + _en_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else getattr(user, 'remnawave_uuid', None) + ) + if _en_uuid and subscription.status == 'active': + await service.enable_remnawave_user(_en_uuid) except Exception as e: logger.error('Ошибка синхронизации с RemnaWave при докупке трафика', error=e) @@ -7236,8 +7241,15 @@ async def toggle_daily_subscription_pause_endpoint( # and to ensure is_daily_paused mutation is not overwritten by populate_existing from app.database.crud.user import lock_user_for_pricing + target_sub_id = subscription.id user = await lock_user_for_pricing(db, user.id) - subscription = user.subscription + locked_subs = getattr(user, 'subscriptions', None) or [] + subscription = next((s for s in locked_subs if s.id == target_sub_id), None) + if not subscription: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={'code': 'subscription_lost', 'message': 'Subscription not found after lock'}, + ) # Определяем состояние из LOCKED экземпляра from app.database.models import SubscriptionStatus diff --git a/app/webapi/routes/subscriptions.py b/app/webapi/routes/subscriptions.py index 4d4e6fce..82cad9e0 100644 --- a/app/webapi/routes/subscriptions.py +++ b/app/webapi/routes/subscriptions.py @@ -281,8 +281,13 @@ async def add_subscription_traffic_endpoint( await service.update_remnawave_user(db, subscription) user = await get_user_by_id(db, subscription.user_id) - if user and user.remnawave_uuid and subscription.status == 'active': - await service.enable_remnawave_user(user.remnawave_uuid) + _enable_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else (user.remnawave_uuid if user else None) + ) + if user and _enable_uuid and subscription.status == 'active': + await service.enable_remnawave_user(_enable_uuid) subscription = await _get_subscription(db, subscription.id) return _serialize_subscription(subscription) @@ -306,8 +311,13 @@ async def add_subscription_devices_endpoint( await service.update_remnawave_user(db, subscription) user = await get_user_by_id(db, subscription.user_id) - if user and user.remnawave_uuid and subscription.status == 'active': - await service.enable_remnawave_user(user.remnawave_uuid) + _enable_uuid = ( + subscription.remnawave_uuid + if settings.is_multi_tariff_enabled() and subscription.remnawave_uuid + else (user.remnawave_uuid if user else None) + ) + if user and _enable_uuid and subscription.status == 'active': + await service.enable_remnawave_user(_enable_uuid) subscription = await _get_subscription(db, subscription.id) return _serialize_subscription(subscription) diff --git a/app/webapi/routes/users.py b/app/webapi/routes/users.py index 85250f3e..f99d7342 100644 --- a/app/webapi/routes/users.py +++ b/app/webapi/routes/users.py @@ -470,7 +470,11 @@ async def create_user_subscription( update_server_counters=True, ) - # Создаем пользователя в RemnaWave для платных подписок + subscription_service = SubscriptionService() + await subscription_service.create_remnawave_user(db, subscription) + + # Provision trial subscriptions in RemnaWave as well + if payload.is_trial: subscription_service = SubscriptionService() await subscription_service.create_remnawave_user(db, subscription)