لا يمكنك اختبار تكاملك إذا كان كل سيناريو يتطلب مشتريا حقيقيا وتحويلا حقيقيا ونقرة بشرية. لهذا توفر بيئة 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، وتعيد تقريرها:

فرض tick فوري
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.