from django.shortcuts import get_object_or_404 from drf_spectacular.utils import extend_schema from rest_framework.exceptions import ValidationError from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.throttling import ScopedRateThrottle from rest_framework.views import APIView from .models import AffiliateAccount, Conversion from .serializers import ( AffiliateMeResponseSerializer, ConversionListResponseSerializer, TrackRequestSerializer, TrackResponseSerializer, ) class GuestAwareAffiliateThrottle(ScopedRateThrottle): """ همان الگویِ GuestAwareDiscountThrottle/GuestAwareFamilyThrottle و بقیه؛ چرخشِ حسابِ مهمان (deviceId جدید → pk جدید) یک ScopedRateThrottleِ سادهٔ per-user رو دور می‌زد. اینجا حساس‌تره چون referralCode یک شناسه‌ی حدس‌زدنی‌ست (404 برای کدِ نامعتبر = oracle) و یک حدسِ درست یک Conversion واقعی می‌سازه که به کمیسیونِ مالیِ affiliate صاحبِ کد منجر می‌شه؛ یعنی بدونِ این throttle، چرخشِ حساب مهمان به مهاجم guessهای عملاً نامحدودی برای enumerate/fraud کردنِ کدهای رفرال می‌داد. برای کاربرهای مهمان کلیدِ cache از pk به IP کلاینت تغییر می‌کنه؛ رفتارِ throttle برای کاربرهای غیرمهمان (per-user، مثلِ قبل) دست‌نخورده می‌مونه. """ def get_cache_key(self, request, view): user = request.user if user and user.is_authenticated and getattr(user, 'is_guest', False): ident = f'guest-ip:{self.get_ident(request)}' elif user and user.is_authenticated: ident = user.pk else: ident = self.get_ident(request) return self.cache_format % {'scope': self.scope, 'ident': ident} class AffiliateMeView(APIView): """GET /api/affiliate/me — حساب و کد ارجاع کاربر""" permission_classes = [IsAuthenticated] @extend_schema(responses=AffiliateMeResponseSerializer, tags=['Affiliate (affiliate)']) def get(self, request): account, _ = AffiliateAccount.objects.get_or_create(user_id=request.user.id) data = {'referralCode': account.referral_code, 'commissionRate': account.commission_rate} return Response(AffiliateMeResponseSerializer(data).data) class TrackConversionView(APIView): """ POST /api/affiliate/track — ثبت کلیک/ارجاع طبق سند، این اپ فقط با referralCode به AffiliateAccount وصل می‌شه. referredUserId دیگه ورودیِ کلاینت نیست: چون این درخواست باید توسطِ خودِ کاربرِ تازه‌ارجاع‌شده (بعد از لاگین/ثبت‌نام) زده بشه، هویتش رو از توکن احرازشده (request.user) می‌گیریم، نه از بدنه‌ی درخواست. # هویت از request.user گرفته می‌شه (نه بدنه‌ی درخواست) چون یک # referredUserIdِ کلاینت‌دار یعنی هر کلاینتِ ناشناس می‌تونست برای هر # کاربرِ دیگه‌ای Conversion بسازه یا با اشغالِ همه‌ی userId ها (قیدِ # unique رو دورزدن) مسیرِ ارجاعِ واقعی رو مسدود کنه. """ permission_classes = [IsAuthenticated] # referralCode نامعتبر ۴۰۴ می‌گیره (یک oracle قابل‌probe)، پس throttle # مجزا هم probe رو کند می‌کنه هم (با GuestAwareAffiliateThrottle) جلویِ # bucket تازه از چرخشِ حساب مهمان رو می‌گیره. throttle_scope = 'affiliate_track' throttle_classes = [GuestAwareAffiliateThrottle] @extend_schema( request=TrackRequestSerializer, responses=TrackResponseSerializer, tags=['Affiliate (affiliate)'] ) def post(self, request): serializer = TrackRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) v = serializer.validated_data account = get_object_or_404(AffiliateAccount, referral_code=v['referralCode']) referred_user_id = request.user.id if referred_user_id == account.user_id: raise ValidationError({'referredUserId': 'امکان ثبت ارجاع برای خودِ صاحب کد وجود ندارد.'}) conversion, _ = Conversion.objects.get_or_create( referred_user_id=referred_user_id, defaults={'affiliate_account': account}, ) data = {'success': True, 'conversionId': str(conversion.pk), 'status': conversion.status} return Response(TrackResponseSerializer(data).data) class ConversionsListView(APIView): """GET /api/affiliate/conversions — لیست تبدیل‌ها و کمیسیون""" permission_classes = [IsAuthenticated] @extend_schema(responses=ConversionListResponseSerializer, tags=['Affiliate (affiliate)']) def get(self, request): account = AffiliateAccount.objects.filter(user_id=request.user.id).first() conversions = account.conversions.all() if account else Conversion.objects.none() items = [ { 'id': str(c.pk), 'referredUserId': str(c.referred_user_id), 'status': c.status, 'commissionAmount': c.commission_amount, } for c in conversions ] data = {'conversions': items} return Response(ConversionListResponseSerializer(data).data)