send the Bazaar access token without "Bearer" and cap its cache at an hour

Tried against the real Bazaar API on 2026-09-26 after the first live
authorization: "Authorization: Bearer <t>" gets 403 "Access token is not
valid", the bare token is accepted, even though token_type says Bearer.
The refresh grant also returned expires_in=3600000, which the old code
would have cached for ~41 days; the TTL is now capped at one hour and
401/403 clear it.

404 invalid_value (wrong package or SKU) is our misconfiguration, so it
now raises 503 instead of failing the user's order; only error=not_found
means "not purchased", as Bazaar's docs say.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 22:12:20 +03:30
parent ff212855a5
commit 7d205caa79
3 changed files with 39 additions and 13 deletions

View File

@@ -166,11 +166,21 @@ def complete_authorization(*, code: str, state: str = '') -> None:
raise BazaarAuthorizationError('این لینک همین الان یک بار استفاده شد.')
cache.delete(ACCESS_TOKEN_CACHE_KEY)
if data.get('access_token'):
cache.set(ACCESS_TOKEN_CACHE_KEY, data['access_token'],
timeout=max(int(data.get('expires_in', 3600)) - 60, 60))
cache.set(ACCESS_TOKEN_CACHE_KEY, data['access_token'], timeout=_access_token_ttl(data))
logger.info('Bazaar API authorized; refresh token stored')
def _access_token_ttl(data: dict) -> int:
# سند ۳۶۰۰ می‌گوید ولی بازارِ واقعی ۲۰۲۶-۰۹-۲۶ `expires_in: 3600000` داد
# (احتمالاً میلی‌ثانیه). کش را به یک ساعت سقف می‌زنیم تا ۴۱ روز توکنِ
# مرده نگه نداریم؛ روی 401/403 هم پاک می‌شود.
try:
seconds = int(data.get('expires_in', 3600))
except (TypeError, ValueError):
seconds = 3600
return max(min(seconds, 3600) - 60, 60)
def _get_access_token() -> str:
"""
ACCESS_TOKEN رو کش می‌کنه (توی cache framework جنگو) تا هر verify یک
@@ -214,9 +224,7 @@ def _get_access_token() -> str:
logger.error('Bazaar access-token response malformed: status=%s', resp.status_code)
raise BazaarAPIError()
# طبق سند بازار access_token معمولاً ~۱ ساعت اعتباره؛ کمی زودتر منقضی
# می‌کنیم تا لبه‌ی زمانی مشکل نسازه.
cache.set(ACCESS_TOKEN_CACHE_KEY, token, timeout=int(data.get('expires_in', 3600)) - 60)
cache.set(ACCESS_TOKEN_CACHE_KEY, token, timeout=_access_token_ttl(data))
return token
@@ -234,7 +242,10 @@ def _auth_headers() -> dict:
"""
if settings.BAZAAR_PISHKHAN_API_SECRET:
return {'CAFEBAZAAR-PISHKHAN-API-SECRET': settings.BAZAAR_PISHKHAN_API_SECRET}
return {'Authorization': f'Bearer {_get_access_token()}'}
# بدونِ «Bearer»: با آن بازار 403 «Access token is not valid» می‌دهد، با
# توکنِ خام قبول می‌کند (آزمایش روی بازارِ واقعی، ۲۰۲۶-۰۹-۲۶)، هرچند
# token_typeِ خودش Bearer است.
return {'Authorization': _get_access_token()}
def verify_purchase(*, product_sku: str, purchase_token: str, is_subscription: bool = False) -> BazaarPurchaseResult:
@@ -247,7 +258,7 @@ def verify_purchase(*, product_sku: str, purchase_token: str, is_subscription: b
تک‌خرید: api/validate/<pkg>/inapp/<sku>/purchases/<token>/
اشتراک: api/applications/<pkg>/subscriptions/<sku>/purchases/<token>
به گفته‌ی سند «تنها اگر error برابر not_found باشد خرید انجام نشده»؛ پس
فقط ۴۰۴ نامعتبر است و بقیه‌ی خطاها (کلیدِ غلط، سقفِ روزانه، قطعی) ۵۰۳ اند
فقط همان نامعتبر است و بقیه‌ی خطاها (کلیدِ غلط، سقفِ روزانه، قطعی) ۵۰۳ اند
تا یک خریدِ واقعی به‌خاطرِ خطای ما failed نشود.
"""
if not is_configured():
@@ -279,13 +290,16 @@ def verify_purchase(*, product_sku: str, purchase_token: str, is_subscription: b
except ValueError:
data = None
if resp.status_code == 404:
return BazaarPurchaseResult(is_valid=False, raw=data if isinstance(data, dict) else {'status_code': 404})
error = data.get('error') if isinstance(data, dict) else None
if resp.status_code == 404 and error == 'not_found':
return BazaarPurchaseResult(is_valid=False, raw=data)
# 404 با invalid_value («Package name is invalid» / «Product is not found»)
# یعنی تنظیمِ ما غلط است، نه خریدِ کاربر؛ مثلِ بقیه‌ی خطاها ۵۰۳.
if resp.status_code != 200 or not isinstance(data, dict):
if resp.status_code == 401:
if resp.status_code in (401, 403):
cache.delete(ACCESS_TOKEN_CACHE_KEY)
logger.error('Bazaar verify-purchase failed: status=%s error=%s', resp.status_code,
data.get('error') if isinstance(data, dict) else None)
logger.error('Bazaar verify-purchase failed: status=%s error=%s desc=%s', resp.status_code,
error, data.get('error_description') if isinstance(data, dict) else None)
raise BazaarAPIError()
if is_subscription: