خریدها و اشتراک‌ها

قرارداد پیاده‌سازی خرید مستقیم، خرید استوری، تمدید و بازیابی خرید در SDK نسخه ۲

طراحی فرایند خرید در SDK نسخه ۲

این سند قرارداد اجرایی پیاده‌سازی خرید در نسخه جدید SDKهای اینپی فای است. هدف اصلی این نسخه، اصلاح فرایند خریدهای کافه‌بازار و مایکت بدون ایجاد تغییر در رفتار اپ‌هایی است که همچنان از SDK و API نسخه ۱ استفاده می‌کنند.

اصل سازگاری با نسخه قبل

  • API و SDK نسخه ۱ نباید تغییر رفتار داشته باشند.
  • متد عمومی purchase در SDK نسخه ۲ نیز حفظ می‌شود تا مهاجرت برای توسعه‌دهنده ساده باشد.
  • SDK در داخل متد purchase و بر اساس storePlatform مسیر مناسب را انتخاب می‌کند.
  • پرداخت مستقیم همچنان از فرایند پایدار نسخه ۱ استفاده می‌کند.
  • فقط خریدهای کافه‌بازار و مایکت به API جدید خرید استوری منتقل می‌شوند.
purchase(package)
├── DirectAndroid / DirectIos / DirectWeb
│   └── POST /app/v1/purchase
└── Bazar / Myket
    ├── اجرای خرید در SDK فروشگاه
    └── POST /app/v2/store/purchases

بنابراین انتشار SDK نسخه ۲ به معنی حذف یا بازنویسی پرداخت مستقیم نیست. SDK جدید یک API عمومی یکپارچه دارد، اما در لایه داخلی از دو مسیر متفاوت استفاده می‌کند.

قرارداد عمومی پیشنهادی SDK

نام دقیق کلاس‌ها و callbackها می‌تواند با زبان SDK هماهنگ شود، ولی رفتار عمومی باید معادل قرارداد زیر باشد:

fun purchase(
    packageToPurchase: اینپی فایPackage,
    onResult: (PurchaseResult) -> Unit,
)

fun restorePurchases(
    onResult: (RestoreResult) -> Unit,
)

نتیجه خرید بهتر است حداقل حالت‌های زیر را پوشش دهد:

sealed interface PurchaseResult {
    data class Success(val customerInfo: مشتریInfo?) : PurchaseResult
    data class PendingDelivery(val deliveryId: Long) : PurchaseResult
    data class Cancelled(val message: String?) : PurchaseResult
    data class Failed(val code: String?, val message: String?) : PurchaseResult
}

نام‌ها صرفاً پیشنهاد طراحی SDK هستند و جزو قرارداد HTTP محسوب نمی‌شوند.

انتخاب مسیر خرید

SDK باید مقدار storePlatform دریافت‌شده از تنظیمات اپ را مرجع انتخاب مسیر قرار دهد. انتخاب مسیر بر اساس اطلاعاتی که اپ میزبان به‌صورت دستی ارسال می‌کند مجاز نیست.

نوع اپمسیررفتار
DirectAndroid/app/v1/purchaseفرایند فعلی پرداخت مستقیم و دریافت لینک درگاه
DirectIos/app/v1/purchaseفرایند فعلی پرداخت مستقیم
DirectWeb/app/v1/purchaseفرایند فعلی پرداخت وب
Bazar/app/v2/store/purchasesخرید با Billing SDK بازار و سپس استعلام سرور به سرور
Myket/app/v2/store/purchasesخرید با Billing SDK مایکت و سپس استعلام سرور به سرور

پرداخت مستقیم

در پرداخت مستقیم، SDK نسخه ۲ باید همان درخواست فعلی /app/v1/purchase را با همان فیلدها و همان نحوه مدیریت redirect و callback ارسال کند.

SDK نباید برای یکسان‌سازی ظاهری، پاسخ پرداخت مستقیم را به API خرید استوری ارسال کند. یکسان‌سازی فقط باید در API عمومی SDK و نوع نتیجه‌ای که به اپ میزبان تحویل داده می‌شود انجام شود.

خرید استوری

