Question packs removed at the root; all subscriptions 4 assistant questions a day; Bazaar is subscription-only
- subscription.0017/0018, payment.0013: pack plans deleted, QuestionCredit/HeartsPass models and Plan.kind / question_credits / hearts_days / Order.question_credits / hearts_days dropped. The only paid pack order was the company owner's test account (user 26). - Assistant: no credit spending; quota is the daily cap only (free 2, every plan 4, gift subscription 4). - Bazaar: verify_purchase is subscription-only (no inapp guess), start/purchase responses lose productType, verify refuses an order without a subscription. A paid order with no subscription now logs ERROR. - Removed POST /api/payment/orders (internal; no caller). Its permission tests stay. - Pack-based tests rewritten on subscriptions (refund, security, Bazaar token); pack-only test files deleted. - docs: profit table for 4/day on both payment routes plus free users; handbook, operations, CLAUDE.md current. 949 tests green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,8 +23,6 @@ from .serializers import (
|
||||
BazaarStartResponseSerializer,
|
||||
BazaarVerifyRequestSerializer,
|
||||
BazaarVerifyResponseSerializer,
|
||||
CreateOrderRequestSerializer,
|
||||
CreateOrderResponseSerializer,
|
||||
OrderDetailResponseSerializer,
|
||||
ZarinpalCallbackResponseSerializer,
|
||||
)
|
||||
@@ -49,73 +47,6 @@ def _redirect_to_configured_url(url: str) -> HttpResponseRedirect:
|
||||
return redirect_cls(url)
|
||||
|
||||
|
||||
class CreateOrderView(APIView):
|
||||
"""
|
||||
POST /api/payment/orders — ساخت سفارش (فراخوانیشده از اپ Subscription)
|
||||
طبق سند API، این یک فراخوانی سرور-به-سرور از اپ Subscriptionه، نه از
|
||||
کلاینت. در عمل subscription/views.py مستقیماً تابع create_order رو
|
||||
import و صدا میزنه، پس این endpoint برای جریان عادی از بیرون صدا زده
|
||||
نمیشه؛ برای همین بهجای AllowAny یک shared-secret داخلی
|
||||
(HasInternalServiceKey، هدر X-Internal-Key) لازمه، وگرنه هر کلاینتی
|
||||
میتونست با یک userId دلخواه سفارش بسازه.
|
||||
"""
|
||||
|
||||
permission_classes = [HasInternalServiceKey]
|
||||
|
||||
# ⚠️ افزوده شده: هدرهای موردنیاز (X-Internal-Key, Idempotency-Key) قبلاً
|
||||
# مستند نشده بودن و از Swagger UI اصلاً قابل ستکردن/تستکردن نبودن.
|
||||
@extend_schema(
|
||||
request=CreateOrderRequestSerializer,
|
||||
responses=CreateOrderResponseSerializer,
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
'X-Internal-Key', OpenApiTypes.STR, OpenApiParameter.HEADER, required=True,
|
||||
description='کلید مخفیِ فراخوانی داخلی/سرور-به-سرور (settings.INTERNAL_SERVICE_KEY).',
|
||||
),
|
||||
OpenApiParameter(
|
||||
'Idempotency-Key', OpenApiTypes.STR, OpenApiParameter.HEADER, required=False,
|
||||
description=(
|
||||
'کلید یکتای idempotency برای جلوگیری از ساخت سفارش تکراری هنگام retry. '
|
||||
'در صورت عدم ارسال، یک کلید تصادفی ساخته میشود.'
|
||||
),
|
||||
),
|
||||
],
|
||||
tags=['Payment (payment)'],
|
||||
)
|
||||
def post(self, request):
|
||||
serializer = CreateOrderRequestSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
v = serializer.validated_data
|
||||
|
||||
if v.get('provider') == 'bazaar' and not v.get('bazaarProductSku'):
|
||||
raise ValidationError({'bazaarProductSku': 'سفارشِ بازار بدونِ SKU ساخته نمیشود.'})
|
||||
|
||||
plan = None
|
||||
if v.get('planId'):
|
||||
from subscription.models import Plan
|
||||
|
||||
plan = Plan.objects.filter(pk=v['planId']).first() if (str(v['planId']).isascii() and str(v['planId']).isdigit()) else None
|
||||
if plan is None:
|
||||
raise ValidationError({'planId': 'پلنی با این شناسه وجود ندارد.'})
|
||||
|
||||
order, payment_url = create_order(
|
||||
user_id=int(v['userId']),
|
||||
amount=v['amount'],
|
||||
currency=v.get('currency') or 'IRT',
|
||||
subscription_id=int(v['subscriptionId']) if v.get('subscriptionId') else None,
|
||||
idempotency_key=request.headers.get('Idempotency-Key'),
|
||||
provider=v.get('provider') or 'gateway',
|
||||
bazaar_product_sku=v.get('bazaarProductSku') or '',
|
||||
plan_id=plan.id if plan else None,
|
||||
plan_title=plan.title if plan else '',
|
||||
)
|
||||
data = {
|
||||
'orderId': str(order.id), 'status': order.status,
|
||||
'paymentUrl': payment_url, 'provider': order.provider,
|
||||
}
|
||||
return Response(CreateOrderResponseSerializer(data).data)
|
||||
|
||||
|
||||
class PaymentRedirectView(APIView):
|
||||
"""
|
||||
GET /api/payment/orders/{id}/pay — همان paymentUrlی که
|
||||
@@ -340,12 +271,12 @@ class BazaarVerifyPurchaseView(APIView):
|
||||
raise ValidationError({'orderId': 'این سفارش محصولِ بازار ندارد.'})
|
||||
if self._second_purchase_on_paid_order(order_lookup, v['purchaseToken']):
|
||||
return self._second_purchase_response()
|
||||
# نوعِ محصول از سفارش: سفارشِ اشتراک subscription_id دارد (PurchaseView)، بسته ندارد. پیشتر نوع
|
||||
# حدس زده میشد (اول تکخرید، بعد اشتراک).
|
||||
# فقط اشتراک از بازار فروخته میشود (subscription.0017)؛ سفارشِ بیاشتراک چیزی برای فعال کردن ندارد.
|
||||
if not order_lookup.subscription_id:
|
||||
raise ValidationError({'orderId': 'این سفارش اشتراکی ندارد.'})
|
||||
result = bazaar.verify_purchase(
|
||||
product_sku=order_lookup.bazaar_product_sku,
|
||||
purchase_token=v['purchaseToken'],
|
||||
is_subscription=bool(order_lookup.subscription_id),
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
@@ -438,7 +369,6 @@ class BazaarStartPurchaseView(APIView):
|
||||
data = {
|
||||
'orderId': str(order.id),
|
||||
'sku': order.bazaar_product_sku,
|
||||
'productType': 'subscription' if order.subscription_id else 'inapp',
|
||||
'amount': order.amount,
|
||||
'dynamicPriceToken': token,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user