# مقارنة: خوارزمية اقتراح الكابتن الحالية مقابل أفضل الممارسات (Best Practice)

**الملف:** توثيق تطور خوارزمية `suggestCaptains()` في مشروع كابيتانو للوجستيات.
**تاريخ الإعداد:** 2026-09-10
**المستندات المرتبطة:** `docs/feature-04-manual-order-assignment.md` · `docs/feature-04-manual-test-flow.md`

---

## 1. الوضع الحالي (خوارزمية Feature 04)

المصدر: `app/Services/Order/OrderService.php` → `suggestCaptains()`
+ `app/Repositories/Driver/DriverRepository.php` → `suggestableCaptains()`
+ `app/Services/Order/DistanceService.php`

### خطوات الخوارزمية الحالية

1. **الفلترة (من يستحق أن يظهر):** الكابتن لا يُقترح إلا إذا تحققت كل الشروط:
   - `status = approved` (موافق عليه)
   - `is_active = true` (حسابه مفعّل)
   - له سجل `driver_availabilities` بقيمة `is_online = true` (متاح الآن)
   - لديه موقع مسجّل في `driver_locations` (أبلغ عن موقع)
   - **لا يحمل طلباً** في الحالات `assigned / picked_up / on_the_way` (غير مشغول)

2. **المسافة:** تُحسب بالخطّ المستقيم (Haversine) بين:
   - نقطة الالتقاط `pickup_lat/lng` للطلب
   - **آخر موقع مُبلَّغ عنه** للكابتن في جدول `driver_locations`

3. **الترتيب:** تصاعدياً حسب `distance_km`، وعند تساوي المسافة يُرتَّب حسب اسم الكابتن (استقرار القائمة).

4. **التسليم:** لا يوجد تعيين تلقائي — القائمة مجرّد ترشيح مرتّب، والتخصيص النهائي عبر `/assign` (يعيد التحقق من نفس الشروط).

### نقاط القوة في الحل الحالي
- بسيط وسريع التنفيذ، بدون اعتماديات خارجية (يعمل بدون إنترنت/مفاتيح).
- متوافق مع قاعدة بيانات الاختبار (SQLite) لأن الحساب يتم في PHP وليس في DB.
- قاعدة "المشغول لا يُعرض له طلب ثانٍ" تمنع تحميل كابتن فوق طاقته.

### نقاط الضعف (الأسباب الحقيقية لتحسينها)

| الثغرة | الوصف | الأثر |
|---|---|---|
| **خط مستقيم وليس طريقاً** | Haversine يهمل الشوارع والاتجاهات الواحدة والجسور؛ في المدن الشبكية (مثل الرياض) المسافة الفعلية أطول بنحو 20–40% | ترتيب مضلّل: أقرب "بطائر" قد يكون أبعدهم "بالسيارة" |
| **موقع قديم ثابت** | المسافة محسوبة من **آخر** موقع مُبلَّغ، مهما كان عمره (ساعات/أيام) | "الاقتراح الأقرب" قد لا يكون موجوداً في مكانه أصلاً |
| **لا توجد سرعة/اتجاه** | لا يُؤخذ: هل الكابتن يتحرك؟ نحو أين؟ | إسقاط موقعه الحالي غير ممكن؛ القيمة «حاضر/قديم» غير معروضة للديسباتشر |
| **لا يوجد ETA** | لا نعرض وقت وصول الكابتن للالتقاط؛ المسافة وحدها لا تكفي لتحميل "من سيصل أولاً" | المستخدم يُتخذ قراره على مقياس ناقص |
| **استثناء المشغول نهائياً** | أي كابتن في طلب قيد التنفيذ يُستبعد حتى لو كان على وشك إنهاء التسليم | قد يفوتنا الكابتن الذي سيصل أولاً فعلياً |
| **فحص شامل O(N)** | تُحسب المسافة لكل الكابتن المؤهلين في كل طلب، بدون فهرسة مكانية | لا يتحمل الكثافة/الحجم الكبير |
| **الأفضل "الأقرب" فقط** | لا يدخل في الترتيب: التقييم، تاريخ القبول/الرفض، سعة المركبة، المنطقة الأقرب لعمل الكابتن | الترتيب لا يعبّر عن "الأنسب" بل عن "الأقرب قياساً هندسياً" |

---

## 2. الخوارزمية الأفضل ممارسةً (Best Practice المقترحة)

### التدفق المرحلي المقترح

```
الكابتن المؤهل
   │  (approved + active + online + له موقع حديث < 10 دقائق + غير مشغول)
   ▼
فلترة مكانية مسبقة (Spatial pre-filter)
   │  (نقطة POINT + ST_Distance_Sphere ≤ مثلاً 40 كم) ← يقلّص عدد المرشحين
   ▼
احتساب مسافة الطريق + زمن الوصول لكل مرشح (Routing / ETA)
   │  (مزود توجيه: Google Routes / OSRM / GraphHopper / Here)
   │  عند فشل المزود → بديل سريع: Haversine × 1.25 + وقت أساسي
   ▼
الترتيب حسب ETA (وليس كم) ، ثم بالاسم عند التساوي
   ▼
الإخراج: { captain, distance_km, eta_minutes, captured_at (عمر الموقع) }
```

