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