144 lines
6.1 KiB
Python
144 lines
6.1 KiB
Python
from django.db import transaction
|
|
from django.db.models import Q
|
|
from django.utils import timezone
|
|
from rest_framework import generics, status
|
|
from rest_framework.exceptions import NotFound
|
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
|
|
from apps.account.models import User
|
|
from apps.billing.models import BazaarPurchase, FeaturePlan, GemPack, GemTransaction
|
|
from apps.billing.providers.bazaar import verify_purchase
|
|
|
|
from .serializers import (
|
|
ActivateFeatureSerializer,
|
|
CreatePurchaseSerializer,
|
|
FeaturePlanSerializer,
|
|
GemPackSerializer,
|
|
WalletSerializer,
|
|
)
|
|
from .services import FeatureAccessService, GemWalletService
|
|
|
|
|
|
def _wallet_data(user: User) -> dict:
|
|
return {
|
|
"gem_balance": user.gem_balance,
|
|
"is_in_trial": user.is_in_trial,
|
|
"trial_expires_at": user.trial_expires_at,
|
|
"referral_code": user.referral_code,
|
|
"referral_count": User.objects.filter(referred_by=user, referral_bonus_granted=True).count(),
|
|
}
|
|
|
|
|
|
class WalletView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request):
|
|
return Response(WalletSerializer(_wallet_data(request.user)).data)
|
|
|
|
|
|
class FeatureListView(generics.ListAPIView):
|
|
"""فهرستِ ابزارها — عمدا AllowAny: کاربرِ مهمان (بدون حساب) هم باید
|
|
بتواند بفهمد کدام ابزار رایگان است. این پاسخ هیچ دادهی شخصی ندارد؛
|
|
فقط کاتالوگِ ابزارها و قیمتهاست."""
|
|
|
|
serializer_class = FeaturePlanSerializer
|
|
permission_classes = [AllowAny]
|
|
# ابزارهای رایگان is_purchasable=False دارند (فروختنی نیستند) ولی باید
|
|
# حتما در لیست بیایند — وگرنه اپ آنها را نمیشناسد و قفل نشانشان میدهد.
|
|
queryset = (
|
|
FeaturePlan.objects.filter(Q(is_purchasable=True) | Q(is_free=True))
|
|
.prefetch_related("tiers")
|
|
)
|
|
|
|
|
|
class ActivateFeatureView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
throttle_scope = "billing_write"
|
|
|
|
def post(self, request, key):
|
|
serializer = ActivateFeatureSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
FeatureAccessService.activate(request.user, key, serializer.validated_data["duration_months"])
|
|
return Response(
|
|
{
|
|
"success": True,
|
|
"feature": FeaturePlanSerializer(
|
|
FeaturePlan.objects.get(key=key), context={"request": request}
|
|
).data,
|
|
"wallet": WalletSerializer(_wallet_data(request.user)).data,
|
|
}
|
|
)
|
|
|
|
|
|
class GemPackListView(generics.ListAPIView):
|
|
serializer_class = GemPackSerializer
|
|
permission_classes = [IsAuthenticated]
|
|
queryset = GemPack.objects.filter(is_active=True)
|
|
|
|
|
|
class CreatePurchaseView(APIView):
|
|
"""کلاینت بعد از باز کردن لینک خرید کافهبازار و برگشتن، اینجا فقط
|
|
میگوید «این خرید را شروع کردم»؛ جمی اضافه نمیشود تا verify تایید کند."""
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
throttle_scope = "billing_write"
|
|
|
|
def post(self, request):
|
|
serializer = CreatePurchaseSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
try:
|
|
gem_pack = GemPack.objects.get(id=serializer.validated_data["gem_pack_id"], is_active=True)
|
|
except GemPack.DoesNotExist:
|
|
raise NotFound("بستهی جم یافت نشد.")
|
|
|
|
purchase, _created = BazaarPurchase.objects.get_or_create(
|
|
purchase_token=serializer.validated_data["purchase_token"],
|
|
defaults={"user": request.user, "gem_pack": gem_pack},
|
|
)
|
|
return Response({"id": purchase.id, "status": purchase.status}, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
class VerifyPurchaseView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
throttle_scope = "billing_write"
|
|
|
|
def post(self, request, purchase_id):
|
|
# قفل ردیفِ همین خرید تا پایان تراکنش — بدون این، دو درخواست verify
|
|
# همزمان (دابلتپ یا retry شبکه) هر دو میتوانند از چک status=pending
|
|
# رد شوند و جم را دوبار اضافه کنند. با این قفل، دومی تا کامیت شدن
|
|
# اولی معطل میماند و بعد status=verified را میبیند و کاری نمیکند.
|
|
with transaction.atomic():
|
|
try:
|
|
purchase = BazaarPurchase.objects.select_for_update().get(id=purchase_id, user=request.user)
|
|
except BazaarPurchase.DoesNotExist:
|
|
raise NotFound("خرید یافت نشد.")
|
|
|
|
if purchase.status == BazaarPurchase.Status.VERIFIED:
|
|
return Response(
|
|
{"status": purchase.status, "wallet": WalletSerializer(_wallet_data(request.user)).data}
|
|
)
|
|
|
|
is_valid = verify_purchase(purchase.gem_pack.bazaar_sku, purchase.purchase_token)
|
|
if not is_valid:
|
|
purchase.status = BazaarPurchase.Status.FAILED
|
|
purchase.save(update_fields=["status"])
|
|
return Response(
|
|
{"status": purchase.status, "detail": "تایید خرید ناموفق بود."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
GemWalletService.add_gems(
|
|
request.user,
|
|
purchase.gem_pack.gem_amount,
|
|
GemTransaction.Reason.PURCHASE,
|
|
note=f"خرید {purchase.gem_pack.display_name}",
|
|
)
|
|
purchase.status = BazaarPurchase.Status.VERIFIED
|
|
purchase.gems_granted = purchase.gem_pack.gem_amount
|
|
purchase.verified_at = timezone.now()
|
|
purchase.save(update_fields=["status", "gems_granted", "verified_at"])
|
|
|
|
return Response({"status": purchase.status, "wallet": WalletSerializer(_wallet_data(request.user)).data})
|