فرایند خرید بازار و مایکت به ترتیب زیر است:

  1. SDK خرید را با Billing SDK رسمی فروشگاه آغاز می‌کند.
  2. پس از دریافت نتیجه موفق از فروشگاه، اطلاعات خرید خام در حافظه SDK نگه داشته می‌شود.
  3. SDK توکن خرید را به /app/v2/store/purchases ارسال می‌کند.
  4. سرور اینپی فای خرید را مستقیماً از سرور فروشگاه استعلام می‌کند.
  5. فقط پس از تأیید سرور، پرداخت ثبت و حق دسترسی فعال یا تمدید می‌شود.
  6. SDK نتیجه نهایی سرور را به اپ میزبان تحویل می‌دهد.

موفق بودن callback محلی فروشگاه به‌تنهایی به معنی فعال شدن حق دسترسی نیست.

درخواست ثبت خرید

POST /app/v2/store/purchases
Content-Type: application/json
{
  "apikey": "YOUR_APP_API_KEY",
  "token": "CUSTOMER_TOKEN",
  "appIdentifier": "com.example.app",
  "productIdentifier": "premium_monthly",
  "offeringIdentifier": "default",
  "country": "IR",
  "appVersion": "2.0.0",
  "forceVersion": 2,
  "operation": "purchase",
  "purchase": {
    "token": "STORE_PURCHASE_TOKEN",
    "purchaseTime": 1788076800000,
    "orderId": "OPTIONAL_STORE_ORDER_ID",
    "packageName": "com.example.app",
    "developerPayload": null,
    "originalJson": "OPTIONAL_RAW_PURCHASE_JSON",
    "signature": "OPTIONAL_PURCHASE_SIGNATURE"
  }
}

فیلدهای apikey، token، appIdentifier، productIdentifier، offeringIdentifier، country، appVersion، operation و purchase.token الزامی‌اند. سایر فیلدهای شیء purchase در صورت ارائه‌شدن توسط SDK فروشگاه ارسال می‌شوند.

SDK نباید قیمت، درصد تخفیف، حق دسترسی یا شناسه پروژه را به‌عنوان داده معتبر خرید ارسال کند. این مقادیر در سرور و از روی تنظیمات محصول محاسبه می‌شوند.

پاسخ خرید

{
  "status": true,
  "data": {
    "purchase": {
      "status": "COMPLETED",
      "paymentId": 1201,
      "eventId": 584,
      "alreadyProcessed": false,
      "deliveryId": null
    },
    "hasForceUpdate": false,
    "forceVersion": 2
  }
}

مقادیر فعلی data.purchase.status عبارت‌اند از:

وضعیتمعنیاقدام SDK
COMPLETEDخرید غیرمصرفی یا اشتراک ثبت شدنتیجه موفق و دریافت مجدد مشتریInfo
RESTOREDخرید قبلی بازیابی و دسترسی ثبت شدنتیجه موفق و دریافت مجدد مشتریInfo
ALREADY_PROCESSEDاین رویداد قبلاً ثبت شده استموفق در نظر گرفته شود؛ خرید تکرار نشود
DELIVERY_REQUIREDکالای مصرفی تأیید شده ولی هنوز تحویل نشده استتحویل محتوا با کلید deliveryId
CONSUME_REQUIREDمحتوا تحویل شده و فروشگاه باید consume شوداجرای consume یا تلاش مجدد

محصولات مصرفی

برای جلوگیری از از دست رفتن خرید یا تحویل دوباره، ترتیب عملیات کالای مصرفی باید دقیقاً رعایت شود:

  1. خرید در فروشگاه انجام می‌شود.
  2. سرور پاسخ DELIVERY_REQUIRED و یک deliveryId می‌دهد.
  3. اپ میزبان محتوا را به‌شکل idempotent و با کلید deliveryId تحویل می‌دهد.
  4. SDK تحویل موفق را به سرور اعلام می‌کند.
  5. SDK خرید را در Billing SDK فروشگاه consume می‌کند.
  6. نتیجه consume به سرور گزارش می‌شود.
Store purchase
    ↓
DELIVERY_REQUIRED
    ↓ deliver exactly once by deliveryId
POST /app/v2/store/deliveries/{deliveryId}/delivered
    ↓
CONSUME_REQUIRED
    ↓ consume in store SDK
POST /app/v2/store/deliveries/{deliveryId}/consume-result
    ↓
COMPLETED

