لا يمكنك اختبار تكاملك إذا كان كل سيناريو يتطلب مشتريا حقيقيا وتحويلا حقيقيا ونقرة بشرية. لهذا توفر بيئة Sandbox خمس نقاط نهاية للمحاكاة تفرض كل مرحلة من دورة الحياة: إيداع الوصل، تأكيد استلام المال، الرفض، انتهاء الصلاحية، ومرور ساعة الفوترة.
لا شيء مزيف هنا: تمر هذه النقاط عبر آلة الحالات الحقيقية ومسار التأكيد الحقيقي وتصدر أحداث webhook الحقيقية. ما تلاحظه في Sandbox هو بالضبط ما ستفعله بيئة الإنتاج.
إيداع وصل الدفع
تؤدي POST /payments/intents/:id/simulate/proof حركة المشتري: ترفق وصلا اصطناعيا (ملف PDF مكتوب فيه «PREUVE SIMULEE - SANDBOX»، يظهر في لوحة التحكم مثل أي وصل آخر) وتنقل intent إلى الحالة proof_submitted. وترسل الأحداث payment.proof_submitted ثم payment.proof_analyzed ثم payment.proof_validated نحو نقطة الاستقبال الخاصة بك في Sandbox، مع تحليل محاكى حكمه match. وينتهي intent في الحالة proof_validated: الوصل متماسك، والدفعة تبقى في انتظار التأكيد.
curl -X POST https://api.lahsab.com/payments/intents/INTENT_ID/simulate/proof \
-H "x-api-key: lsk_sandbox_CLE" نفس الضوابط المطبقة على نقطة الإيداع العمومية: intent منتهي الصلاحية يرد بالخطأ 409 PAYMENT_INTENT_EXPIRED، و intent في حالة نهائية يرد بالخطأ 409 PAYMENT_INVALID_TRANSITION.
تأكيد استلام المال
تؤدي POST /payments/intents/:id/simulate/confirm نقرتك على زر تأكيد: يمر التأكيد عبر مسار الإشارة المعتاد، لذلك تحمل حمولة حدث payment.confirmed القيمة confirmationSource: "manual" كما في الإنتاج. ينتقل intent إلى confirmed، وإذا كان مرتبطا بفاتورة فإنها تسوى: يصدر invoice.paid وتتقدم فترة الاشتراك.
curl -X POST https://api.lahsab.com/payments/intents/INTENT_ID/simulate/confirm \
-H "x-api-key: lsk_sandbox_CLE" صالحة انطلاقا من pending أو proof_submitted أو proof_validated. والعملية عديمة الأثر عند التكرار (idempotent): استدعاء النقطة على intent مؤكد مسبقا يعيد intent المؤكد نفسه دون خطأ.
الرفض
ترفض POST /payments/intents/:id/simulate/reject الدفعة. الجسم { "reason": "…" } اختياري. ينتقل intent إلى rejected ويصدر payment.rejected.
curl -X POST https://api.lahsab.com/payments/intents/INTENT_ID/simulate/reject \
-H "x-api-key: lsk_sandbox_CLE" \
-H "content-type: application/json" \
-d '{ "reason": "Fonds jamais reçus" }' سيناريو «وصل مودع لكن المال لم يصل قط» (الحالة التي يجب أن يعرف كودك التعامل معها) تحصل عليه بتسلسل simulate/proof ثم simulate/reject.
انتهاء الصلاحية
تفرض POST /payments/intents/:id/simulate/expire انتهاء الصلاحية دون انتظار expiresAt. صالحة فقط انطلاقا من pending (وإلا 409 PAYMENT_INVALID_TRANSITION). تصدر payment.expired.
curl -X POST https://api.lahsab.com/payments/intents/INTENT_ID/simulate/expire \
-H "x-api-key: lsk_sandbox_CLE" تشغيل ساعة الفوترة
التجديد والتأخر في الدفع لا ينتجان عن نداء API: بل تنتجهما ساعة الفوترة التي تدور مرة كل ساعة. تنفذها POST /billing/simulate/tick الآن، محصورة في بياناتك في Sandbox، وتعيد تقريرها:
curl -X POST https://api.lahsab.com/billing/simulate/tick \
-H "x-api-key: lsk_sandbox_CLE" {
"expiredIntents": 0,
"issuedInvoices": 1,
"markedOverdue": 0,
"markedUncollectible": 0,
"closedSubscriptions": 0,
"purgedIdempotencyKeys": 0
}
| الحقل | ما يحصيه |
|---|---|
expiredIntents | كائنات intent بحالة pending انتقلت إلى expired (تجاوزت أجلها). |
issuedInvoices | فواتير التجديد الصادرة. |
markedOverdue | فواتير open انقضى أجل استحقاقها للتو. |
markedUncollectible | فواتير انتقلت إلى uncollectible (استنفدت مهلة السماح). |
closedSubscriptions | اشتراكات انتهت (إلغاء مبرمج بلغ أجله). |
purgedIdempotencyKeys | دائما 0 في الوضع المحصور. |
الحد الأقصى لهذه النقطة هو 6 نداءات في الدقيقة (429 RATE_LIMITED بعد ذلك).
مسار الاشتراك في وضع متسارع
سعر بقيمة interval: "day" مع الساعة المحاكاة: عدة دورات اشتراك كاملة في دقائق معدودة، بينما تستغرق الدورة الواحدة شهرا في الإنتاج.
1. الكتالوج والزبون
curl -X POST https://api.lahsab.com/products \
-H "x-api-key: lsk_sandbox_CLE" \
-H "content-type: application/json" \
-d '{ "name": "Abonnement Test" }'
# Un prix à la journée : un cycle complet dure 24 h, pas un mois.
curl -X POST https://api.lahsab.com/prices \
-H "x-api-key: lsk_sandbox_CLE" \
-H "content-type: application/json" \
-d '{
"productId": "PRODUCT_ID",
"amount": 500,
"type": "recurring",
"interval": "day"
}'
curl -X POST https://api.lahsab.com/customers \
-H "x-api-key: lsk_sandbox_CLE" \
-H "content-type: application/json" \
-d '{ "externalId": "user_test", "name": "Client Test", "phone": "0770000000" }' 2. إنشاء الاشتراك
curl -X POST https://api.lahsab.com/subscriptions \
-H "x-api-key: lsk_sandbox_CLE" \
-H "content-type: application/json" \
-d '{ "customerId": "CUSTOMER_ID", "priceId": "PRICE_ID" }' يولد الاشتراك بالحالة incomplete، وتصدر فاتورته الأولى، وتحمل الاستجابة latestInvoice.checkoutUrl، وهو رابط ينتهي بالمقطع /pay/INTENT_ID. هذا المقطع الأخير هو المعرف الذي تطبق عليه نقاط المحاكاة.
3. دفع الفاتورة الأولى
curl -X POST https://api.lahsab.com/payments/intents/INTENT_ID/simulate/proof \
-H "x-api-key: lsk_sandbox_CLE"
curl -X POST https://api.lahsab.com/payments/intents/INTENT_ID/simulate/confirm \
-H "x-api-key: lsk_sandbox_CLE" تنتقل الفاتورة إلى paid، وينتقل الاشتراك من incomplete إلى active، وتستقبل على نقطتك في Sandbox الأحداث payment.proof_submitted ثم payment.proof_analyzed ثم payment.proof_validated ثم payment.confirmed ثم invoice.paid.
4. تشغيل ساعة الفوترة
نفذ POST /billing/simulate/tick: يعود التقرير حاملا "issuedInvoices": 1، أي أن فاتورة التجديد صدرت لأن فترة اليوم الواحد تنتهي داخل نافذة الإصدار (invoiceLeadDays).
5. دفع فاتورة التجديد
استرجع رابط فاتورة التجديد (GET /subscriptions/:id ثم latestInvoice.checkoutUrl)، وأعد تنفيذ simulate/proof ثم simulate/confirm على intent الخاص بها. يصلك invoice.paid ويتقدم currentPeriodEnd بفترة واحدة بالضبط، انطلاقا من نهاية الفترة السابقة، وليس أبدا من «الآن».
الأخطاء
| الحالة | الاستجابة |
|---|---|
مفتاح lsk_live_… أو الجلسة في وضع الإنتاج | 400 ENVIRONMENT_NOT_ALLOWED |
| بدون مصادقة | 401 AUTH_UNAUTHORIZED |
| intent تابع لتاجر آخر، أو intent في بيئة live، أو معرف مجهول | 404 PAYMENT_INTENT_NOT_FOUND |
| انتقال غير ممكن (مثلا: جعل intent مؤكد منتهي الصلاحية) | 409 PAYMENT_INVALID_TRANSITION |
| وصل على intent منتهي الصلاحية | 409 PAYMENT_INTENT_EXPIRED |
| أكثر من 6 نداءات في الدقيقة على الساعة | 429 RATE_LIMITED |
أي intent خارج نطاقك (تاجر آخر أو بيئة أخرى) يرد بالرمز 404 وليس أبدا 403: فهو غير مرئي أصلا، لا ممنوع.
ماذا بعد؟
- محرك الاشتراكات وقاعدته الذهبية: الاشتراكات.
- التحقق من توقيع الأحداث المستلمة: Webhooks.
- النقاط الخمس بالتفصيل: مرجع API.