uchill/backend/apps/referrals/views.py

484 lines
18 KiB
Python
Raw Permalink 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 django.utils import timezone
from decimal import Decimal
from .models import (
ReferralLevel,
ReferralSettings,
UserReferralProfile,
BonusAccount,
ReferralEarning,
PointsTransaction,
BonusTransaction,
PromoCode,
PromoCodeUsage,
ReferralInvitedEmail,
PendingReferralBonus,
)
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
)
# Защита от накрутки: email уже был приглашён ранее (бэклог)
email_lower = request.user.email.lower().strip()
if ReferralInvitedEmail.objects.filter(email=email_lower).exists():
return Response(
{'error': 'Этот email уже был приглашён ранее'},
status=status.HTTP_400_BAD_REQUEST
)
# Устанавливаем реферера
profile.referred_by = referrer_profile.user
profile.save()
# Добавляем email в бэклог приглашённых
ReferralInvitedEmail.objects.create(
email=email_lower,
referrer=referrer_profile.user,
referred_user=request.user,
)
# Обновляем счётчик рефералов (без начисления очков — очки начисляются после проверки активности)
settings_obj = ReferralSettings.get_settings()
referrer_profile.direct_referrals_count += 1
referrer_profile.save(update_fields=['direct_referrals_count'])
# Очки начисляются отложенно: через 30 дней при 20+ днях активности реферала или при 21 дне активности
now = timezone.now()
PendingReferralBonus.objects.create(
referrer=referrer_profile.user,
referred_user=request.user,
referred_at=now,
points=settings_obj.points_direct_referral,
level=1,
reason=f'Регистрация реферала {request.user.email}',
status=PendingReferralBonus.STATUS_PENDING,
)
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'])
PendingReferralBonus.objects.create(
referrer=referrer_profile.referred_by,
referred_user=request.user,
referred_at=now,
points=settings_obj.points_indirect_referral,
level=2,
reason=f'Регистрация непрямого реферала {request.user.email}',
status=PendingReferralBonus.STATUS_PENDING,
)
except (UserReferralProfile.DoesNotExist, AttributeError):
pass
# Уведомление рефереру: по вашей ссылке зарегистрировался новый пользователь
referrer_user = referrer_profile.user
new_user_name = request.user.get_full_name() or request.user.email or 'Новый пользователь'
try:
from apps.notifications.services import NotificationService
NotificationService.create_notification_with_telegram(
recipient=referrer_user,
notification_type='system',
title='🎉 Новый реферал',
message=f'По вашей реферальной ссылке зарегистрировался {new_user_name}',
priority='normal',
action_url='/referrals',
)
except Exception:
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)