يدفع عميل بالبطاقة. تقول بوابة الدفع «نعم». لكن تطبيقك لا يعلم بذلك أبدًا: أغلق العميل التبويب عند صفحة النجاح، أو انقطعت إشارة هاتفه بين شاشة البنك وشاشتك. فخرجت أمواله ويقول طلبك «قيد الانتظار».

أو العكس. ترسل البوابة إشعارها مرتين لأن ردّك كان بطيئًا في المرة الأولى، فتُشحن محفظة العميل مرتين.

الفشلان ينبعان من سوء الفهم نفسه لطريقة عمل الدفع الإلكتروني. وهذا المقال عن إتقان هذا الجزء أيًّا كانت البوابة: Paymob أو فوري أو كاشير أو Stripe أو غيرها. تختلف تفاصيل كل مزوّد ووثائقه هي الفيصل في كل خلاف، لكن الشكل العام واحد في كل مكان.

رسالتان، وواحدة فقط هي الحقيقة

حين يدفع العميل، يصل نظامك ما يصل إلى شيئين:

  1. إعادة التوجيه (redirect). يعود متصفح العميل إلى موقعك، غالبًا مع بعض القيم في الرابط تقول إن العملية نجحت.
  2. الـ webhook (ويسمى أيضًا callback أو إشعار). يتصل خادم البوابة بخادمك مباشرة ويخبره بما جرى للدفعة.

إعادة التوجيه مجاملة للعميل. تعتمد على متصفح قد يُغلق أو تنقطع إشارته أو يُعبث به، وأي شخص يستطيع كتابة رابط نجاح في شريط العنوان. تعامل معها كصفحة تُعرض، لا كدليل على الدفع. أما الـ webhook، بعد التحقق منه، فهو الحقيقة.

فالتسلسل: أنشئ الطلب، وأنشئ الدفعة عند البوابة، وأرسل العميل ليدفع، وانتظر الـ webhook الموثَّق لينقل الطلب إلى الأمام. وصفحة العودة تستطيع أن تقول «نؤكد دفعتك» ثم تستعلم من خادمك إلى أن يصل الـ webhook.

الخطوة الأولى: تحقق أن الرسالة حقيقية

عنوان الـ webhook عندك عام. يستطيع أي شخص أن يرسل إليه، فأول ما يفعله المعالج أن يتأكد أن البوابة هي التي أرسلت.

أغلب البوابات توقّع إشعاراتها بسر مشترك باستخدام HMAC. تحسب أنت التوقيع من الرسالة بنفسك وتقارنه بالذي وصل. والوصفة (أي الحقول، وبأي ترتيب، وبأي خوارزمية) في وثائق المزوّد، وبعضهم يوقّع جسم الطلب الخام وبعضهم يوقّع قائمة حقول تُضم بترتيب ثابت. اتبعها بحذافيرها.

وهناك تفصيلتان مهمتان:

  • قارن بزمن ثابت باستخدام hash_equals لا ==، كي لا تسرّب المقارنة موضع الاختلاف بين النصين.
  • إن كان التوقيع يغطي الجسم الخام، فتحقق من البايتات التي وصلتك. لا تفكّ JSON ثم تعيد ترميزه أولًا، فهذا يغيّر البايتات.
$expected = hash_hmac('sha512', $signedString, config('payments.hmac_secret'));

if (! hash_equals($expected, $received)) {
    Log::warning('Payment notice with a bad signature', ['ip' => $request->ip()]);

    return response('Invalid signature', 401);
}

الطلب الذي يفشل هنا ليس دفعة. سجّله، وردّ بخطأ، ولا تفعل شيئًا آخر.

الخطوة الثانية: عالج كل دفعة مرة واحدة

قد يصل الـ webhook مرتين، أو متأخرًا، أو بغير ترتيب. تعيد البوابات المحاولة كلما لم تتلقَّ ردًّا ناجحًا سريعًا، ولا تنسّق مع حال خادمك. والقاعدة التي تصمد أمام كل هذا بسيطة: وصول الإشعار نفسه مرة أخرى لا يجب أن يغيّر شيئًا.

اجعل هذا صحيحًا في قاعدة البيانات، لا في الأمل:

  • احتفظ بسجل خاص بك لكل دفعة، فيه رقم الطلب ورقم العملية عند البوابة، وحالة: معلقة أو مدفوعة أو فاشلة أو مستردة.
  • ضع فهرسًا فريدًا على رقم العملية عند البوابة.
  • عالج الإشعار داخل معاملة تقفل صف الدفعة، وتتوقف مبكرًا إن كانت الدفعة في حالة نهائية.
DB::transaction(function () use ($notice) {
    $payment = Payment::where('provider_order_id', $notice['order_id'])
        ->lockForUpdate()
        ->first();

    if (! $payment) {
        Log::error('Notice for an unknown payment', $notice);

        return;
    }

    if ($payment->isFinal()) {
        return; // a repeat, or a late duplicate: nothing to do
    }

    if ($notice['amount_minor'] !== $payment->amount_minor
        || $notice['currency'] !== $payment->currency) {
        $payment->markMismatch($notice); // do not mark as paid; tell a human

        return;
    }

    if ($notice['pending']) {
        $payment->recordPending($notice);
    } elseif ($notice['success']) {
        $payment->markPaid($notice['transaction_id'], $notice);
    } else {
        $payment->markFailed($notice);
    }
});

