Grades 4 and 5, Bazaar purchase from the app, pack SKUs

- rag: MATH_4 / MATH_5 textbooks (TOC and page offsets read from the PDFs)
- content: grades 4 and 5 in the seed files: 7 units / 28 nodes each,
  204 + 194 questions (every node >= 4), all re-solved blind once
- subscription: Plan.bazaar_sku (admin); POST /api/subscription/purchase
  accepts paymentMethod=bazaar for packs, rejects unknown methods (was ignored)
- payment: idempotency key cannot switch provider; discount codes refused
  on Bazaar orders (Bazaar charges the fixed SKU price)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 20:01:44 +03:30
parent ab5e53730a
commit 8cb326ba94
15 changed files with 16699 additions and 3897 deletions

View File

@@ -5,6 +5,7 @@ from django.db.models import F
from django.shortcuts import get_object_or_404
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, 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
@@ -69,6 +70,7 @@ class PlansListView(APIView):
'kind': p.kind,
'questionCredits': p.question_credits,
'heartsDays': p.hearts_days,
'bazaarSku': p.bazaar_sku,
}
for p in Plan.objects.filter(on_sale=True).order_by('kind', 'price')
],
@@ -119,6 +121,21 @@ class PurchaseView(APIView):
plan = get_object_or_404(Plan, pk=v['planId'], on_sale=True)
idempotency_key = request.headers.get('Idempotency-Key')
if v['paymentMethod'] == 'bazaar':
# فقط بسته: اشتراک‌ها دیگر فروخته نمی‌شوند و مسیرِ اشتراکِ بازار (تمدیدِ خودکار) هرگز
# با خریدِ واقعی آزموده نشده. SKU از پلن، نه از کلاینت.
if plan.kind != 'pack':
raise ValidationError({'paymentMethod': 'خرید از بازار فقط برای بسته‌های سوال است.'})
if not plan.bazaar_sku:
raise ValidationError({'planId': 'این بسته هنوز در کافه‌بازار تعریف نشده است.'})
order, _ = create_order(
user_id=user.id, amount=plan.price, currency=plan.currency,
idempotency_key=idempotency_key, plan_id=plan.id, plan_title=plan.title,
provider='bazaar', bazaar_product_sku=plan.bazaar_sku,
)
return Response(PurchaseResponseSerializer(
{'orderId': str(order.id), 'paymentUrl': None, 'bazaarSku': order.bazaar_product_sku}).data)
if plan.kind == 'pack':
# بسته به Subscription دست نمی‌زند: اعتبار و روزهای قلب بعد از پرداخت از روی
# خودِ سفارش داده می‌شوند (payment.services → subscription.services.grant_pack).