اعلام تحویل

POST /app/v2/store/deliveries/584/delivered
Content-Type: application/json
{
  "apikey": "YOUR_APP_API_KEY",
  "token": "CUSTOMER_TOKEN"
}

فراخوانی تکراری این endpoint نباید باعث تحویل دوباره محتوا شود. اپ میزبان نیز باید deliveryIdهای تحویل‌شده را پایدار نگه دارد.

اعلام نتیجه consume

POST /app/v2/store/deliveries/584/consume-result
Content-Type: application/json

حالت موفق:

{
  "apikey": "YOUR_APP_API_KEY",
  "token": "CUSTOMER_TOKEN",
  "result": "succeeded"
}

حالت ناموفق:

{
  "apikey": "YOUR_APP_API_KEY",
  "token": "CUSTOMER_TOKEN",
  "result": "failed",
  "errorCode": "STORE_TEMPORARILY_UNAVAILABLE"
}

در صورت خطا، SDK باید تلاش consume را با backoff تکرار کند. پیش از دریافت تأیید تحویل از سرور، consume نباید انجام شود؛ چون در صورت شکست شبکه ممکن است خرید از فروشگاه حذف شود ولی محتوا به کاربر نرسیده باشد.

Restore خریدها

restorePurchases برای بازیابی اشتراک‌ها و محصولات غیرمصرفی در نصب مجدد، تعویض دستگاه یا ورود دوباره کاربر است.

SDK باید خریدهای موجود را از Billing SDK فروشگاه دریافت کند و هر خرید را جداگانه به /app/v2/store/purchases بفرستد. بدنه درخواست مشابه خرید عادی است، با این تفاوت:

{
  "operation": "restore"
}

قواعد Restore:

  • فقط برای Subscription و NonConsumable اجرا می‌شود.
  • محصول Consumable قابل Restore نیست.
  • هر توکن باید ابتدا در سرور فروشگاه تأیید شود.
  • رویداد ثبت‌شده دوباره حق دسترسی یا payment ایجاد نمی‌کند.
  • توکن متعلق به مشتری، اپ یا محصول دیگر پذیرفته نمی‌شود.
  • پس از پایان عملیات، SDK باید مشتریInfo را دوباره دریافت کند.
  • شکست یک آیتم نباید مانع بررسی سایر خریدهای قابل بازیابی شود؛ نتیجه نهایی باید تعداد موفق، تکراری و ناموفق را مشخص کند.

تمدید اشتراک بازار

در بازار، توکن اصلی اشتراک ممکن است در تمدیدها ثابت بماند. سرور هر دوره را با ترکیب توکن و validUntil تأییدشده توسط بازار تشخیص می‌دهد.

  • همان توکن با validUntil جدید یک تمدید جدید است.
  • همان توکن با همان validUntil رویداد تکراری است.
  • autoRenewing وضعیت تمدید خودکار را نشان می‌دهد، اما به‌تنهایی مجوز فعال‌کردن حق دسترسی نیست.
  • در صورت دریافت linkedSubscriptionToken، زنجیره قبلی و توکن جایگزین‌شده به یک اشتراک متصل می‌شوند.
  • تاریخ انقضای زنجیره هیچ‌گاه با پاسخ قدیمی به عقب برگردانده نمی‌شود.

SDK نباید با مقایسه محلی تاریخ‌ها حق دسترسی را تمدید کند؛ تصمیم نهایی پس از استعلام سرور به سرور گرفته می‌شود.

تمدید اشتراک مایکت

در مایکت هر خرید یا تمدید می‌تواند توکن متفاوتی داشته باشد. در نتیجه:

  • هر purchase token یک رویداد مستقل محسوب می‌شود.
  • SDK باید همه خریدهای فعال برگردانده‌شده توسط مایکت را sync کند.
  • ارسال دوباره همان توکن idempotent است و پرداخت تکراری ایجاد نمی‌کند.
  • SDK نباید منطق توکن ثابت بازار را برای مایکت اعمال کند.
  • مدت حق دسترسی بر اساس محصول و نتیجه استعلام معتبر سرور تعیین می‌شود.

همگام‌سازی هنگام راه‌اندازی SDK