والآثار التي تتبع الدفع (تأكيد الطلب، شحن المحفظة، إرسال رسالة) مكانها داخل المعاملة نفسها، أو في عمل يُوضع في الطابور منها. فلا يفترق «مدفوع» عن «شُحنت المحفظة».

الخطوة الثالثة: ثق بسجلك أنت في المبلغ

يقول الإشعار ما دُفع. وتقول قاعدة بياناتك ما كان يجب أن يُدفع. قارن المبلغ والعملة بطلبك أنت، لا بمتصفح العميل. وإن اختلفا، فلا تعلّم الطلب مدفوعًا: سجّل عدم تطابق وأخبر شخصًا. هذا الفحص وحده يمنع صنفًا كاملًا من الهجمات، ويلتقط أيضًا الأخطاء البريئة في طريقة إرسال المبالغ إلى البوابة (الجنيه مقابل القرش مثال كلاسيكي).

الخطوة الرابعة: أجب بسرعة، وقل ما تعنيه

البوابة تنتظر ردّك. احفظ ما يجب حفظه ثم أرجع 2xx بسرعة. وإن كان العمل المرتبط بالدفعة بطيئًا، كإرسال البريد أو إنشاء الفواتير، فضعه في الطابور وأجب.

وما تُرجعه حين يكون هناك خطأ قرار حقيقي:

  • توقيع غير صالح: خطأ، ولا يُعالَج شيء.
  • عطل مؤقت عندك (قاعدة البيانات متوقفة): خطأ، لتعيد البوابة المحاولة. وهذا بالضبط ما صُمّمت له إعادة المحاولة.
  • إشعار لدفعة لا تعرفها: سجّله بصوت عالٍ وأجب بنجاح، وإلا استمرت البوابة في إعادة شيء لن يطابق أبدًا. لكن نبّه شخصًا، لأن ذلك يعني غالبًا خطأ برمجيًّا.

الخطوة الخامسة: الحالات التي ستقابلها

لا تصمّم الدفعة على أنها مدفوعة أو غير مدفوعة. الدفعات الحقيقية لها حالات أكثر:

الحالة معناها ما تفعله
معلقة لم يُنهِ العميل، أو طريقة بنكية أو كشك ما زالت تنتظر أبقِ الطلب مفتوحًا واعرض رسالة واضحة
مدفوعة مؤكدة وتطابق الطلب نفّذ، مرة واحدة
فاشلة رُفضت أو انتهت اسمح بمحاولة جديدة؛ وأفرج عن أي مخزون محجوز
مستردة رُدّ المال اعكس ما فعلته الدفعة
عدم تطابق دُفعت لكن المبلغ أو العملة خاطئة احجزها للمراجعة

وبعض الطرق، كالدفع النقدي عبر كشك، قد تستغرق أيامًا لتصبح مدفوعة. والطلب الذي «فشل» قبل ساعة قد يصبح مدفوعًا لاحقًا، فقرّر مسبقًا ما يحدث حين يصل مال لطلب ألغيته. والجواب في الغالب استرداد، والأفضل أن يكون مساره مكتوبًا قبل أن يقع.

الخطوة السادسة: لا تعتمد على الـ webhooks وحدها

قد تضيع الـ webhooks: خادمك توقف ساعة، أو جدار حماية مضبوط خطأ، أو نشر أسقط طلبات. أضف شبكة أمان: عملًا مجدولًا ينظر في الدفعات التي بقيت معلقة بعد مدة، ويسأل واجهة البوابة عن حالتها، ويطبّق الجواب عبر الدالة نفسها التي يستخدمها الـ webhook. وفي كل يوم قارن إجمالي المدفوع عندك بتقرير البوابة نفسه.

واحتفظ بالإشعار الخام كذلك. فحين يعترض عميل على خصم بعد ستة أشهر، فالحمولة الأصلية بتوقيعها أفضل دليل عندك.

ماذا تختبر

  • الـ webhook نفسه مرسلًا مرتين: أثر واحد.
  • مبلغ أو توقيع جرى العبث به: مرفوض.
  • إشعار قبل أن يوجد سجل الطلب، وإشعار لطلب غير معروف.
  • نجاح ثم فشل، وفشل ثم نجاح، بالترتيبين.
  • إشعار معلق يتبعه مدفوع.
  • مال يصل لطلب ملغى.
  • المعالج يفشل في منتصف العمل: لا شيء يبقى نصف منفَّذ.

قائمة مراجعة

  • إعادة التوجيه صفحة لا دليل.
  • كل إشعار يُتحقق منه بمقارنة توقيع بزمن ثابت.
  • للدفعات حالة، ورقم فريد عند البوابة، وصف مقفول أثناء تغيّرها.
  • المبلغ والعملة يُقارنان بطلبك أنت.
  • النجاح يُؤكَّد بسرعة، والعمل البطيء يُوضع في الطابور.
  • الإشعارات المجهولة تُسجَّل وتُنبَّه.
  • عمل مجدول يوفّق ما فاتته الـ webhooks.
  • الإشعارات الخام محفوظة.

وإن كنت تربط بوابة دفع بمتجر أو نظام حجز أو محفظة، وتريد مراجعة مسار الدفع قبل الإطلاق، حدّثنا عنه. أخطاء الدفع هي النوع الباهظ.