- 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>
173 lines
8.8 KiB
Python
173 lines
8.8 KiB
Python
"""
|
||
خریدِ بستهی سوال از راهِ کافهبازار (بازبینیِ ۲۰۲۶-۱۰-۰۲).
|
||
|
||
سفارشِ بازار فقط از `POST /api/payment/orders` (کلیدِ داخلی) ساخته میشود. آن endpoint
|
||
`planId` نمیگرفت، پس سفارشِ بسته بی`plan_id` ساخته میشد و بعد از تأییدِ بازار «paid» میشد
|
||
بیآنکه `grant_pack` چیزی بدهد: پول گرفته میشد و اعتباری نمیآمد. پاسخِ تأیید هم وضعیتِ
|
||
پیش از پرداخت («pending») را برمیگرداند. بازار با `bazaar.verify_purchase`ِ ساختگی آزموده
|
||
میشود؛ خریدِ واقعیِ بازار هنوز هیچجا آزموده نشده.
|
||
"""
|
||
from unittest.mock import patch
|
||
|
||
from django.core.cache import cache
|
||
from django.test import TestCase, override_settings
|
||
from rest_framework.test import APIClient
|
||
|
||
from accounts.authentication import issue_access_token
|
||
from accounts.models import User
|
||
from subscription.models import Plan
|
||
from subscription.services import credit_balance
|
||
|
||
from .bazaar import BazaarPurchaseResult
|
||
from .models import Order
|
||
|
||
|
||
@override_settings(INTERNAL_SERVICE_KEY='internal-test-key')
|
||
class BazaarPackTests(TestCase):
|
||
def setUp(self):
|
||
cache.clear()
|
||
self.user = User.objects.create(phone='09120007101', is_guest=False)
|
||
self.client = APIClient()
|
||
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
|
||
self.pack = Plan.objects.get(kind='pack', question_credits=20)
|
||
|
||
def tearDown(self):
|
||
cache.clear()
|
||
|
||
def create_bazaar_order(self, **extra):
|
||
response = APIClient().post('/api/payment/orders', {
|
||
'userId': str(self.user.id), 'amount': self.pack.price, 'provider': 'bazaar',
|
||
'bazaarProductSku': 'pack20', **extra,
|
||
}, format='json', HTTP_X_INTERNAL_KEY='internal-test-key')
|
||
self.assertEqual(response.status_code, 200, response.content)
|
||
return response.json()['orderId']
|
||
|
||
def verify(self, order_id, valid=True):
|
||
with patch('payment.bazaar.verify_purchase',
|
||
return_value=BazaarPurchaseResult(is_valid=valid, purchase_state=0 if valid else None)):
|
||
return self.client.post('/api/payment/bazaar/verify', {
|
||
'orderId': order_id, 'productSku': 'pack20', 'purchaseToken': 'token-1'}, format='json')
|
||
|
||
def test_a_pack_bought_through_bazaar_gives_its_credits(self):
|
||
order_id = self.create_bazaar_order(planId=str(self.pack.id))
|
||
order = Order.objects.get(pk=order_id)
|
||
self.assertEqual((order.plan_id, order.plan_title, order.list_price), (self.pack.id, self.pack.title, self.pack.price))
|
||
self.verify(order_id)
|
||
self.assertEqual(credit_balance(self.user.id), 20)
|
||
|
||
def test_verify_reports_the_status_the_order_actually_has(self):
|
||
order_id = self.create_bazaar_order(planId=str(self.pack.id))
|
||
response = self.verify(order_id)
|
||
self.assertEqual(Order.objects.get(pk=order_id).status, 'paid')
|
||
self.assertEqual(response.json()['status'], 'paid')
|
||
|
||
def test_unknown_plan_is_rejected(self):
|
||
response = APIClient().post('/api/payment/orders', {
|
||
'userId': str(self.user.id), 'amount': 1, 'provider': 'bazaar', 'planId': '999999',
|
||
}, format='json', HTTP_X_INTERNAL_KEY='internal-test-key')
|
||
self.assertEqual(response.status_code, 400)
|
||
|
||
|
||
@override_settings(INTERNAL_SERVICE_KEY='internal-test-key')
|
||
class BazaarSkuComesFromTheOrderTests(TestCase):
|
||
"""تأییدِ بازار SKU را فقط از سفارش میگیرد، نه از کلاینت (۲۰۲۶-۱۰-۰۳)."""
|
||
|
||
def setUp(self):
|
||
cache.clear()
|
||
self.user = User.objects.create(phone='09120007102', is_guest=False)
|
||
self.client = APIClient()
|
||
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
|
||
|
||
def test_a_bazaar_order_needs_a_sku(self):
|
||
response = APIClient().post('/api/payment/orders', {
|
||
'userId': str(self.user.id), 'amount': 1000, 'provider': 'bazaar',
|
||
}, format='json', HTTP_X_INTERNAL_KEY='internal-test-key')
|
||
self.assertEqual(response.status_code, 400)
|
||
|
||
def test_verify_never_uses_the_clients_sku(self):
|
||
order = Order.objects.create(user_id=self.user.id, amount=259000, provider='bazaar',
|
||
idempotency_key='no-sku-1')
|
||
with patch('payment.bazaar.verify_purchase') as verify:
|
||
response = self.client.post('/api/payment/bazaar/verify', {
|
||
'orderId': str(order.id), 'productSku': 'cheapest', 'purchaseToken': 't'}, format='json')
|
||
self.assertEqual(response.status_code, 400)
|
||
verify.assert_not_called()
|
||
order.refresh_from_db()
|
||
self.assertEqual(order.status, 'pending')
|
||
|
||
|
||
class BazaarPurchaseFromTheAppTests(TestCase):
|
||
"""
|
||
خریدِ بازار از مسیرِ خودِ کاربر: `POST /api/subscription/purchase` با
|
||
`paymentMethod: "bazaar"` (۲۰۲۶-۱۰-۰۳). پیش از آن تنها راهِ ساختِ سفارشِ بازار کلیدِ
|
||
داخلی بود و `paymentMethod` هر مقداری را میپذیرفت و نادیده میگرفت.
|
||
"""
|
||
|
||
def setUp(self):
|
||
cache.clear()
|
||
self.user = User.objects.create(phone='09120007103', is_guest=False)
|
||
self.client = APIClient()
|
||
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
|
||
self.pack = Plan.objects.get(kind='pack', question_credits=20)
|
||
self.pack.bazaar_sku = 'tadrisino_pack_20'
|
||
self.pack.save()
|
||
|
||
def tearDown(self):
|
||
cache.clear()
|
||
|
||
def buy(self, plan, method='bazaar', **headers):
|
||
return self.client.post('/api/subscription/purchase',
|
||
{'planId': str(plan.id), 'paymentMethod': method}, format='json', **headers)
|
||
|
||
def test_bazaar_order_carries_the_plans_sku_and_no_payment_url(self):
|
||
response = self.buy(self.pack)
|
||
self.assertEqual(response.status_code, 200, response.content)
|
||
body = response.json()
|
||
self.assertIsNone(body['paymentUrl'])
|
||
self.assertEqual(body['bazaarSku'], 'tadrisino_pack_20')
|
||
order = Order.objects.get(pk=body['orderId'])
|
||
self.assertEqual((order.provider, order.bazaar_product_sku, order.plan_id),
|
||
('bazaar', 'tadrisino_pack_20', self.pack.id))
|
||
|
||
def test_whole_flow_gives_the_pack_credits(self):
|
||
order_id = self.buy(self.pack).json()['orderId']
|
||
with patch('payment.bazaar.verify_purchase',
|
||
return_value=BazaarPurchaseResult(is_valid=True, purchase_state=0)) as verify:
|
||
response = self.client.post('/api/payment/bazaar/verify',
|
||
{'orderId': order_id, 'purchaseToken': 'tok-app-1'}, format='json')
|
||
self.assertEqual(response.json()['status'], 'paid')
|
||
self.assertEqual(verify.call_args.kwargs['product_sku'], 'tadrisino_pack_20')
|
||
self.assertEqual(credit_balance(self.user.id), 20)
|
||
|
||
def test_pack_without_a_sku_is_refused_not_sent_to_zarinpal(self):
|
||
other = Plan.objects.get(kind='pack', question_credits=60)
|
||
response = self.buy(other)
|
||
self.assertEqual(response.status_code, 400)
|
||
self.assertFalse(Order.objects.filter(user_id=self.user.id).exists())
|
||
|
||
def test_unknown_payment_method_is_a_400(self):
|
||
self.assertEqual(self.buy(self.pack, method='paypal').status_code, 400)
|
||
|
||
def test_zarinpal_is_still_the_default(self):
|
||
response = self.client.post('/api/subscription/purchase', {'planId': str(self.pack.id)}, format='json')
|
||
self.assertEqual(response.status_code, 200)
|
||
self.assertIn('/pay', response.json()['paymentUrl'])
|
||
self.assertEqual(Order.objects.get(pk=response.json()['orderId']).provider, 'gateway')
|
||
|
||
def test_same_idempotency_key_cannot_switch_payment_method(self):
|
||
self.assertEqual(self.buy(self.pack, HTTP_IDEMPOTENCY_KEY='k-1').status_code, 200)
|
||
self.assertEqual(self.buy(self.pack, method='zarinpal', HTTP_IDEMPOTENCY_KEY='k-1').status_code, 409)
|
||
|
||
def test_discount_code_is_refused_on_a_bazaar_order(self):
|
||
from discount.models import DiscountCode
|
||
|
||
DiscountCode.objects.create(code='HALF', type='percent', value=50, max_uses=10)
|
||
order_id = self.buy(self.pack).json()['orderId']
|
||
response = self.client.post('/api/discount/redeem', {'code': 'HALF', 'orderId': order_id}, format='json')
|
||
self.assertEqual(response.status_code, 400)
|
||
self.assertEqual(Order.objects.get(pk=order_id).amount, self.pack.price)
|
||
|
||
def test_plans_list_exposes_the_sku(self):
|
||
plans = self.client.get('/api/subscription/plans').json()['plans']
|
||
self.assertEqual(next(p for p in plans if p['id'] == str(self.pack.id))['bazaarSku'], 'tadrisino_pack_20')
|