231 lines
8.9 KiB
Python
231 lines
8.9 KiB
Python
"""
|
||
API views для промокодов.
|
||
"""
|
||
from rest_framework import status
|
||
from rest_framework.decorators import api_view, permission_classes
|
||
from rest_framework.response import Response
|
||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||
from .services import PromoCodeService
|
||
from .models import PromoCode
|
||
from rest_framework import serializers
|
||
|
||
|
||
class PromoCodeValidateSerializer(serializers.Serializer):
|
||
"""Сериализатор валидации промокода."""
|
||
code = serializers.CharField(max_length=50, required=True)
|
||
plan_id = serializers.IntegerField(required=False)
|
||
student_count = serializers.IntegerField(required=False, default=0, min_value=0)
|
||
duration_days = serializers.IntegerField(required=False, default=30, min_value=1)
|
||
|
||
|
||
@api_view(['POST'])
|
||
@permission_classes([AllowAny])
|
||
def validate_promo_code(request):
|
||
"""
|
||
Валидация промокода и расчет скидки.
|
||
|
||
POST /api/subscriptions/promo-codes/validate/
|
||
Body: {
|
||
"code": "PROMO2024",
|
||
"plan_id": 1,
|
||
"student_count": 5,
|
||
"duration_days": 30
|
||
}
|
||
"""
|
||
serializer = PromoCodeValidateSerializer(data=request.data)
|
||
serializer.is_valid(raise_exception=True)
|
||
|
||
code = serializer.validated_data['code']
|
||
plan_id = serializer.validated_data.get('plan_id')
|
||
student_count = serializer.validated_data.get('student_count', 0)
|
||
duration_days = serializer.validated_data.get('duration_days', 30)
|
||
|
||
# Валидация промокода
|
||
user = request.user if request.user.is_authenticated else None
|
||
promo_result = PromoCodeService.validate_promo_code(code, user)
|
||
|
||
if not promo_result['valid']:
|
||
return Response(
|
||
{
|
||
'valid': False,
|
||
'error': promo_result['error']
|
||
},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
promo_code = promo_result['promo_code']
|
||
|
||
# Если указан план, рассчитываем скидку
|
||
if plan_id:
|
||
from .models import SubscriptionPlan
|
||
try:
|
||
plan = SubscriptionPlan.objects.select_related().get(id=plan_id, is_active=True)
|
||
price_data = plan.calculate_price(
|
||
student_count=student_count,
|
||
duration_days=duration_days,
|
||
promo_code=promo_code
|
||
)
|
||
|
||
return Response({
|
||
'valid': True,
|
||
'promo_code': {
|
||
'code': promo_code.code,
|
||
'discount_type': promo_code.discount_type,
|
||
'discount_value': float(promo_code.discount_value),
|
||
},
|
||
'price': {
|
||
'original_amount': float(price_data['original_amount']),
|
||
'discount_amount': float(price_data['discount_amount']),
|
||
'final_amount': float(price_data['final_amount']),
|
||
}
|
||
})
|
||
except SubscriptionPlan.DoesNotExist:
|
||
return Response(
|
||
{
|
||
'valid': True,
|
||
'promo_code': {
|
||
'code': promo_code.code,
|
||
'discount_type': promo_code.discount_type,
|
||
'discount_value': float(promo_code.discount_value),
|
||
},
|
||
'price': None
|
||
}
|
||
)
|
||
|
||
# Если план не указан, просто возвращаем информацию о промокоде
|
||
return Response({
|
||
'valid': True,
|
||
'promo_code': {
|
||
'code': promo_code.code,
|
||
'discount_type': promo_code.discount_type,
|
||
'discount_value': float(promo_code.discount_value),
|
||
}
|
||
})
|
||
|
||
|
||
@api_view(['GET'])
|
||
@permission_classes([IsAuthenticated])
|
||
def calculate_price(request):
|
||
"""
|
||
Расчет цены подписки с учетом промокода.
|
||
|
||
GET /api/subscriptions/calculate-price/?plan_id=1&student_count=5&duration_days=30&promo_code=PROMO2024
|
||
"""
|
||
from .models import SubscriptionPlan
|
||
from .services import SubscriptionService
|
||
|
||
plan_id = request.query_params.get('plan_id')
|
||
student_count = int(request.query_params.get('student_count', 0))
|
||
duration_days = int(request.query_params.get('duration_days', 30))
|
||
promo_code_str = request.query_params.get('promo_code')
|
||
remaining_days = request.query_params.get('remaining_days')
|
||
is_renewal = request.query_params.get('is_renewal') == 'true'
|
||
is_add_students = request.query_params.get('is_add_students') == 'true'
|
||
current_student_count = request.query_params.get('current_student_count')
|
||
|
||
if not plan_id:
|
||
return Response(
|
||
{'error': 'Необходимо указать plan_id'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
try:
|
||
plan = SubscriptionPlan.objects.select_related().get(id=plan_id, is_active=True)
|
||
except SubscriptionPlan.DoesNotExist:
|
||
return Response(
|
||
{'error': 'Тарифный план не найден'},
|
||
status=status.HTTP_404_NOT_FOUND
|
||
)
|
||
|
||
# Проверяем доступность периода
|
||
if not plan.is_duration_available(duration_days):
|
||
available = plan.get_available_durations()
|
||
return Response(
|
||
{'error': f'Период {duration_days} дней недоступен для этого тарифа. Доступные периоды: {", ".join(map(str, available))}'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# Валидация промокода если указан
|
||
promo_code = None
|
||
if promo_code_str:
|
||
promo_result = PromoCodeService.validate_promo_code(promo_code_str, request.user)
|
||
if promo_result['valid']:
|
||
promo_code = promo_result['promo_code']
|
||
else:
|
||
return Response(
|
||
{'error': promo_result['error']},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# Если это продление и есть оставшиеся дни, учитываем их
|
||
remaining_days_int = None
|
||
if (is_renewal or is_add_students) and remaining_days:
|
||
try:
|
||
remaining_days_int = int(remaining_days)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# Получаем текущее количество учеников для расчета доплаты
|
||
current_student_count_int = 0
|
||
if current_student_count:
|
||
try:
|
||
current_student_count_int = int(current_student_count)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
elif is_renewal and request.user.is_authenticated:
|
||
# Если не передано, пытаемся получить из активной подписки
|
||
from .models import Subscription
|
||
try:
|
||
subscription = Subscription.objects.filter(
|
||
user=request.user,
|
||
plan=plan,
|
||
status__in=['active', 'trial']
|
||
).select_related('plan').order_by('-end_date').first()
|
||
if subscription:
|
||
current_student_count_int = subscription.student_count or 0
|
||
except:
|
||
pass
|
||
|
||
# Рассчитываем цену
|
||
price_data = SubscriptionService.calculate_subscription_price(
|
||
plan=plan,
|
||
student_count=student_count,
|
||
duration_days=duration_days,
|
||
promo_code=promo_code,
|
||
remaining_days=remaining_days_int,
|
||
is_add_students=is_add_students,
|
||
current_student_count=current_student_count_int
|
||
)
|
||
|
||
response_data = {
|
||
'original_amount': float(price_data['original_amount']),
|
||
'discount_amount': float(price_data['discount_amount']),
|
||
'final_amount': float(price_data['final_amount']),
|
||
}
|
||
|
||
# Добавляем информацию о доплате за новых учеников
|
||
if 'extra_students_payment' in price_data:
|
||
response_data['extra_students_payment'] = price_data['extra_students_payment']
|
||
response_data['extra_students_count'] = price_data.get('extra_students_count', 0)
|
||
response_data['remaining_days_payment'] = price_data.get('remaining_days_payment', 0)
|
||
|
||
# Добавляем информацию о скидке за длительность
|
||
if 'duration_discount' in price_data:
|
||
response_data['duration_discount'] = price_data['duration_discount']
|
||
|
||
# Добавляем дату окончания подписки
|
||
if 'end_date' in price_data:
|
||
response_data['end_date'] = price_data['end_date']
|
||
if 'end_date_display' in price_data:
|
||
response_data['end_date_display'] = price_data['end_date_display']
|
||
|
||
if promo_code:
|
||
response_data['promo_code'] = {
|
||
'code': promo_code.code,
|
||
'discount_type': promo_code.discount_type,
|
||
'discount_value': float(promo_code.discount_value),
|
||
}
|
||
|
||
return Response(response_data)
|
||
|