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

@@ -94,3 +94,79 @@ class BazaarSkuComesFromTheOrderTests(TestCase):
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')