كل الأعمال

دراسة حالة · المدفوعات والحجز

B3 Academy — لا تضيع أي دفعة

منصة تعليم ورعاية تبيع ستة أنواع من المنتجات عبر pipeline دفع واحد، ويجب فيها ألّا يتحمّل العميل أبدًا تكلفة فشل يقع بعد الدفع.

المشكلة

B3 Academy أكاديمية لطب الأعشاب في الكويت. تجمع منصتها بين التعليم والرعاية: دورات فيها اختبارات قصيرة وشهادات، وكتب مطبوعة وكتب إلكترونية، واشتراكات مدفوعة تفتح محادثة جماعية، واستشارات في العيادة أو عبر الفيديو، وباقات استشارات، ورحلات. وتسرد المواصفات 129 حالة استخدام.

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

دوري

كنت مطوّر الـ Backend الوحيد وبنيت الـ Backend كاملًا: API العملاء ولوحة تحكم الإدارة التي خلفه.

المعمارية

  1. Controller
  2. FormRequest
  3. DTO
  4. Service
  5. Resource
مسار الطلب.

الدفع والفوترة كلٌّ منهما module مستقل. كل بوابة دفع هي strategy خلف interface مشتركة، ولا تُطلق الـ events الخاصة بالطلبات المدفوعة إلا بعد commit الـ transaction في قاعدة البيانات.

القرارات الأساسية

pipeline واحد لستة منتجات. كل عملية شراء تنشئ transaction دفع تحمل snapshot لما يُشترى، مع idempotency key فريد لكل عميل. وحين تردّ بوابة الدفع، توجّه service واحدة النتيجة إلى نوع التسليم المناسب.

لا تثق بالـ callback. الـ callback يخبر النظام بأن شيئًا ما حدث. بعدها يسأل النظام بوابة الدفع مباشرة عن حالة الدفعة ويتصرف بناءً على جوابها.

سجّل الدفعة قبل التسليم. هذا هو القرار الذي يشير إليه العنوان.

  1. إنشاء الـ transaction مع snapshot و idempotency key

    طلب ثانٍ بالمفتاح نفسه يُرجع الـ transaction الأولى.

  2. العميل يدفع عند بوابة الدفع

  3. التحقق من الحالة لدى بوابة الدفع

    الـ callback يشغّل التحقق فقط.

  4. تسجيل الدفعة ثم commit

    من هنا فصاعدًا، المال مسجَّل ومحسوب مهما حدث بعد ذلك.

  5. التسليم: حجز الجلسات ومنح صلاحية الوصول

    إذا فشل: تبقى الدفعة مسجَّلة وينتظر الطلب التسليم.

  6. إعادة المحاولة بالمفتاح نفسه تستأنف التسليم

    ولا تخصم المبلغ مرة أخرى.

فشل التسليم لا يسبب أبدًا rollback لسجل الدفعة.

locks حيث يمكن أن يتصادم شخصان. شراء باقة استشارات يأخذ lock على مستوى العميل و lock على مستوى الطبيب. وصفوف الـ transaction والطلب يؤخذ عليها lock أثناء تغييرها.

الجزء الصعب: موعد يُؤخذ بعد إتمام الشراء

الباقة تحجز عدة جلسات. وبين إتمام الشراء والتسليم قد يأخذ شخص آخر أحد هذه المواعيد. استرداد ثمن الباقة كلها بسبب موعد واحد ضائع حلّ خاطئ للعميل وللأكاديمية.

يُبقي النظام على عملية الشراء، ويضع علامة على الجلسة المتعارضة، ويُشعر العميل، ويتيح له اختيار وقت آخر. وأمر مجدول يرسل تذكيرات حتى يُحلّ التعارض.

النتيجة

  • نحو 510 دوالّ اختبار. اختبار شراء الباقة وحده يغطي فشلًا بعد الخصم، وإعادة محاولة تستأنف التسليم، و callbacks مكررة.
  • إعادة مجموعة اختبارات فاشلة إلى النجاح (69 اختبارًا فاشلًا) كشفت ستة أخطاء حقيقية، منها سجلات غير موجودة كانت تُرجَع كأخطاء خادم، و guard للمشرفين المحظورين لم يكن مطبّقًا قط.
  • بنيت أيضًا: رفع الملفات الكبيرة على أجزاء (chunked uploads)، وأسعارًا بعملات متعددة مع أسعار صرف مخزّنة في الـ cache وثلاثة مزوّدين احتياطيين، وستة أوامر مجدولة.

ما الذي سأفعله بعد ذلك

  • تشغيل اختبارات الـ locks على MySQL. هي تنجح على SQLite في الذاكرة، وهذا لا يثبت السلوك تحت تزامن حقيقي.
  • تقسيم service الحجز، وقد أصبحت أكبر class في المشروع.
  • إضافة pipeline يشغّل الاختبارات عند كل merge request.

السيرة الذاتية

تحميل PDF
صفحة السيرة الذاتية 1صفحة السيرة الذاتية 2