### التحسينات الموصى بها (مرتّبة حسب الجدوى والأثر)

| # | التحسين | الوصف | الأثر |
|---|---|---|---|
| 1 | **فلتر حداثة الموقع** | استبعاد الكابتن الذي `captured_at` أقدم من نافذة زمنية (10 دقائق افتراضياً) | يمنع "شبح الكابتن" — الموقع القديم لا يُعتد به |
| 2 | **إظهار عمر الموقع + ETA** | إضافة `captured_at` و `eta_minutes` لردّ الـ API حتى يرى الديسباتشر «آخر ظهور قبل 12 دقيقة» | شفافية القرار في الواجهة |
| 3 | **فهرسة مكانية** | عمود `POINT` + `ST_Distance_Sphere` (MySQL 8) لتصفية المرشحين ضمن نطاق قبل الحساب الدقيق | تقلّص تكلفة الـ O(N) وتجهّز النظام للحجم الكبير |
| 4 | **مسافة طريق وليست خطاً مستقيماً** | توجيه فعلي captain → pickup عبر مزود خرائط | دقة 20–40% أعلى في تقدير المسافة |
| 5 | **ترتيب حسب ETA لا بالكيلومتر** | وقت الوصول المتوقع (مع الزحام حيث يتوفر) هو مقياس القرار | "من سيصل أولاً" بدل "من هو الأقرب هندسياً" |
| 6 | **إسقاط الموضع (Dead reckoning)** | الاحتفاظ بآخر 2+ نقاط والاستقراء بالاتجاه والسرعة لتقدير الموقع الحالي | الموقع المقترح "لحظي" وليس "آخر نبضة" |
| 7 | **نقاط موزونة (Score) فوق المسافة** | مزيج مرن: ETA + تقييم + سجل قبول/رفض + سعة المركبة + تفضيل المنطقة، مع إظهار `score` وأسبابه | الترتيب يعبّر عن "الأنسب" وليس "الأقرب" فقط |
| 8 | **إدراج "قارب على الانتهاء"** | الكابتن المشغول الذي يتبقّى له وقت قصير يُعرض مع وسْم `busy_until` (إذا كان وقته للوصول < وقت المرشحين الآخرين) | أفضل كابتن فعلي قد يكون "المنتهي قريباً" |
| 9 | **تقسيم جغرافي + كاش** | شبكة مناطق + فهرس `zone → captain → last fix` في Redis تتحدّث من نقطة الموقع الحية | اقتراحات من الكاش بدل مسح الـ DB لكل طلب |

### مبدأ حاسم عند التطوير
- `assign()` يعيد نفس التحقّق من الأهلية — فلا يوجد «باب ثانٍ» يعيّن كابتن لم يكن مؤهلاً في الاقتراح.
- `DistanceService` النهائية تُفصل خلف **واجهة** بحيث يُستبدل مزود التوجيه دون كسر بقية الكود، مع بقاء الاختبارات تعمل على الحل السريع الاحتياطي (كما تفعل الآن على SQLite).

---

## 3. جدول مقارنة مختصر

| المعيار | الخوارزمية الحالية | الأفضل ممارسةً |
|---|---|---|
| مقياس المسافة | خط مستقيم (Haversine) | مسافة الطريق عبر مزود توجيه (+ بديل سريع) |
| مصدر موقع الكابتن | آخر موقع مُبلَّغ (بأي عمر) | آخر موقع **حديث** (نافذة < 10 دقائق) |
| مقياس الترتيب | `distance_km` | `eta_minutes` (وقت الوصول) |
| معلومات إضافية للديسباتشر | المسافة فقط | المسافة + ETA + عمر الموقع + (لاحقاً score وأسبابه) |
| معالجة المشغول | استثناء نهائي | استثناء + خيار «قارب على الانتهاء» مع وسْم |
| الأداء عند الكثافة | فحص O(N) بدون فهارس | فهرسة `ST_Distance_Sphere` + تقسيم/كاش |
| شاملية «الأفضل» | الأقرب هندسياً فقط | نقاط موزونة (ETA+تقييم+سعة+منطقة) |
| اعتماديات خارجية | لا شيء | مزود توجيه (اختياري، مع بديل يعمل بدون إنترنت) |
| الجاهزية للاختبار الآلي | ممتازة (PHP بحت، DB-agnostic) | تحافظ على نفس الميزة عبر واجهة + بديل |

---

## 4. خطة التنفيذ المقترحة

- **المرحلة 1 (مباشرة، بدون مفاتيح):** فلتر حداثة الموقع + إظهار `captured_at`/`eta_minutes` + فهرسة `ST_Distance_Sphere` كتصفية مسبقة.
- **المرحلة 2:** عقد `DistanceService` خلف واجهة، وإضافة مزود توجيه (يبدأ بـ OSRM العام — بدون مفتاح — كإثبات مفهوم، ثم Google/Here بمفتاح العميل).
- **المرحلة 3:** ترتيب وزني (score) + إعادة تعريف «المشغول» حسب الزمن المتبقي + تقسيم جغرافي وكاش إذا تطلّب الحجم.

> **ملاحظة:** أي تغيير في قواعد الأهلية يجب أن ينعكس تلقائياً على `assign()` وليس على قائمة الاقتراح فقط، للحفاظ على قاعدة «باب واحد للتعيين».