234 lines
14 KiB
Python
234 lines
14 KiB
Python
"""
|
||
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
|
||
|