454 lines
16 KiB
Python
454 lines
16 KiB
Python
"""
|
||
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
|
||
|
||
# Уведомление рефереру: по вашей ссылке зарегистрировался новый пользователь
|
||
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)
|
||
|