""" Middleware для проверки подписки и ограничений. """ from django.http import JsonResponse from django.utils.deprecation import MiddlewareMixin from django.urls import resolve import logging logger = logging.getLogger(__name__) class SubscriptionMiddleware(MiddlewareMixin): """ Middleware для проверки подписки и ограничений по ученикам. Проверяет: - Наличие активной подписки - Лимиты по количеству учеников (для типа "за ученика") - Лимиты по функционалу """ # URL-пути, которые не требуют проверки подписки EXEMPT_PATHS = [ '/api/auth/', '/api/subscriptions/', '/admin/', '/api/docs/', ] # URL-пути, которые требуют проверки лимита учеников STUDENT_LIMIT_PATHS = [ '/api/clients/', '/api/users/clients/', ] def process_view(self, request, view_func, view_args, view_kwargs): """ Обработка запроса ПЕРЕД вызовом view. Используем process_view вместо process_request, чтобы быть уверенными, что AuthenticationMiddleware уже обработал запрос. """ # Логируем ВСЕ POST/PUT/PATCH/DELETE запросы для отладки (уровень debug, не error) if request.method in ['POST', 'PUT', 'PATCH', 'DELETE']: logger.debug( f"🔍 SubscriptionMiddleware.process_view: {request.method} {request.path}" ) # Пропускаем exempt пути (это нормальное поведение, не ошибка) if any(request.path.startswith(path) for path in self.EXEMPT_PATHS): # EXEMPT пути - это нормальное поведение, логируем только на уровне debug logger.debug(f"SubscriptionMiddleware: EXEMPT path {request.path} (skipping subscription check)") return None # Проверяем наличие атрибута user if not hasattr(request, 'user'): # Это может быть нормальным для некоторых путей, логируем debug logger.debug(f"SubscriptionMiddleware: no user attribute for {request.path}") return None # Проверяем только для аутентифицированных пользователей if not request.user.is_authenticated: # Для неаутентифицированных пользователей пропускаем (DRF сам вернет 401) logger.debug(f"SubscriptionMiddleware: user not authenticated for {request.path}") return None # Проверяем только для менторов if not hasattr(request.user, 'role') or request.user.role != 'mentor': # Для не-менторов пропускаем (это нормальное поведение) logger.debug(f"SubscriptionMiddleware: user is not mentor (role={getattr(request.user, 'role', None)}) for {request.path}") return None # Логируем начало проверки для всех POST/PUT/PATCH/DELETE запросов (уровень debug) if request.method in ['POST', 'PUT', 'PATCH', 'DELETE']: logger.debug( f"SubscriptionMiddleware: Checking subscription for {request.method} {request.path}, " f"user={request.user.id} ({request.user.email}), role={request.user.role}" ) # Получаем активную подписку from .services import SubscriptionService from .models import Subscription from django.utils import timezone # Получаем все подписки пользователя (включая истекшие для диагностики) all_subscriptions = Subscription.objects.filter( user=request.user, status__in=['trial', 'active'] ).order_by('-end_date') # Также проверяем истекшие подписки для диагностики expired_subscriptions = Subscription.objects.filter( user=request.user, status__in=['expired', 'past_due', 'cancelled'] ).order_by('-end_date') # Проверяем, есть ли действительно активная подписка active_subscription = None for sub in all_subscriptions: if sub.is_active(): active_subscription = sub break # Логируем информацию о всех подписках для диагностики if not active_subscription and (all_subscriptions.exists() or expired_subscriptions.exists()): logger.warning( f"SubscriptionMiddleware: user={request.user.id} has subscriptions but none are active. " f"Active status subscriptions: {all_subscriptions.count()}, " f"Expired status subscriptions: {expired_subscriptions.count()}" ) if expired_subscriptions.exists(): expired = expired_subscriptions.first() logger.warning( f"Latest expired subscription: id={expired.id}, status={expired.status}, " f"end_date={expired.end_date}, is_active={expired.is_active()}" ) # Логируем для отладки logger.info( f"SubscriptionMiddleware: path={request.path}, method={request.method}, " f"user={request.user.id}, has_active_subscription={active_subscription is not None}, " f"total_subscriptions={all_subscriptions.count()}" ) # Если нет активной подписки, блокируем все операции изменения данных if not active_subscription: # Разрешаем доступ к некоторым путям (подписки, авторизация, админка) if request.path.startswith('/api/subscriptions/'): return None # Блокируем все операции изменения данных (POST, PUT, PATCH, DELETE) if request.method in ['POST', 'PUT', 'PATCH', 'DELETE']: # Получаем информацию о подписке для сообщения об ошибке from apps.users.models import Client current_clients_count = Client.objects.filter(mentors=request.user).count() # Если подписка есть, но истекла (проверяем истекшие подписки) expired_subscription = expired_subscriptions.first() if expired_subscriptions.exists() else (all_subscriptions.first() if all_subscriptions.exists() else None) if expired_subscription: logger.error( f"❌ BLOCKING REQUEST: path={request.path}, method={request.method}, " f"user={request.user.id} ({request.user.email}) - subscription expired (id={expired_subscription.id}, " f"status={expired_subscription.status}, end_date={expired_subscription.end_date}, " f"is_active={expired_subscription.is_active()})" ) response_data = { 'error': 'Подписка истекла', 'detail': 'Ваша подписка истекла. Для продолжения работы необходимо продлить подписку.', 'subscription_id': expired_subscription.id, 'end_date': expired_subscription.end_date.strftime('%Y-%m-%d') if expired_subscription.end_date else None, 'current_students': current_clients_count, 'paid_students': expired_subscription.student_count if expired_subscription.plan.subscription_type == 'per_student' else None, 'requires_renewal': True } response = JsonResponse(response_data, status=403) logger.error(f"✅ Returning 403 response for {request.path} with data: {response_data}") return response else: # Если подписки нет вообще logger.error( f"❌ BLOCKING REQUEST: path={request.path}, method={request.method}, " f"user={request.user.id} - no active subscription" ) response = JsonResponse( { 'error': 'Требуется активная подписка', 'detail': 'Для использования платформы необходимо оформить подписку' }, status=403 ) logger.error(f"✅ Returning 403 response for {request.path}") return response # Для GET запросов разрешаем только чтение (можно показать данные, но не редактировать) return None # Проверяем лимит учеников для типа "за ученика" if active_subscription.plan.subscription_type == 'per_student': # Проверяем только для путей, связанных с клиентами if any(request.path.startswith(path) for path in self.STUDENT_LIMIT_PATHS): if request.method in ['POST']: # При создании нового клиента from apps.users.models import Client current_clients_count = Client.objects.filter( mentor=request.user ).count() # Разрешаем добавление, но отслеживаем превышение # Если превышен лимит оплаченных учеников, обновляем информацию о неоплаченных if current_clients_count >= active_subscription.student_count: # Рассчитываем количество неоплаченных учеников unpaid_count = current_clients_count - active_subscription.student_count + 1 # +1 потому что добавляем нового # Обновляем подписку с информацией о неоплаченных учениках active_subscription.unpaid_students_count = unpaid_count # Рассчитываем доплату from .services import SubscriptionService payment_data = SubscriptionService.calculate_extra_students_payment( subscription=active_subscription, new_student_count=current_clients_count + 1 ) active_subscription.pending_payment_amount = payment_data['payment_amount'] active_subscription.save(update_fields=['unpaid_students_count', 'pending_payment_amount']) # Разрешаем добавление, но возвращаем информацию о необходимости доплаты # Это будет обработано на фронтенде для показа модального окна # Не блокируем запрос, но добавляем заголовок с информацией # Фактически, мы разрешаем создание, но фронтенд должен проверить ответ # и показать модальное окно pass # Разрешаем создание, фронтенд обработает через API # Проверяем лимиты для ежемесячной подписки else: if any(request.path.startswith(path) for path in self.STUDENT_LIMIT_PATHS): if request.method in ['POST']: if not active_subscription.check_limit('clients'): max_clients = active_subscription.plan.max_clients from apps.users.models import Client current_clients_count = Client.objects.filter( mentor=request.user ).count() return JsonResponse( { 'error': 'Достигнут лимит учеников', 'detail': f'Ваш тарифный план позволяет добавить максимум {max_clients} учеников.', 'current_count': current_clients_count, 'max_count': max_clients }, status=403 ) return None