بوابة الدفع الأولى سهلة. تتبع SDK المزوّد، توصّل كنترولر، وتُطلق. المشكلة تبدأ من الثانية — وحين طلب عميل دعم خامسة، كان أسلوب "أضف if آخر" سيحوّل الدفع إلى مستنقع.

ما أبقاه نظيفاً عبر سبع بوابات هو رفضي أن يعرف باقي التطبيق أيّ مزوّد يخاطب. التطبيق يطلب دفعة؛ ولا يهمّه من يعالجها.

عقد واحد تلتزم به كل بوابة

كل شيء يبدأ بواجهة واحدة. كل مزوّد يُنفّذها، ولا شيء خارج طبقة الدفع يرى كلاس مزوّد مباشرة.

interface PaymentGateway
{
    public function charge(PaymentRequest $request): PaymentResult;

    public function verifyWebhook(Request $request): WebhookEvent;
}

PaymentRequest وPaymentResult كائنات قيمة خاصة بي، لا بالمزوّد. هذا ما يتخطّاه الناس، وهو الأهم: لحظة تسرّب مصفوفة بشكل Paymob إلى كنترولر، ترتبط بـPaymob للأبد. الترجمة من وإلى أشكالي عند الحدود هي ما يجعل المزوّدين قابلين للاستبدال.

درايفر لكل مزوّد، يُحلّ بالاسم

كل بوابة كلاس صغير مكتفٍ بذاته. لا تشير إحداها للأخرى.

final class PayTabsGateway implements PaymentGateway
{
    public function charge(PaymentRequest $request): PaymentResult
    {
        $response = Http::withToken(config('services.paytabs.key'))
            ->post('https://secure.paytabs.com/payment/request', [
                'cart_amount' => $request->amount,
                'cart_currency' => $request->currency,
                // ...التحويل الخاص بالمزوّد يعيش هنا فقط
            ]);

        return PaymentResult::fromPayTabs($response->json());
    }
}

مدير صغير يحلّ الدرايفر الصحيح من الإعدادات، فيبقى باقي التطبيق محايداً تجاه المزوّد:

$gateway = PaymentManager::driver($order->gateway); // 'paytabs', 'paymob', ...
$result  = $gateway->charge($paymentRequest);

إضافة البوابة الثامنة الآن كلاس جديد وسطر إعداد واحد. لا يتغيّر أي ملف قائم — وهذا بالضبط مبدأ المفتوح/المغلق يؤدّي عمله، وبالضبط ما تريده قرب المال.

الـWebhooks حيث المال الحقيقي — والأخطاء الحقيقية

نداء الدفع هو النصف السهل. معظم الأعطال التي صحّحتها تعيش في الـwebhook: المزوّد يعاودك لتأكيد الدفع، وذلك النداء عدائي افتراضياً. قد يصل مرتين، أو خارج الترتيب، أو مزوّراً.

ثلاث قواعد لا أكسرها:

  • تحقّق من التوقيع أولاً، قبل قراءة أي شيء. كل مزوّد جادّ يوقّع webhooks. إن فشل التوقيع فهو 403 لا دفعة.
  • اجعل المعالجة idempotent. أخزّن معرّف المعاملة بقيد فريد وأعامل المكرّر كنجاح دون إعادة إضافة رصيد. إطلاق الـwebhook مرتين يجب أن يكون بلا أثر، لا طلباً مزدوجاً.
  • أقرّ بسرعة واعمل لاحقاً. تحقّق، خزّن الحدث الخام، أعِد 200، وادفع العمل الثقيل (التنفيذ، الإيميلات، الفواتير) إلى طابور. المزوّدون يعيدون المحاولة عند المهلة، ومعالج webhook بطيء يصبح بهدوء آلة تسليم مكرّر.
public function handle(Request $request, string $provider)
{
    $event = PaymentManager::driver($provider)->verifyWebhook($request); // يرمي عند توقيع خاطئ

    ProcessPaymentEvent::dispatch($event); // في طابور، idempotent

    return response()->noContent(); // 200 فوراً
}

ماذا يمنحك هذا

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