استكشاف الأخطاء وإصلاحها
لا يصلني الـ webhook
التشخيص
- لوحة التحكم ← Webhooks ← السجلات: هل يظهر الطلب في القائمة؟
- لا ← لم يُطلَق الحدث. تأكّد من أن المعاملة وصلت إلى
success/failed/cancelled/expired. افرض استعلامًا عبرPOST/v1/payments/{id}/verify. - نعم، مع
http_statusغير 2xx ← رفض خادمك الطلب. راجع «نقطة الاستقبال ترد بـ 4xx/5xx». - نعم، مع
error_message: "timeout"← استغرق خادمك أكثر من 15 ثانية. راجع «انتهاء المهلة».
- لا ← لم يُطلَق الحدث. تأكّد من أن المعاملة وصلت إلى
نقطة الاستقبال ترد بـ 4xx/5xx
| http_status | السبب المعتاد |
|---|---|
| 400 | تحليل الجسم: يقرأ خادمك الحمولة قبل التحقق من التوقيع |
| 401 / 403 | وسيط مصادقة يحجب الطلبات الخارجية: استثنِ /webhooks/soamtar |
| 404 | مسار خاطئ: تحقّق من الرابط الدقيق في لوحة التحكم |
| 422 | تحقق إطار العمل: اقبل الحمولة كما هي |
| 500 | خطأ في التطبيق: راجع سجلاتك |
| 502 / 503 | الوكيل العكسي متوقف أو التطبيق لم يبدأ |
الحل: استثنِ مسار الـ webhook من حماية CSRF (مثل withoutMiddleware([VerifyCsrfToken::class]) في Laravel) ومن وسيط المصادقة لديك.
انتهاء المهلة
يجب أن يرد معالجك بـ 2xx في أقل من 15 ثانية. للمعالجة الطويلة:
// Bad
public function webhook(Request $request) {
$this->processOrder($request); // 30 seconds
$this->sendEmail($request);
return response('ok');
}
// Good
public function webhook(Request $request) {
ProcessSoamtarWebhookJob::dispatch($request->all());
return response('ok'); // immediate
}يتلقى خادمي الـ webhook خمس مرات
هذه إعادة محاولة، وهي متوقعة. الأسباب:
- ترد بـ 2xx لكن بعد أكثر من 15 ثانية ← سجّلت SOAMTAR انتهاء المهلة
- ترد برمز غير 2xx
- ترد بـ 2xx لكن الاستجابة لا تصل إلى SOAMTAR أبدًا (مشكلة شبكة)
الحل: أزل التكرار من جهتك بالاعتماد على الزوج (event، data.id).
CREATE UNIQUE INDEX uq_inbound_event ON your_webhook_log(event, transaction_id);توقيع webhook غير صالح
العَرَض
يفشل التحقق من HMAC رغم استخدامك السرّ الصحيح.
السبب 1: تعديل الجسم قبل التحقق
تحلّل Express وSymfony وLaravel الـ JSON تلقائيًا. يجب أن تتحقق من التوقيع على الجسم الخام، قبل أي تحليل.
// Bad
app.use(express.json()) // parses JSON
app.post('/webhook', (req, res) => {
verify(req.body) // req.body is already an object, not the raw body
})
// Good
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
verify(req.body.toString()) // raw
const event = JSON.parse(req.body.toString())
})السبب 2: ترميز HMAC
قارن القيم الست عشرية بأحرف صغيرة على الجانبين (تُعيد hash_hmac في PHP أحرفًا صغيرة؛ تحقّق من ذلك في Node). استخدم hash_equals / crypto.timingSafeEqual وليس ===.
السبب 3: طابع زمني منتهٍ
يُرفض |now - t| > 300s. تحقّق من ساعة خادمك (NTP). لا توسّع هامش التسامح لمعالجة عمليات تسليم قديمة: أعد إرسالها من Dashboard → Webhook logs → Resend (تُوقَّع كل محاولة من جديد بطابع زمني حديث) أو استخدم استعلام verify.
تعارض Idempotency-Key
العَرَض
409 idempotency_conflict عند إعادة إرسال الطلب.
التشخيص
curl -X POST .../v1/payments \
-H "Idempotency-Key: abc-123" \
-d '{"merchant_ref":"ORDER-7","amount":"150.00","currency":"SLE"}'
# 201
curl -X POST .../v1/payments \
-H "Idempotency-Key: abc-123" \
-d '{"merchant_ref":"ORDER-7","amount":"150.00","currency":"SLE","description":"oops"}'
# 409 — different bodyالحل:
- لـإعادة المحاولة: المفتاح نفسه والجسم نفسه تمامًا.
- لـطلب جديد: مفتاح جديد.
الممارسة الجيدة
ولّد المفتاح عندما ينقر العميل على «ادفع» واحفظه (تخزين محلي أو قاعدة بيانات) حتى النتيجة النهائية. لا تولّده من جديد في كل إعادة محاولة HTTP.
معاملة عالقة في pending
التشخيص
curl -X POST https://app.so-amtar.com/v1/payments/{id}/verify \
-H "Authorization: Bearer sk_test_xxx"يستعلم verify من Orange Money ويزامن الحالة. إذا أكّد Orange Money، تتقدّم المعاملة ويُرسَل إليك webhook.
إذا لم يتغيّر شيء
- لم يؤكد العميل طلب Orange Money برمز PIN (يبقى الطلب مفتوحًا نحو 5 دقائق): اطلب منه التحقق من هاتفه، أو ابدأ دفعة جديدة.
- لدى Orange Money حادث: استفسر من الدعم.
- تنتهي صلاحية المعاملة تلقائيًا عند
expires_at(ساعتان افتراضيًا). - إذا بقيت عالقة: راسل
support@so-amtar.comمع مرجع SOAMTAR (TXN-...).
استرداد دفعة Orange Money
استخدم POST/v1/refunds (بمفتاح يملك الصلاحية can_refund):
- أرسل
transaction_idالخاص بالدفع؛ احذفamountلاسترداد المبلغ المتبقي أو حدّده لاسترداد جزئي. - يُعاد المبلغ إلى رقم Orange Money الخاص بالدافع ويُخصم من رصيدك. تبقى عملية الاسترداد في الحالة
processingحتى تأكيد Orange، وتصلك النتيجة عبرrefund.success/refund.failed. - إذا تلقيت
refund_payer_phone_missing، فالدفع لا يحمل رقم الدافع: تواصل مع الدعم.
راجع الاسترداد (بالإنجليزية).
الرصيد المعروض لا يطابق تسوياتي
| المكوّن | مُدرج في الرصيد المعروض؟ |
|---|---|
success لم يُسوَّ بعد | نعم |
pending | لا |
success مُسوّى سابقًا | لا |
| استرداد أو دفعة صادرة | نعم (مخصوم) |
| اعتراض على الدفع مفتوح | محجوز في رصيد منفصل |
يختلف الرصيد «المتاح» عن الرصيد «المحاسبي»؛ وتعرض لوحة التحكم كليهما.
مفتاح live لا يجد الكائن الخاص بي
أنت تستخدم مفتاح live على كائن أُنشئ في بيئة الاختبار (أو العكس). استخدم مفتاح الوضع المطابق: ترتبط المعرّفات بالوضع بشكل صارم.
مشكلات الاتصال بالواجهة البرمجية (SSL، انتهاء المهلة…)
خطأ SSL: SSL certificate problem: unable to get local issuer certificate
يظهر عادةً على خوادم Windows أو في تثبيتات PHP/cURL القديمة التي تفتقر إلى حزمة شهادات CA محدّثة.
الحل:
- نزّل الملف الرسمي
cacert.pemمن curl.se/ca/cacert.pem. - وجّه
php.iniإليه:inicurl.cainfo = "C:/chemin/vers/votre/cacert.pem" openssl.cafile = "C:/chemin/vers/votre/cacert.pem" - أعد تشغيل خادم الويب.
خطأ: Connection Timeout
إذا تعذّر على خادمك الوصول إلى app.so-amtar.com:
- جدار الحماية: يجب السماح لخادمك بإرسال طلبات صادرة على المنفذ 443 (HTTPS).
- DNS: تأكّد من أن
app.so-amtar.comيُحلّ من خادمك. - الوكيل (Proxy): إذا كان خادمك يستخدم وكيلًا، فاضبط SDK وفقًا لذلك (أو متغيرات البيئة
HTTP_PROXY).
خطأ: 401 Unauthorized (مفتاح غير صالح)
- استخدم المفتاح السرّي (
sk_...) في الاستدعاءات من الخادم الخلفي، لا المفتاح العام (pk_...). - تحقّق من الوضع: لا يعمل مفتاح
sk_test_إلا مع كائنات بيئة الاختبار. - تأكّد من أنك لم تنسخ مسافات أو فواصل أسطر مع المفتاح.
لمعرفة المزيد
- الدعم:
support@so-amtar.com