uchill/backend/apps/referrals/views.py

438 lines
16 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.

"""
API views для реферальной системы и промокодов.
"""
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny
from django.db.models import Sum, Q, F
from decimal import Decimal
from .models import (
ReferralLevel,
ReferralSettings,
UserReferralProfile,
BonusAccount,
ReferralEarning,
PointsTransaction,
BonusTransaction,
PromoCode,
PromoCodeUsage,
)
from .serializers import (
ReferralLevelSerializer,
UserReferralProfileSerializer,
BonusAccountSerializer,
ReferralEarningSerializer,
PointsTransactionSerializer,
BonusTransactionSerializer,
PromoCodeSerializer,
PromoCodeUsageSerializer,
SetReferrerSerializer,
PromoCodeValidationSerializer,
ReferralSettingsSerializer
)
class ReferralViewSet(viewsets.ViewSet):
"""
ViewSet для реферальной системы.
"""
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def my_profile(self, request):
"""
Получить свой реферальный профиль.
GET /api/referrals/my_profile/
"""
profile, created = UserReferralProfile.objects.get_or_create(
user=request.user
)
serializer = UserReferralProfileSerializer(
profile,
context={'request': request}
)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def levels(self, request):
"""
Получить все уровни.
GET /api/referrals/levels/
"""
levels = ReferralLevel.objects.all().order_by('level')
serializer = ReferralLevelSerializer(levels, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def referral_settings(self, request):
"""
Получить настройки реферальной программы.
GET /api/referrals/referral_settings/
"""
settings_obj = ReferralSettings.get_settings()
serializer = ReferralSettingsSerializer(settings_obj)
return Response(serializer.data)
@action(detail=False, methods=['post'])
def set_referrer(self, request):
"""
Установить реферера по коду.
POST /api/referrals/set_referrer/
Body: {
"referral_code": "ABC12345"
}
"""
serializer = SetReferrerSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
referral_code = serializer.validated_data['referral_code']
try:
profile = request.user.referral_profile
except UserReferralProfile.DoesNotExist:
return Response(
{'error': 'Реферальный профиль не найден'},
status=status.HTTP_404_NOT_FOUND
)
# Проверяем что реферер еще не установлен
if profile.referred_by:
return Response(
{'error': 'Реферер уже установлен'},
status=status.HTTP_400_BAD_REQUEST
)
# Находим реферера
try:
referrer_profile = UserReferralProfile.objects.get(
referral_code=referral_code
)
except UserReferralProfile.DoesNotExist:
return Response(
{'error': 'Неверный реферальный код'},
status=status.HTTP_404_NOT_FOUND
)
# Проверяем что не ссылаемся на себя
if referrer_profile.user == request.user:
return Response(
{'error': 'Нельзя быть рефералом самого себя'},
status=status.HTTP_400_BAD_REQUEST
)
# Устанавливаем реферера
profile.referred_by = referrer_profile.user
profile.save()
# Обновляем статистику и начисляем очки (сигнал update_referrer_stats
# срабатывает только при created=True, а здесь — update существующего профиля)
settings_obj = ReferralSettings.get_settings()
referrer_profile.direct_referrals_count += 1
referrer_profile.save(update_fields=['direct_referrals_count'])
referrer_profile.add_points(
settings_obj.points_direct_referral,
reason=f'Регистрация реферала {request.user.email}'
)
if referrer_profile.referred_by:
try:
level2_profile = referrer_profile.referred_by.referral_profile
level2_profile.indirect_referrals_count += 1
level2_profile.save(update_fields=['indirect_referrals_count'])
level2_profile.add_points(
settings_obj.points_indirect_referral,
reason=f'Регистрация непрямого реферала {request.user.email}'
)
except (UserReferralProfile.DoesNotExist, AttributeError):
pass
return Response({
'success': True,
'message': f'Реферер установлен: {referrer_profile.user.email}'
})
@action(detail=False, methods=['get'])
def my_referrals(self, request):
"""
Получить своих рефералов.
GET /api/referrals/my_referrals/
"""
# Прямые рефералы
direct_referrals = UserReferralProfile.objects.filter(
referred_by=request.user
).select_related('user', 'current_level', 'referred_by').only(
'id', 'user_id', 'current_level_id', 'referred_by_id', 'total_points', 'created_at'
)
# Непрямые рефералы (рефералы рефералов)
indirect_referrals = UserReferralProfile.objects.filter(
referred_by__referral_profile__referred_by=request.user
).select_related('user', 'current_level', 'referred_by', 'referred_by__referral_profile').only(
'id', 'user_id', 'current_level_id', 'referred_by_id', 'total_points', 'created_at'
)
return Response({
'direct': [
{
'email': r.user.email,
'level': r.current_level.name if r.current_level else 'Новичок',
'total_points': r.total_points,
'created_at': r.created_at
}
for r in direct_referrals
],
'indirect': [
{
'email': r.user.email,
'level': r.current_level.name if r.current_level else 'Новичок',
'total_points': r.total_points,
'created_at': r.created_at
}
for r in indirect_referrals
]
})
@action(detail=False, methods=['get'])
def stats(self, request):
"""
Получить статистику.
GET /api/referrals/stats/
"""
try:
profile = request.user.referral_profile
bonus_account = request.user.bonus_account
except (UserReferralProfile.DoesNotExist, BonusAccount.DoesNotExist):
return Response(
{'error': 'Профиль не найден'},
status=status.HTTP_404_NOT_FOUND
)
# Оптимизация: один запрос для заработков по уровням
earnings_stats = ReferralEarning.objects.filter(
referrer=request.user
).values('level').annotate(
total=Sum('earned_amount')
)
earnings_by_level = {item['level']: item['total'] for item in earnings_stats}
level1_earnings = earnings_by_level.get(1, 0)
level2_earnings = earnings_by_level.get(2, 0)
return Response({
'referral_code': profile.referral_code,
'total_points': profile.total_points,
'current_level': {
'level': profile.current_level.level if profile.current_level else 1,
'name': profile.current_level.name if profile.current_level else 'Новичок',
'bonus_payment_percent': profile.get_max_bonus_payment_percent()
},
'referrals': {
'direct': profile.direct_referrals_count,
'indirect': profile.indirect_referrals_count,
'total': profile.direct_referrals_count + profile.indirect_referrals_count
},
'earnings': {
'level1': float(level1_earnings),
'level2': float(level2_earnings),
'total': float(profile.total_earned)
},
'bonus_account': {
'balance': float(bonus_account.balance),
'total_earned': float(bonus_account.total_earned),
'total_spent': float(bonus_account.total_spent)
}
})
class BonusAccountViewSet(viewsets.ViewSet):
"""
ViewSet для бонусного счета.
"""
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def balance(self, request):
"""
Получить баланс бонусного счета.
GET /api/bonus/balance/
"""
account, created = BonusAccount.objects.get_or_create(user=request.user)
serializer = BonusAccountSerializer(account)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def transactions(self, request):
"""
Получить историю транзакций.
GET /api/bonus/transactions/
"""
transactions = BonusTransaction.objects.filter(
user=request.user
).select_related('user').only(
'id', 'user_id', 'amount', 'transaction_type', 'description', 'created_at'
).order_by('-created_at')[:50]
serializer = BonusTransactionSerializer(transactions, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def earnings(self, request):
"""
Получить историю заработков с рефералов.
GET /api/bonus/earnings/
"""
earnings = ReferralEarning.objects.filter(
referrer=request.user
).select_related('referrer', 'referred').only(
'id', 'referrer_id', 'referred_id', 'level', 'earned_amount', 'created_at'
).order_by('-created_at')[:50]
serializer = ReferralEarningSerializer(earnings, many=True)
return Response(serializer.data)
class PointsViewSet(viewsets.ViewSet):
"""
ViewSet для очков.
"""
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def transactions(self, request):
"""
Получить историю начисления очков.
GET /api/points/transactions/
"""
transactions = PointsTransaction.objects.filter(
user=request.user
).select_related('user').only(
'id', 'user_id', 'points', 'transaction_type', 'description', 'created_at'
).order_by('-created_at')[:50]
serializer = PointsTransactionSerializer(transactions, many=True)
return Response(serializer.data)
class PromoCodeViewSet(viewsets.ViewSet):
"""
ViewSet для промокодов.
"""
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['post'])
def validate(self, request):
"""
Проверить валидность промокода для плана.
POST /api/promo/validate/
Body: {
"code": "PROMO123",
"plan_id": 1
}
"""
serializer = PromoCodeValidationSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
code = serializer.validated_data['code']
plan_id = serializer.validated_data['plan_id']
try:
promo = PromoCode.objects.get(code=code.upper())
except PromoCode.DoesNotExist:
return Response(
{'error': 'Промокод не найден'},
status=status.HTTP_404_NOT_FOUND
)
# Проверяем валидность
is_valid, message = promo.is_valid()
if not is_valid:
return Response(
{'error': message},
status=status.HTTP_400_BAD_REQUEST
)
# Проверяем применимость к плану
from apps.subscriptions.models import SubscriptionPlan
try:
plan = SubscriptionPlan.objects.get(id=plan_id)
except SubscriptionPlan.DoesNotExist:
return Response(
{'error': 'План не найден'},
status=status.HTTP_404_NOT_FOUND
)
if not promo.can_apply_to_plan(plan):
return Response(
{'error': 'Промокод не применим к этому плану'},
status=status.HTTP_400_BAD_REQUEST
)
# Рассчитываем скидку
discount = promo.calculate_discount(plan.price)
final_price = plan.price - discount
return Response({
'valid': True,
'promo_code': {
'code': promo.code,
'name': promo.name,
'description': promo.description,
'discount_type': promo.discount_type,
'discount_value': float(promo.discount_value)
},
'original_price': float(plan.price),
'discount': float(discount),
'final_price': float(final_price)
})
@action(detail=False, methods=['get'])
def my_usage(self, request):
"""
Получить историю использования промокодов.
GET /api/promo/my_usage/
"""
usages = PromoCodeUsage.objects.filter(
user=request.user
).select_related('promo_code', 'user').only(
'id', 'promo_code_id', 'user_id', 'discount_amount', 'created_at'
).order_by('-created_at')[:20]
serializer = PromoCodeUsageSerializer(usages, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'], permission_classes=[AllowAny])
def public(self, request):
"""
Получить активные публичные промокоды.
GET /api/promo/public/
"""
from django.utils import timezone
promos = PromoCode.objects.filter(
is_active=True
).filter(
Q(valid_until__isnull=True) | Q(valid_until__gt=timezone.now())
).filter(
Q(max_uses__isnull=True) | Q(current_uses__lt=F('max_uses'))
).only(
'id', 'code', 'name', 'description', 'discount_type', 'discount_value',
'valid_from', 'valid_until', 'max_uses', 'current_uses', 'is_active'
)[:10]
serializer = PromoCodeSerializer(promos, many=True)
return Response(serializer.data)