uchill/backend/apps/subscriptions/middleware.py

234 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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