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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user