diff --git a/app/cabinet/routes/admin_users.py b/app/cabinet/routes/admin_users.py index e36c0b3f..7f205f2a 100644 --- a/app/cabinet/routes/admin_users.py +++ b/app/cabinet/routes/admin_users.py @@ -1122,26 +1122,18 @@ async def update_user_subscription( ) # Preserve extra purchased devices above the old tariff's base limit - extra_devices = 0 - if subscription.tariff_id: - old_tariff = await get_tariff_by_id(db, subscription.tariff_id) - if old_tariff and old_tariff.device_limit: - extra_devices = max(0, (subscription.device_limit or old_tariff.device_limit) - old_tariff.device_limit) + from app.database.crud.subscription import calc_device_limit_on_tariff_switch - from app.config import settings + old_tariff = await get_tariff_by_id(db, subscription.tariff_id) if subscription.tariff_id else None subscription.tariff_id = request.tariff_id subscription.traffic_limit_gb = tariff.traffic_limit_gb - - new_base = tariff.device_limit or 1 - new_total = new_base + extra_devices - # Cap at new tariff's max_device_limit, falling back to global MAX_DEVICES_LIMIT - effective_max = tariff.max_device_limit or ( - settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None + subscription.device_limit = calc_device_limit_on_tariff_switch( + current_device_limit=subscription.device_limit, + old_tariff_device_limit=old_tariff.device_limit if old_tariff else None, + new_tariff_device_limit=tariff.device_limit, + max_device_limit=tariff.max_device_limit, ) - if effective_max and new_total > effective_max: - new_total = effective_max - subscription.device_limit = new_total # Set squads from tariff if tariff.allowed_squads: subscription.connected_squads = tariff.allowed_squads diff --git a/app/cabinet/routes/contests.py b/app/cabinet/routes/contests.py index b7cfaa23..b7ff501f 100644 --- a/app/cabinet/routes/contests.py +++ b/app/cabinet/routes/contests.py @@ -86,6 +86,7 @@ def _user_allowed(subscription) -> bool: return subscription.status in { SubscriptionStatus.ACTIVE.value, SubscriptionStatus.TRIAL.value, + SubscriptionStatus.LIMITED.value, } diff --git a/app/cabinet/routes/subscription.py b/app/cabinet/routes/subscription.py index 292b1216..cd03505f 100644 --- a/app/cabinet/routes/subscription.py +++ b/app/cabinet/routes/subscription.py @@ -141,6 +141,7 @@ def _subscription_to_response( actual_status = subscription.actual_status is_expired = actual_status == 'expired' is_active = actual_status in ('active', 'trial') + is_limited = actual_status == 'limited' # Calculate time remaining days_left = 0 @@ -235,6 +236,7 @@ def _subscription_to_response( hide_subscription_link=hide_link, is_active=is_active, is_expired=is_expired, + is_limited=is_limited, traffic_purchases=traffic_purchases or [], is_daily=is_daily, is_daily_paused=is_daily_paused, @@ -574,7 +576,7 @@ async def renew_subscription( # Extend from end_date or now if expired now = datetime.now(UTC) - was_expired = user.subscription.status in ('expired', 'disabled') or ( + was_expired = user.subscription.status in ('expired', 'disabled', 'limited') or ( user.subscription.end_date is not None and user.subscription.end_date <= now ) @@ -4366,9 +4368,18 @@ async def switch_tariff( # Update subscription old_tariff_name = current_tariff.name if current_tariff else 'Unknown' + + # Preserve extra purchased devices above the old tariff's base limit + from app.database.crud.subscription import calc_device_limit_on_tariff_switch + user.subscription.tariff_id = new_tariff.id user.subscription.traffic_limit_gb = new_tariff.traffic_limit_gb - user.subscription.device_limit = new_tariff.device_limit + user.subscription.device_limit = calc_device_limit_on_tariff_switch( + current_device_limit=user.subscription.device_limit, + old_tariff_device_limit=current_tariff.device_limit if current_tariff else None, + new_tariff_device_limit=new_tariff.device_limit, + max_device_limit=new_tariff.max_device_limit, + ) user.subscription.connected_squads = new_tariff.allowed_squads or [] # Reset purchased traffic and delete TrafficPurchase records on tariff switch @@ -4516,6 +4527,7 @@ async def toggle_subscription_pause( was_disabled = user.subscription.status in ( SubscriptionStatus.DISABLED.value, SubscriptionStatus.EXPIRED.value, + SubscriptionStatus.LIMITED.value, ) # System-DISABLED subs (insufficient balance) should always be treated as needing resume, diff --git a/app/cabinet/schemas/subscription.py b/app/cabinet/schemas/subscription.py index 83e505a0..6b2d38e6 100644 --- a/app/cabinet/schemas/subscription.py +++ b/app/cabinet/schemas/subscription.py @@ -48,6 +48,7 @@ class SubscriptionData(BaseModel): hide_subscription_link: bool = False # Скрывать ли отображение ссылки (но кнопки работают) is_active: bool is_expired: bool + is_limited: bool = False traffic_purchases: list[TrafficPurchaseInfo] = [] # Daily tariff fields is_daily: bool = False diff --git a/app/cabinet/schemas/users.py b/app/cabinet/schemas/users.py index 02822dc4..84482acc 100644 --- a/app/cabinet/schemas/users.py +++ b/app/cabinet/schemas/users.py @@ -22,6 +22,7 @@ class SubscriptionStatusEnum(StrEnum): ACTIVE = 'active' EXPIRED = 'expired' DISABLED = 'disabled' + LIMITED = 'limited' PENDING = 'pending' diff --git a/app/database/crud/subscription.py b/app/database/crud/subscription.py index adf3c12d..f209a624 100644 --- a/app/database/crud/subscription.py +++ b/app/database/crud/subscription.py @@ -38,6 +38,31 @@ def is_recently_updated_by_webhook(subscription: Subscription) -> bool: return elapsed < _WEBHOOK_GUARD_SECONDS +def calc_device_limit_on_tariff_switch( + current_device_limit: int | None, + old_tariff_device_limit: int | None, + new_tariff_device_limit: int | None, + max_device_limit: int | None = None, +) -> int: + """Calculate device_limit preserving extra purchased devices when switching tariffs. + + Extra devices = current_device_limit - old_tariff_device_limit (clamped to 0). + Result = new_tariff_device_limit + extra_devices, capped at max_device_limit. + """ + old_base = old_tariff_device_limit if old_tariff_device_limit is not None else 0 + current = current_device_limit if current_device_limit is not None else old_base + extra = max(0, current - old_base) + + new_base = new_tariff_device_limit if new_tariff_device_limit is not None else 1 + total = new_base + extra + + effective_max = max_device_limit or (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None) + if effective_max and total > effective_max: + total = effective_max + + return total + + def is_active_paid_subscription(subscription: Subscription | None) -> bool: """Return True if subscription is active, paid (non-trial), and not expired.""" if not subscription: @@ -389,6 +414,7 @@ async def extend_subscription( was_expired = subscription.status in ( SubscriptionStatus.EXPIRED.value, SubscriptionStatus.DISABLED.value, + SubscriptionStatus.LIMITED.value, ) or (subscription.end_date is not None and subscription.end_date <= current_time) if is_tariff_change: @@ -445,6 +471,7 @@ async def extend_subscription( if days > 0 and subscription.status in ( SubscriptionStatus.EXPIRED.value, SubscriptionStatus.DISABLED.value, + SubscriptionStatus.LIMITED.value, ): previous_status = subscription.status subscription.status = SubscriptionStatus.ACTIVE.value @@ -780,7 +807,11 @@ async def reactivate_subscription(db: AsyncSession, subscription: Subscription) now = datetime.now(UTC) # Тихо выходим если реактивация не нужна (уже активна или другой статус) - reactivatable_statuses = {SubscriptionStatus.DISABLED.value, SubscriptionStatus.EXPIRED.value} + reactivatable_statuses = { + SubscriptionStatus.DISABLED.value, + SubscriptionStatus.EXPIRED.value, + SubscriptionStatus.LIMITED.value, + } if subscription.status not in reactivatable_statuses: return subscription @@ -2215,8 +2246,12 @@ async def resume_daily_subscription( subscription.is_daily_paused = False - # Восстанавливаем статус ACTIVE если подписка была DISABLED/EXPIRED - if subscription.status in (SubscriptionStatus.DISABLED.value, SubscriptionStatus.EXPIRED.value): + # Восстанавливаем статус ACTIVE если подписка была DISABLED/EXPIRED/LIMITED + if subscription.status in ( + SubscriptionStatus.DISABLED.value, + SubscriptionStatus.EXPIRED.value, + SubscriptionStatus.LIMITED.value, + ): previous_status = subscription.status subscription.status = SubscriptionStatus.ACTIVE.value # Обновляем время последнего списания для корректного расчёта следующего diff --git a/app/database/models.py b/app/database/models.py index 49702a1e..dfd14e26 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -123,6 +123,7 @@ class SubscriptionStatus(Enum): ACTIVE = 'active' EXPIRED = 'expired' DISABLED = 'disabled' + LIMITED = 'limited' PENDING = 'pending' @@ -1350,6 +1351,9 @@ class Subscription(Base): if self.status == SubscriptionStatus.DISABLED.value: return 'disabled' + if self.status == SubscriptionStatus.LIMITED.value: + return 'limited' + if self.status == SubscriptionStatus.ACTIVE.value: if end is None or end <= current_time: return 'expired' @@ -1374,6 +1378,8 @@ class Subscription(Base): return '🟢 Активна' if actual_status == 'disabled': return '⚫ Отключена' + if actual_status == 'limited': + return '⚠️ Трафик исчерпан' if actual_status == 'trial': return '🎯 Тестовая' @@ -1391,6 +1397,8 @@ class Subscription(Base): return '💎' if actual_status == 'disabled': return '⚫' + if actual_status == 'limited': + return '⚠️' if actual_status == 'trial': return '🎁' @@ -1439,7 +1447,7 @@ class Subscription(Base): else: self.end_date = datetime.now(UTC) + timedelta(days=days) - if self.status == SubscriptionStatus.EXPIRED.value: + if self.status in (SubscriptionStatus.EXPIRED.value, SubscriptionStatus.LIMITED.value): self.status = SubscriptionStatus.ACTIVE.value def add_traffic(self, gb: int): diff --git a/app/handlers/admin/messages.py b/app/handlers/admin/messages.py index 174f0556..bea36071 100644 --- a/app/handlers/admin/messages.py +++ b/app/handlers/admin/messages.py @@ -1557,7 +1557,11 @@ async def get_target_users_count(db: AsyncSession, target: str) -> int: if target == 'expired': # Истекшие подписки now = datetime.now(UTC) - expired_statuses = [SubscriptionStatus.EXPIRED.value, SubscriptionStatus.DISABLED.value] + expired_statuses = [ + SubscriptionStatus.EXPIRED.value, + SubscriptionStatus.DISABLED.value, + SubscriptionStatus.LIMITED.value, + ] query = ( select(sql_func.count(distinct(User.id))) .outerjoin(Subscription, User.id == Subscription.user_id) @@ -1576,7 +1580,11 @@ async def get_target_users_count(db: AsyncSession, target: str) -> int: if target == 'expired_subscribers': # То же что и expired now = datetime.now(UTC) - expired_statuses = [SubscriptionStatus.EXPIRED.value, SubscriptionStatus.DISABLED.value] + expired_statuses = [ + SubscriptionStatus.EXPIRED.value, + SubscriptionStatus.DISABLED.value, + SubscriptionStatus.LIMITED.value, + ] query = ( select(sql_func.count(distinct(User.id))) .outerjoin(Subscription, User.id == Subscription.user_id) diff --git a/app/handlers/menu.py b/app/handlers/menu.py index 40d74296..41563956 100644 --- a/app/handlers/menu.py +++ b/app/handlers/menu.py @@ -1089,6 +1089,9 @@ def _get_subscription_status(user: User, texts, is_daily_tariff: bool = False) - if actual_status == 'disabled': return texts.t('SUB_STATUS_DISABLED', '⚫ Отключена') + if actual_status == 'limited': + return texts.t('SUB_STATUS_LIMITED', '⚠️ Трафик исчерпан') + if actual_status == 'expired': return texts.t( 'SUB_STATUS_EXPIRED', diff --git a/app/handlers/start.py b/app/handlers/start.py index 13cc4220..92230351 100644 --- a/app/handlers/start.py +++ b/app/handlers/start.py @@ -1897,6 +1897,9 @@ def _get_subscription_status(user, texts): if actual_status == 'disabled': return texts.t('SUB_STATUS_DISABLED', '⚫ Отключена') + if actual_status == 'limited': + return texts.t('SUB_STATUS_LIMITED', '⚠️ Трафик исчерпан') + if actual_status == 'pending': return texts.t('SUB_STATUS_PENDING', '⏳ Ожидает активации') diff --git a/app/handlers/subscription/purchase.py b/app/handlers/subscription/purchase.py index 1b7f7cf6..9aed816e 100644 --- a/app/handlers/subscription/purchase.py +++ b/app/handlers/subscription/purchase.py @@ -209,7 +209,11 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d current_time = datetime.now(UTC) - if subscription.status == 'disabled': + if subscription.status == 'limited': + actual_status = 'limited' + status_display = texts.t('SUBSCRIPTION_STATUS_LIMITED', 'Трафик исчерпан') + status_emoji = '⚠️' + elif subscription.status == 'disabled': actual_status = 'disabled' status_display = texts.t('SUBSCRIPTION_STATUS_DISABLED', 'Приостановлена') status_emoji = '⏸️' @@ -3216,7 +3220,11 @@ async def handle_toggle_daily_subscription_pause(callback: types.CallbackQuery, from app.database.models import SubscriptionStatus was_paused = getattr(subscription, 'is_daily_paused', False) - is_inactive = subscription.status in (SubscriptionStatus.DISABLED.value, SubscriptionStatus.EXPIRED.value) + is_inactive = subscription.status in ( + SubscriptionStatus.DISABLED.value, + SubscriptionStatus.EXPIRED.value, + SubscriptionStatus.LIMITED.value, + ) needs_resume = was_paused or is_inactive # При возобновлении проверяем баланс diff --git a/app/handlers/subscription/tariff_purchase.py b/app/handlers/subscription/tariff_purchase.py index 990e0170..5efb27e1 100644 --- a/app/handlers/subscription/tariff_purchase.py +++ b/app/handlers/subscription/tariff_purchase.py @@ -1326,14 +1326,20 @@ async def confirm_daily_tariff_purchase( if existing_subscription: # Обновляем существующую подписку на суточный тариф - # Сохраняем докупленные устройства при продлении того же тарифа - if existing_subscription.tariff_id == tariff.id: - effective_device_limit = max(tariff.device_limit or 0, existing_subscription.device_limit or 0) - else: - effective_device_limit = tariff.device_limit + # Сохраняем докупленные устройства при смене тарифа + from app.database.crud.subscription import calc_device_limit_on_tariff_switch + + old_tariff = ( + await get_tariff_by_id(db, existing_subscription.tariff_id) if existing_subscription.tariff_id else None + ) existing_subscription.tariff_id = tariff.id existing_subscription.traffic_limit_gb = tariff.traffic_limit_gb - existing_subscription.device_limit = effective_device_limit + existing_subscription.device_limit = calc_device_limit_on_tariff_switch( + current_device_limit=existing_subscription.device_limit, + old_tariff_device_limit=old_tariff.device_limit if old_tariff else None, + new_tariff_device_limit=tariff.device_limit, + max_device_limit=getattr(tariff, 'max_device_limit', None), + ) existing_subscription.connected_squads = squads existing_subscription.status = 'active' existing_subscription.is_trial = False # Сбрасываем триальный статус @@ -2450,14 +2456,18 @@ async def confirm_daily_tariff_switch( squads = [s.squad_uuid for s in all_servers if s.squad_uuid] # Обновляем подписку на суточный тариф - # Сохраняем докупленные устройства при продлении того же тарифа - if subscription.tariff_id == tariff.id: - effective_device_limit = max(tariff.device_limit or 0, subscription.device_limit or 0) - else: - effective_device_limit = tariff.device_limit + # Сохраняем докупленные устройства при смене тарифа + from app.database.crud.subscription import calc_device_limit_on_tariff_switch + + old_tariff = await get_tariff_by_id(db, subscription.tariff_id) if subscription.tariff_id else None subscription.tariff_id = tariff.id subscription.traffic_limit_gb = tariff.traffic_limit_gb - subscription.device_limit = effective_device_limit + subscription.device_limit = calc_device_limit_on_tariff_switch( + current_device_limit=subscription.device_limit, + old_tariff_device_limit=old_tariff.device_limit if old_tariff else None, + new_tariff_device_limit=tariff.device_limit, + max_device_limit=getattr(tariff, 'max_device_limit', None), + ) subscription.connected_squads = squads subscription.status = 'active' subscription.is_trial = False # Сбрасываем триальный статус @@ -3020,14 +3030,18 @@ async def confirm_instant_switch( is_new_daily = getattr(new_tariff, 'is_daily', False) # Обновляем подписку с новыми параметрами тарифа - # Сохраняем докупленные устройства при продлении того же тарифа - if subscription.tariff_id == new_tariff.id: - effective_device_limit = max(new_tariff.device_limit or 0, subscription.device_limit or 0) - else: - effective_device_limit = new_tariff.device_limit + # Сохраняем докупленные устройства при смене тарифа + from app.database.crud.subscription import calc_device_limit_on_tariff_switch + + old_tariff = await get_tariff_by_id(db, subscription.tariff_id) if subscription.tariff_id else None subscription.tariff_id = new_tariff.id subscription.traffic_limit_gb = new_tariff.traffic_limit_gb - subscription.device_limit = effective_device_limit + subscription.device_limit = calc_device_limit_on_tariff_switch( + current_device_limit=subscription.device_limit, + old_tariff_device_limit=old_tariff.device_limit if old_tariff else None, + new_tariff_device_limit=new_tariff.device_limit, + max_device_limit=getattr(new_tariff, 'max_device_limit', None), + ) subscription.connected_squads = squads # Сбрасываем докупленный трафик при смене тарифа diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 7083e1e9..9b8e8622 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -1146,7 +1146,11 @@ def get_subscription_keyboard( sub_status = getattr(subscription, 'status', None) is_paused = getattr(subscription, 'is_daily_paused', False) - is_inactive = sub_status in (SubscriptionStatus.DISABLED.value, SubscriptionStatus.EXPIRED.value) + is_inactive = sub_status in ( + SubscriptionStatus.DISABLED.value, + SubscriptionStatus.EXPIRED.value, + SubscriptionStatus.LIMITED.value, + ) if is_inactive or is_paused: # Подписка остановлена (системой или пользователем) — показываем «Возобновить» diff --git a/app/services/remnawave_service.py b/app/services/remnawave_service.py index 44a114b0..cb4b9948 100644 --- a/app/services/remnawave_service.py +++ b/app/services/remnawave_service.py @@ -1796,6 +1796,8 @@ class RemnaWaveService: if panel_status == 'ACTIVE' and end_date_utc > current_time: new_status = SubscriptionStatus.ACTIVE.value + elif panel_status == 'LIMITED': + new_status = SubscriptionStatus.LIMITED.value elif panel_status == 'DISABLED': new_status = SubscriptionStatus.DISABLED.value elif end_date_utc <= current_time: @@ -2524,7 +2526,10 @@ class RemnaWaveService: stats['checked'] += 1 user = subscription.user - if subscription.status == SubscriptionStatus.DISABLED.value: + if subscription.status in ( + SubscriptionStatus.DISABLED.value, + SubscriptionStatus.LIMITED.value, + ): continue if user.telegram_id not in panel_telegram_ids: diff --git a/app/services/remnawave_webhook_service.py b/app/services/remnawave_webhook_service.py index 8310960c..aae0456b 100644 --- a/app/services/remnawave_webhook_service.py +++ b/app/services/remnawave_webhook_service.py @@ -530,7 +530,7 @@ class RemnaWaveWebhookService: ) -> None: if subscription: self._stamp_webhook_update(subscription) - if subscription.status == SubscriptionStatus.DISABLED.value: + if subscription.status in (SubscriptionStatus.DISABLED.value, SubscriptionStatus.LIMITED.value): await reactivate_subscription(db, subscription) logger.info( 'Webhook: subscription re-enabled for user', subscription_id=subscription.id, user_id=user.id @@ -545,8 +545,11 @@ class RemnaWaveWebhookService: ) -> None: if subscription: self._stamp_webhook_update(subscription) - if subscription.status == SubscriptionStatus.ACTIVE.value: - await deactivate_subscription(db, subscription) + if subscription.status in (SubscriptionStatus.ACTIVE.value, SubscriptionStatus.TRIAL.value): + subscription.status = SubscriptionStatus.LIMITED.value + subscription.updated_at = datetime.now(UTC) + await db.commit() + await db.refresh(subscription) logger.info( 'Webhook: subscription limited (traffic) for user', subscription_id=subscription.id, user_id=user.id ) @@ -561,8 +564,8 @@ class RemnaWaveWebhookService: if subscription: self._stamp_webhook_update(subscription) await update_subscription_usage(db, subscription, 0.0) - # Re-enable if was disabled due to traffic limit - if subscription.status == SubscriptionStatus.DISABLED.value: + # Re-enable if was disabled/limited due to traffic limit + if subscription.status in (SubscriptionStatus.DISABLED.value, SubscriptionStatus.LIMITED.value): await reactivate_subscription(db, subscription) logger.info( 'Webhook: traffic reset for subscription , user', subscription_id=subscription.id, user_id=user.id diff --git a/app/services/subscription_auto_purchase_service.py b/app/services/subscription_auto_purchase_service.py index 1b6095df..b015f89a 100644 --- a/app/services/subscription_auto_purchase_service.py +++ b/app/services/subscription_auto_purchase_service.py @@ -1023,9 +1023,20 @@ async def _auto_purchase_daily_tariff( # Обновляем существующую подписку на суточный тариф # Суточность определяется через tariff.is_daily, поэтому достаточно установить tariff_id was_trial_conversion = existing_subscription.is_trial # Сохраняем до изменения + from app.database.crud.subscription import calc_device_limit_on_tariff_switch + from app.database.crud.tariff import get_tariff_by_id as _get_old_tariff + + old_tariff = ( + await _get_old_tariff(db, existing_subscription.tariff_id) if existing_subscription.tariff_id else None + ) existing_subscription.tariff_id = tariff.id existing_subscription.traffic_limit_gb = tariff.traffic_limit_gb - existing_subscription.device_limit = tariff.device_limit + existing_subscription.device_limit = calc_device_limit_on_tariff_switch( + current_device_limit=existing_subscription.device_limit, + old_tariff_device_limit=old_tariff.device_limit if old_tariff else None, + new_tariff_device_limit=tariff.device_limit, + max_device_limit=getattr(tariff, 'max_device_limit', None), + ) existing_subscription.connected_squads = squads existing_subscription.status = 'active' existing_subscription.is_trial = False @@ -1244,7 +1255,7 @@ async def _auto_add_devices( await user_cart_service.delete_user_cart(user.id) return False - if subscription.status not in ('active', 'trial', 'disabled', 'ACTIVE', 'TRIAL', 'DISABLED'): + if subscription.status not in ('active', 'trial', 'disabled', 'limited', 'ACTIVE', 'TRIAL', 'DISABLED', 'LIMITED'): logger.warning( '🔁 Автопокупка устройств: подписка пользователя не активна (status=)', format_user_id=_format_user_id(user), @@ -1513,7 +1524,7 @@ async def _auto_add_traffic( await user_cart_service.delete_user_cart(user.id) return False - if subscription.status not in ('active', 'trial', 'disabled', 'ACTIVE', 'TRIAL', 'DISABLED'): + if subscription.status not in ('active', 'trial', 'disabled', 'limited', 'ACTIVE', 'TRIAL', 'DISABLED', 'LIMITED'): logger.warning( '🔁 Автопокупка трафика: подписка пользователя не активна (status=)', format_user_id=_format_user_id(user), @@ -2016,8 +2027,12 @@ async def try_resume_disabled_daily_after_topup( if subscription is None: return False - # Only handle DISABLED (or EXPIRED) daily tariff subscriptions - if subscription.status not in (SubscriptionStatus.DISABLED.value, SubscriptionStatus.EXPIRED.value): + # Only handle DISABLED/LIMITED (or EXPIRED) daily tariff subscriptions + if subscription.status not in ( + SubscriptionStatus.DISABLED.value, + SubscriptionStatus.EXPIRED.value, + SubscriptionStatus.LIMITED.value, + ): return False if not getattr(subscription, 'is_daily_tariff', False): return False diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index cf9bc688..4bfdcfb0 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -4815,6 +4815,11 @@ def _ensure_paid_subscription( 'code': 'subscription_disabled', 'message': 'Subscription is disabled', } + elif actual_status == 'limited': + detail = { + 'code': 'traffic_exhausted', + 'message': 'Traffic limit reached. Please purchase additional traffic.', + } else: detail = { 'code': 'subscription_inactive', @@ -7082,9 +7087,16 @@ async def switch_tariff_endpoint( squads = [s.squad_uuid for s in all_servers if s.squad_uuid] # Обновляем подписку - меняем тариф без изменения даты + from app.database.crud.subscription import calc_device_limit_on_tariff_switch + subscription.tariff_id = new_tariff.id subscription.traffic_limit_gb = new_tariff.traffic_limit_gb - subscription.device_limit = new_tariff.device_limit + subscription.device_limit = calc_device_limit_on_tariff_switch( + current_device_limit=subscription.device_limit, + old_tariff_device_limit=current_tariff.device_limit if current_tariff else None, + new_tariff_device_limit=new_tariff.device_limit, + max_device_limit=new_tariff.max_device_limit, + ) subscription.connected_squads = squads # Сбрасываем докупленный трафик при смене тарифа from sqlalchemy import delete as sql_delete @@ -7389,6 +7401,7 @@ async def toggle_daily_subscription_pause_endpoint( was_disabled = subscription.status in ( SubscriptionStatus.DISABLED.value, SubscriptionStatus.EXPIRED.value, + SubscriptionStatus.LIMITED.value, ) # System-DISABLED subs (is_daily_paused=False) должны идти по пути resume