Initial commit - clean project structure
This commit is contained in:
105
payment/bazaar.py
Normal file
105
payment/bazaar.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
کلاینت سرور-به-سرورِ پرداخت درونبرنامهای بازار (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,
|
||||
)
|
||||
Reference in New Issue
Block a user