Files
MathApp/payment/tests_bazaar_pack.py
Zareian 9c3adc3e60 Subscriptions only: Bazaar panel prices (598/898/1398k), assistant quota 5/3/3 from a cost calculation, packs off sale
- subscription.0016: prices equal to the Bazaar panel (senior), ai_daily_quota 5/3/3, card features; packs on_sale=False
  (one paid pack order, user 26, keeps its credits; pack paths stay for that order and for admin).
- Quota math in docs/ai-cost-and-guardrails.md: ~77% of a Bazaar sale reaches us; worst case (full quota every day)
  the assistant costs 23/27/35% of it.
- Assistant quota message points to the subscription, not packs.
982 tests green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:57:16 +03:30

176 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
خریدِ بسته‌ی سوال از راهِ کافه‌بازار (بازبینیِ ۲۰۲۶-۱۰-۰۲).
سفارشِ بازار فقط از `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):
Plan.objects.filter(kind='pack').update(on_sale=True) # بسته‌ها از ۲۰۲۶-۱۰-۱۲ فروشی نیستند (subscription.0016)؛ مسیرشان برای سفارش‌های قبلی و ادمین می‌ماند
cache.clear()
self.user = User.objects.create(phone='09120007101')
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):
Plan.objects.filter(kind='pack').update(on_sale=True) # بسته‌ها از ۲۰۲۶-۱۰-۱۲ فروشی نیستند (subscription.0016)؛ مسیرشان برای سفارش‌های قبلی و ادمین می‌ماند
cache.clear()
self.user = User.objects.create(phone='09120007102')
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):
Plan.objects.filter(kind='pack').update(on_sale=True) # بسته‌ها از ۲۰۲۶-۱۰-۱۲ فروشی نیستند (subscription.0016)؛ مسیرشان برای سفارش‌های قبلی و ادمین می‌ماند
cache.clear()
self.user = User.objects.create(phone='09120007103')
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')