let Bazaar verification find out whether a SKU is a one-time product or a subscription
Nobody has told us how the plans are defined in the Bazaar panel. The verify view always asked the in-app endpoint, so a subscription SKU would get "Product is not found" and a 503. verify_purchase now defaults to is_subscription=None: in-app first, and on that exact answer the same SKU is asked as a subscription. A wrong package name still raises at once. Seen against the live Bazaar API with a fake SKU and token only; no real purchase has gone through it yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -248,7 +248,15 @@ def _auth_headers() -> dict:
|
||||
return {'Authorization': _get_access_token()}
|
||||
|
||||
|
||||
def verify_purchase(*, product_sku: str, purchase_token: str, is_subscription: bool = False) -> BazaarPurchaseResult:
|
||||
def _is_unknown_product(status: int, data) -> bool:
|
||||
return (
|
||||
status == 404 and isinstance(data, dict) and data.get('error') == 'invalid_value'
|
||||
and 'product' in str(data.get('error_description', '')).lower()
|
||||
)
|
||||
|
||||
|
||||
def verify_purchase(*, product_sku: str, purchase_token: str,
|
||||
is_subscription: bool | None = None) -> BazaarPurchaseResult:
|
||||
"""
|
||||
صحتسنجی purchaseToken که کلاینت اندروید بعد از خرید از SDK بازار گرفته.
|
||||
|
||||
@@ -260,6 +268,11 @@ def verify_purchase(*, product_sku: str, purchase_token: str, is_subscription: b
|
||||
به گفتهی سند «تنها اگر error برابر not_found باشد خرید انجام نشده»؛ پس
|
||||
فقط همان نامعتبر است و بقیهی خطاها (کلیدِ غلط، سقفِ روزانه، قطعی) ۵۰۳ اند
|
||||
تا یک خریدِ واقعی بهخاطرِ خطای ما failed نشود.
|
||||
|
||||
is_subscription=None (پیشفرض): نوعِ محصول در پیشخانِ بازار به ما گفته نشده
|
||||
(۲۰۲۶-۰۹-۳۰)، پس اول تکخرید پرسیده میشود و اگر بازار گفت «Product is not
|
||||
found»، همان SKU بهعنوانِ اشتراک. با پاسخِ واقعیِ بازار روی SKU و توکنِ
|
||||
ساختگی دیده شد؛ با خریدِ واقعی هنوز آزموده نشده.
|
||||
"""
|
||||
if not is_configured():
|
||||
# 🔒 fail-closed: بدونِ کلیدِ بازار هیچوقت به گفتهی کلاینت اعتماد
|
||||
@@ -274,35 +287,41 @@ def verify_purchase(*, product_sku: str, purchase_token: str, is_subscription: b
|
||||
pkg = quote(settings.BAZAAR_PACKAGE_NAME, safe='')
|
||||
sku = quote(product_sku, safe='')
|
||||
token = quote(purchase_token, safe='')
|
||||
if is_subscription:
|
||||
url = f'{settings.BAZAAR_API_BASE}/api/applications/{pkg}/subscriptions/{sku}/purchases/{token}'
|
||||
else:
|
||||
url = f'{settings.BAZAAR_API_BASE}/api/validate/{pkg}/inapp/{sku}/purchases/{token}/'
|
||||
|
||||
try:
|
||||
resp = requests.get(url, headers=_auth_headers(), timeout=10)
|
||||
except requests.RequestException:
|
||||
logger.exception('Bazaar verify-purchase request failed')
|
||||
raise BazaarAPIError()
|
||||
def fetch(subscription: bool):
|
||||
if subscription:
|
||||
url = f'{settings.BAZAAR_API_BASE}/api/applications/{pkg}/subscriptions/{sku}/purchases/{token}'
|
||||
else:
|
||||
url = f'{settings.BAZAAR_API_BASE}/api/validate/{pkg}/inapp/{sku}/purchases/{token}/'
|
||||
try:
|
||||
resp = requests.get(url, headers=_auth_headers(), timeout=10)
|
||||
except requests.RequestException:
|
||||
logger.exception('Bazaar verify-purchase request failed')
|
||||
raise BazaarAPIError()
|
||||
try:
|
||||
return resp.status_code, resp.json()
|
||||
except ValueError:
|
||||
return resp.status_code, None
|
||||
|
||||
try:
|
||||
data = resp.json()
|
||||
except ValueError:
|
||||
data = None
|
||||
subscription = bool(is_subscription)
|
||||
status, data = fetch(subscription)
|
||||
if is_subscription is None and _is_unknown_product(status, data):
|
||||
subscription = True
|
||||
status, data = fetch(True)
|
||||
|
||||
error = data.get('error') if isinstance(data, dict) else None
|
||||
if resp.status_code == 404 and error == 'not_found':
|
||||
if status == 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 in (401, 403):
|
||||
if status != 200 or not isinstance(data, dict):
|
||||
if status in (401, 403):
|
||||
cache.delete(ACCESS_TOKEN_CACHE_KEY)
|
||||
logger.error('Bazaar verify-purchase failed: status=%s error=%s desc=%s', resp.status_code,
|
||||
logger.error('Bazaar verify-purchase failed: status=%s error=%s desc=%s', status,
|
||||
error, data.get('error_description') if isinstance(data, dict) else None)
|
||||
raise BazaarAPIError()
|
||||
|
||||
if is_subscription:
|
||||
if subscription:
|
||||
# پاسخِ اشتراک purchaseState ندارد و اشتراکِ منقضی را هم برمیگرداند؛
|
||||
# سند میگوید validUntil را با اکنون مقایسه کنید.
|
||||
valid_until = data.get('validUntilTimestampMsec') or 0
|
||||
|
||||
Reference in New Issue
Block a user