دراسة حالة · توزيع الطلبات
DropCare — إيصال الوصفة الطبية إلى الصيدلية المناسبة
منصة توصيل من الصيدليات تعرض كل طلب على الصيدليات على شرائح، ثم توسّع البحث عن مندوب توصيل حتى يقبله أحدهم.
المشكلة
يصوّر العميل وصفة طبية، أو يسجّل رسالة صوتية، ويريد أن يصله الدواء. على المنصة أن تجد صيدلية تجهّز الطلب، وتتفق على السعر، وتحصّل المبلغ، وتجد مندوب توصيل. يشارك في ذلك خمسة أنواع من المستخدمين: العملاء، والصيدليات مع صيادلتها، وشركات التوصيل، ومندوبو التوصيل، والمشرفون. ويمر الطلب بـ 14 حالة.
دوري
قدت الـ Backend وكتبت معظمه: توزيع الطلبات، والمحفظة، والفواتير، والتسويات، والكوبونات، ومحادثة الدعم، والتقارير المالية، وخادم الـ realtime، و pipeline النشر. انضم مطوّر ثانٍ خلال جزء من المشروع. كنت أحدّد له المهام وأراجع الـ merge requests الخاصة به.
القرارات الأساسية
الطلب يُعرض على الصيدليات على شرائح. قاعدة صاحب المشروع كانت أن الصيدليات التي تقدّم للعملاء خصمًا أعلى ترى الطلب أولًا. نفّذت ذلك على شكل fan-out على مراحل.
العميل يرسل طلبًا
مع صور الوصفة الطبية أو رسالة صوتية.
البحث عن الصيدليات ضمن النطاق
استعلام بالمسافة حول عنوان التوصيل.
إشعارها شريحة بعد شريحة
مجمّعة حسب الخصم، الأعلى أولًا. كل شريحة أدنى هي job في الـ queue بتأخير قصير.
صيدلية تقبل وترسل فاتورة
القبول يفتح غرفة محادثة. و job لانتهاء المهلة يعالج الحالة التي لا يردّ فيها أحد.
العميل يدفع
بالمحفظة أو نقدًا أو عبر Apple Pay.
البحث عن مندوب توصيل في حلقة تتسع
job مؤجّل يعيد جدولة نفسه بنصف قطر أكبر، حتى 100 كم.
الاستلام، التوصيل، التسوية
بحث عن مندوب توصيل يوسّع نفسه. إشعار كل مندوبي التوصيل في المدينة دفعة واحدة مزعج، ونصف القطر الثابت قد يترك الطلب عالقًا. يبدأ البحث قريبًا ثم يتسع.
- كل جولة تبحث في حلقة، من نصف القطر السابق إلى الجديد، فلا يُشعَر أحد مرتين لأنه يقع داخل الاثنين.
- قبل كل جولة يقرأ الـ job الطلب من جديد تحت row lock. وإذا كان مندوب قد قبل الطلب، يتوقف.
- جدول ledger يسجّل من تم إشعاره، وعليه قيد تفرّد (unique constraint). وإذا أُعيدت محاولة job وحاول إشعار المندوب نفسه مرة أخرى، ترفض قاعدة البيانات ذلك دون أي ضرر.
ثلاثة أطراف تتقاسم كل دفعة. الصيدلية ومندوب التوصيل والمنصة، يأخذ كل منهم جزءًا. العمولات تشمل ضريبة القيمة المضافة، وبعض المنتجات خاضع للضريبة وبعضها غير خاضع، والكوبون قد يستهدف رسوم الخدمة أو عمولة التوصيل. service واحدة تحسب التفصيل المالي، والتسويات تقرأ منه.
محفظة تصمد أمام نقرتين. عمليات الخصم والشحن تأخذ lock على صف المحفظة. طلبات شحن الرصيد تمر بموافقة المشرف، مع حماية تمنع الموافقة على الطلب نفسه مرتين.
الـ realtime خارج PHP. المحادثة والمكالمات والتتبع المباشر لمندوب التوصيل تعمل على خادم Node صغير مع Socket.IO، فيبقى تطبيق Laravel بلا حالة (stateless).
بنيت أيضًا
- فواتير مع QR code وملف PDF، يمكن الوصول إليها عبر رابط عام يحمل token.
- سجل حالات يكتبه observer عند كل انتقال، وتقرأ منه jobs التذكير.
- pipeline على GitLab ينشر عبر SSH ويُخرج الموقع دائمًا من وضع الصيانة، حتى عند فشل إحدى الخطوات.
- تقارير مالية للمشرفين مع تصدير إلى Excel.
النتيجة
خمس واجهات API ولوحة إدارة، والتوزيع والدفع والتسوية تعمل من أول طلب حتى صرف مستحقات المزوّد.
ما الذي سأفعله بعد ذلك
- تطبيق نمط «lock ثم إعادة التحقق» المستخدم في البحث عن مندوب التوصيل على قبول الصيدليات، حتى تحسم قاعدة البيانات حالة قبول صيدليتين في اللحظة نفسها.
- إضافة اختبارات آلية للحسابات المالية و jobs التوزيع.