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:
2026-10-12 20:33:38 +03:30
parent 9c3adc3e60
commit 947f946909
39 changed files with 398 additions and 1174 deletions

View File

@@ -118,7 +118,7 @@ class BazaarOAuthCallbackTests(TestCase):
mock_post.return_value.raise_for_status = lambda: None
mock_post.return_value.json.return_value = {'access_token': 'acc', 'expires_in': 3600}
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {'purchaseState': 0}
mock_get.return_value.json.return_value = {'validUntilTimestampMsec': timezone.now().timestamp() * 1000 + 86_400_000}
self.assertTrue(bazaar.verify_purchase(product_sku='s', purchase_token='t').is_valid)
self.assertEqual(mock_post.call_args.kwargs['data']['refresh_token'], 'db-ref')
@@ -141,21 +141,22 @@ class BazaarValidateAgainstDocsTests(TestCase):
@patch('payment.bazaar.requests.post')
@patch('payment.bazaar.requests.get')
def test_inapp_url_and_new_method_header(self, mock_get, mock_post):
self._resp(mock_get, 200, {'purchaseState': 0, 'consumptionState': 1})
self.assertTrue(bazaar.verify_purchase(product_sku='monthly', purchase_token='tok').is_valid)
def test_subscription_url_new_method_header_and_expiry(self, mock_get, mock_post):
now_ms = timezone.now().timestamp() * 1000
self._resp(mock_get, 200, {'validUntilTimestampMsec': now_ms + 86_400_000, 'autoRenewing': True})
result = bazaar.verify_purchase(product_sku='10343', purchase_token='tok')
self.assertTrue(result.is_valid)
self.assertTrue(result.auto_renewing)
self.assertEqual(
mock_get.call_args.args[0],
'https://pardakht.cafebazaar.ir/devapi/v2/api/validate/ir.example.app/inapp/monthly/purchases/tok/',
'https://pardakht.cafebazaar.ir/devapi/v2/api/applications/ir.example.app/subscriptions/10343/purchases/tok/',
)
self.assertEqual(mock_get.call_args.kwargs['headers'],
{'CAFEBAZAAR-PISHKHAN-API-SECRET': 'pishkhan-secret'})
mock_post.assert_not_called() # روشِ جدید OAuth نمی‌خواهد
@patch('payment.bazaar.requests.get')
def test_refunded_is_invalid(self, mock_get):
self._resp(mock_get, 200, {'purchaseState': 1})
self.assertFalse(bazaar.verify_purchase(product_sku='s', purchase_token='t').is_valid)
# اشتراکِ منقضی را هم بازار برمی‌گرداند؛ معتبر نیست.
self._resp(mock_get, 200, {'validUntilTimestampMsec': now_ms - 1000})
self.assertFalse(bazaar.verify_purchase(product_sku='10343', purchase_token='tok').is_valid)
@patch('payment.bazaar.requests.get')
def test_not_found_is_invalid(self, mock_get):
@@ -187,70 +188,3 @@ class BazaarValidateAgainstDocsTests(TestCase):
self._resp(mock_get, 404, {'error': 'not_found'})
bazaar.verify_purchase(product_sku='s', purchase_token='../../applications/x')
self.assertIn('/purchases/..%2F..%2Fapplications%2Fx/', mock_get.call_args.args[0])
@patch('payment.bazaar.requests.get')
def test_subscription_url_and_expiry(self, mock_get):
now_ms = timezone.now().timestamp() * 1000
self._resp(mock_get, 200, {'validUntilTimestampMsec': now_ms + 86_400_000})
self.assertTrue(bazaar.verify_purchase(product_sku='sub', purchase_token='t', is_subscription=True).is_valid)
self.assertEqual(
mock_get.call_args.args[0],
'https://pardakht.cafebazaar.ir/devapi/v2/api/applications/ir.example.app/subscriptions/sub/purchases/t/',
)
self._resp(mock_get, 200, {'validUntilTimestampMsec': now_ms - 1000})
self.assertFalse(bazaar.verify_purchase(product_sku='sub', purchase_token='t', is_subscription=True).is_valid)
@override_settings(
BAZAAR_PISHKHAN_API_SECRET='pishkhan-secret', BAZAAR_REFRESH_TOKEN='',
BAZAAR_PACKAGE_NAME='ir.example.app',
BAZAAR_API_BASE='https://pardakht.cafebazaar.ir/devapi/v2',
)
class BazaarProductTypeAutoDetectTests(TestCase):
"""نوعِ محصول (تک‌خرید/اشتراک) به ما گفته نشده؛ کد خودش امتحان می‌کند."""
def setUp(self):
cache.clear()
def _responses(self, mock_get, *pairs):
responses = []
for status, body in pairs:
r = type('R', (), {})()
r.status_code = status
r.json = (lambda b: (lambda: b))(body)
responses.append(r)
mock_get.side_effect = responses
@patch('payment.bazaar.requests.get')
def test_inapp_product_is_checked_once(self, mock_get):
self._responses(mock_get, (200, {'purchaseState': 0}))
self.assertTrue(bazaar.verify_purchase(product_sku='monthly', purchase_token='t').is_valid)
self.assertEqual(mock_get.call_count, 1)
self.assertIn('/inapp/', mock_get.call_args.args[0])
@patch('payment.bazaar.requests.get')
def test_unknown_inapp_product_falls_back_to_subscription(self, mock_get):
future = timezone.now().timestamp() * 1000 + 86_400_000
self._responses(
mock_get,
(404, {'error': 'invalid_value', 'error_description': 'Product is not found.'}),
(200, {'validUntilTimestampMsec': future}),
)
self.assertTrue(bazaar.verify_purchase(product_sku='monthly', purchase_token='t').is_valid)
self.assertIn('/subscriptions/monthly/purchases/t', mock_get.call_args.args[0])
@patch('payment.bazaar.requests.get')
def test_forged_token_on_subscription_is_invalid(self, mock_get):
self._responses(
mock_get,
(404, {'error': 'invalid_value', 'error_description': 'Product is not found.'}),
(404, {'error': 'not_found'}),
)
self.assertFalse(bazaar.verify_purchase(product_sku='monthly', purchase_token='forged').is_valid)
@patch('payment.bazaar.requests.get')
def test_wrong_package_does_not_fall_back(self, mock_get):
self._responses(mock_get, (404, {'error': 'invalid_value', 'error_description': 'Package name is invalid.'}))
with self.assertRaises(bazaar.BazaarAPIError):
bazaar.verify_purchase(product_sku='monthly', purchase_token='t')
self.assertEqual(mock_get.call_count, 1)