در SDK نسخه ۲، پس از configure و شناسایی مشتری:

  1. اطلاعات فعلی مشتری دریافت شود.
  2. برای اپ‌های بازار و مایکت، خریدهای موجود از Billing SDK خوانده شوند.
  3. اشتراک‌ها و non-consumableها با عملیات restore به سرور sync شوند.
  4. خریدهای consumable تکمیل‌نشده وارد چرخه delivery/consume شوند، نه Restore.
  5. در پایان، مشتریInfo دوباره دریافت و به listener اپ تحویل شود.

همگام‌سازی باید در برابر اجرای هم‌زمان، retry شبکه و بازشدن چندباره اپ idempotent باشد.

مدیریت خطا و retry

  • خطای validation یا عدم تطابق اپ/محصول نباید بدون تغییر دوباره تلاش شود.
  • خطاهای موقت شبکه و فروشگاه باید با exponential backoff تکرار شوند.
  • cancel شدن صفحه خرید یک failure فنی نیست و باید جداگانه به اپ میزبان گزارش شود.
  • SDK نباید توکن خرید، API key، signature یا JSON خام خرید را در log تولیدی چاپ کند.
  • تا دریافت پاسخ قطعی سرور، نتیجه خرید استوری نباید موفق اعلام شود.
  • ALREADY_PROCESSED یک نتیجه idempotent موفق است، نه خطای خرید تکراری.

ذخیره‌سازی محلی SDK

SDK می‌تواند یک صف کوچک و رمزگذاری‌شده برای عملیات تأییدنشده نگه دارد. هر رکورد صف حداقل شامل نوع عملیات، فروشگاه، شناسه محصول و داده لازم برای ارسال مجدد است.

داده صف باید پس از یکی از حالت‌های زیر حذف شود:

  • پاسخ قطعی COMPLETED، RESTORED یا ALREADY_PROCESSED؛
  • تکمیل چرخه consumable با وضعیت COMPLETED؛
  • خطای دائمی که نیاز به اصلاح تنظیمات یا نسخه اپ دارد.

توکن‌های خرید نباید در log یا analytics قرار گیرند.

چک‌لیست پیاده‌سازی SDK

  • متد عمومی purchase برای سازگاری حفظ شده است.
  • پرداخت مستقیم همچنان به /app/v1/purchase می‌رود.
  • بازار و مایکت از /app/v2/store/purchases استفاده می‌کنند.
  • مقدار operation برای خرید purchase و برای بازیابی restore است.
  • Restore فقط اشتراک و non-consumable را پردازش می‌کند.
  • محصولات consumable پس از تحویل و تأیید سرور consume می‌شوند.
  • تحویل محتوا با deliveryId idempotent است.
  • retry شبکه باعث خرید، پرداخت، حق دسترسی یا تحویل تکراری نمی‌شود.
  • بازار بر اساس توکن زنجیره و تاریخ معتبر تمدید sync می‌شود.
  • مایکت هر توکن تمدید را مستقل sync می‌کند.
  • پس از خرید یا Restore موفق، مشتریInfo تازه دریافت می‌شود.
  • اطلاعات حساس خرید در log ثبت نمی‌شود.
  • رفتار SDK نسخه ۱ و اپ‌های فعلی بدون تغییر باقی مانده است.

سناریوهای ضروری تست SDK

  1. پرداخت مستقیم در SDK نسخه ۲ دقیقاً مانند نسخه قبل باز و تکمیل شود.
  2. خرید اشتراک بازار پس از تأیید سرور حق دسترسی ایجاد کند.
  3. ارسال دوباره همان تمدید بازار حق دسترسی را دوباره تمدید نکند.
  4. تمدید بازار با validUntil جدید یک دوره جدید ثبت کند.
  5. تمدید مایکت با توکن جدید یک رویداد جدید ثبت کند.
  6. ارسال دوباره توکن مایکت idempotent باشد.
  7. Restore اشتراک و non-consumable موفق باشد.
  8. Restore محصول consumable رد شود.
  9. قطع شبکه پس از خرید باعث باقی‌ماندن عملیات در صف retry شود.
  10. کالای consumable قبل از تأیید تحویل consume نشود.
  11. خطای consume قابل retry باشد و محتوا دوباره تحویل نشود.
  12. توکن مشتری دیگر یا اپ دیگر قابل استفاده نباشد.