Files
MathApp/payment/bazaar.py

106 lines
5.1 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.
"""
کلاینت سرور-به-سرورِ پرداخت درون‌برنامه‌ای بازار (REST API v2)، طبق داکیومنت
رسمی: developers.cafebazaar.ir/fa/guidelines/in-app-billing
جریان رسمی:
۱) یک‌بار (دستی، از پنل devapi توسعه‌دهنده) با CLIENT_ID/CLIENT_SECRET یک
authorization code گرفته می‌شه و با اون REFRESH_TOKEN ساخته می‌شه —
این مرحله در این فایل نیست چون یک‌بار و دستی‌ست، نه بخشی از API عمومی.
۲) بک‌اند با همون REFRESH_TOKEN، هر بار (یا هر بار که access_token منقضی
شده) یک ACCESS_TOKEN تازه می‌گیره (POST /auth/token با
grant_type=refresh_token).
۳) با همون access_token، وضعیت خرید/اشتراک از endpoint purchase/subscription
خونده می‌شه (GET .../applications/{package}/inapp/{sku}/purchases/{token}/).
⚠️ کافه‌بازار روش صحت‌سنجی رو تغییر داده (نگاه کنید به
developers.cafebazaar.ir/fa/guidelines/in-app-billing/api/postman-and-bazaar-api-new)؛
قبل از اتصال واقعی به production، دقیق‌ترین مسیر endpoint را از همون صفحه یا
پنل devapi خودتون تایید کنید — این فایل فقط اسکلتِ همون جریان رسمی رو
پیاده‌سازی کرده تا بدون کلید واقعی هم (مثل call_payment_gateway در
services.py) از طریق Swagger قابل تست باشه.
"""
from dataclasses import dataclass
import requests
from django.conf import settings
from django.core.cache import cache
ACCESS_TOKEN_CACHE_KEY = 'bazaar_iap_access_token'
@dataclass
class BazaarPurchaseResult:
is_valid: bool
purchase_state: int | None = None # ۰=خریداری‌شده، ۱=لغوشده (طبق سند بازار)
consumption_state: int | None = None
developer_payload: str = ''
raw: dict | None = None
def _get_access_token() -> str:
"""
ACCESS_TOKEN رو کش می‌کنه (توی cache framework جنگو) تا هر verify یک
درخواست OAuth جدا نزنه؛ اگه کش نداشت یا منقضی شده بود، با REFRESH_TOKEN
یکی تازه می‌گیره.
"""
cached = cache.get(ACCESS_TOKEN_CACHE_KEY)
if cached:
return cached
if not settings.BAZAAR_REFRESH_TOKEN:
# بدون کلید واقعی (مثلاً محیط توسعه)، استاب: دقیقاً مثل
# call_payment_gateway در payment/services.py، تا جریان کامل بدون
# کلید واقعی بازار هم قابل تست باشه.
return 'stub-access-token'
resp = requests.post(
f'{settings.BAZAAR_API_BASE}/auth/token/',
data={
'grant_type': 'refresh_token',
'client_id': settings.BAZAAR_CLIENT_ID,
'client_secret': settings.BAZAAR_CLIENT_SECRET,
'refresh_token': settings.BAZAAR_REFRESH_TOKEN,
},
timeout=10,
)
resp.raise_for_status()
data = resp.json()
token = data['access_token']
# طبق سند بازار access_token معمولاً ~۱ ساعت اعتباره؛ کمی زودتر منقضی
# می‌کنیم تا لبه‌ی زمانی مشکل نسازه.
cache.set(ACCESS_TOKEN_CACHE_KEY, token, timeout=int(data.get('expires_in', 3600)) - 60)
return token
def verify_purchase(*, product_sku: str, purchase_token: str, is_subscription: bool = False) -> BazaarPurchaseResult:
"""
صحت‌سنجی purchaseToken که کلاینت اندروید بعد از خرید از SDK بازار گرفته.
is_subscription=True برای اشتراک‌های دوره‌ای بازار (endpoint جدا از خرید
تک‌محصولی)، طبق سند رسمی.
"""
if not settings.BAZAAR_REFRESH_TOKEN:
# استاب توسعه: هر token غیرخالی معتبر فرض می‌شه تا کل جریان
# (ساخت سفارش bazaar → verify → فعال‌سازی اشتراک) بدون کلید واقعی
# از طریق Swagger قابل تست باشه.
return BazaarPurchaseResult(is_valid=bool(purchase_token), purchase_state=0)
access_token = _get_access_token()
kind = 'subscriptions' if is_subscription else 'purchases'
url = (
f'{settings.BAZAAR_API_BASE}/api/applications/'
f'{settings.BAZAAR_PACKAGE_NAME}/{kind}/{product_sku}/{purchase_token}/'
)
resp = requests.get(url, headers={'Authorization': f'Bearer {access_token}'}, timeout=10)
if resp.status_code != 200:
return BazaarPurchaseResult(is_valid=False, raw={'status_code': resp.status_code})
data = resp.json()
purchase_state = data.get('purchaseState')
return BazaarPurchaseResult(
is_valid=purchase_state == 0, # ۰ یعنی خریداری‌شده و معتبر
purchase_state=purchase_state,
consumption_state=data.get('consumptionState'),
developer_payload=data.get('developerPayload', ''),
raw=data,
)