# تحليل استراتيجية الـ Fulfillment وخطة التنفيذ المقترحة

> الوثيقة تحلّل مقترح `kapitano_fulfillment_business_logic_ar.md` **مقابل نظامنا الفعلي** (Logistic + متجر kapitano_web + لوحة kapitano-admin)، وتحدد ما هو موجود وما هو ناقص، وتقترح خطة تنفيذ مرحلية لا تكسر ما يعمل اليوم، مع تحسينات على المقترح نفسه.
> هذه وثيقة تخطيط فقط. لا يوجد أي تنفيذ برمجي مرتبط بها.
> المراجع في الكود مأخوذة من قراءة المستودع بتاريخ 2026-10-13. أسماء الملفات والأعمدة صحيحة، أما أرقام الأسطر فتقريبية.

---

## 1. الخلاصة التنفيذية

1. **المقترح صحيح في اتجاهه**: فصل «الطلب التجاري» عن «تنفيذه الفعلي»، واعتبار الكابتن منفّذ **مهام** لا مالك طلب، واشتقاق حالة الطلب من حالة بنوده. هذه هي النقطة التي يصطدم بها نظامنا اليوم فعلاً (متعدد الموردين + تغليف + تسليم بين كباتن).
2. **نظامنا ليس نقطة الصفر**: لدينا بالفعل نصف المفهوم متفرقاً:
   - `order_pickups` = نموذج أولي لـ Supplier Task (محطة لكل مورد، تأكيد استلام لكل محطة، عدد ومبلغ لكل محطة).
   - الطلب المغلَّف (`leg` + `parent_order_id`) = استراتيجية WRAPPING لكن بأسلوب «صفّان في جدول الطلبات».
   - تسليم الكابتن (`OrderHandoverService`) = نموذج أولي لـ Handoff.
3. **العقبة الحقيقية الوحيدة** ليست كثرة الكيانات، بل أن **كل شيء في النظام مربوط بعمود واحد: `orders.driver_id`** (الصلاحيات، السعة، الطرق، العروض، الأرباح، قائمة الكابتن). أي خطة لا تعالج هذا العمود بوعي ستفشل أو ستنتج نظامين متوازيين.
4. **التوصية**: لا تنفّذ المقترح كاملاً دفعة واحدة. نفّذه **بالتدرّج (Strangler)** على 5 مراحل، تبدأ بما يعطي قيمة فورية دون تغيير ملكية الكابتن (كميات البنود والاستثناءات)، ثم **مهمة جمع لكل كابتن** (وهي القفزة الكبرى)، ثم اشتقاق الحالة، ثم توحيد التغليف على المهام. ويُؤجَّل Hub و Cross-Dock و Shipment Unit إلى أن يطلبها العمل فعلاً.
5. **عقد المتجر (Store API) لا يتغير** في أي مرحلة: المتجر يرى طلباً واحداً بحالات الطلب الحالية. هذا شرط قبول لا تفاوض فيه.

---

## 2. ما الذي يقترحه المستند (ملخص مختصر)

| المفهوم في المستند | المعنى |
|---|---|
| Order مقابل Fulfillment | الطلب هو ما اشتراه العميل. التنفيذ هو كيف نوصله. لا نخلطهما. |
| Fulfillment Strategy | DIRECT (من المورد للعميل) أو WRAPPING (مورد ← مكان تغليف ← عميل)، ولاحقاً HUB / CROSS_DOCK. |
| Collection Plan | خطة جمع الطلب، وتتفرع إلى Tasks، مهمة لكل مورد، ويمكن إسنادها لكباتن مختلفين. |
| Delivery Task | مهمة التوصيل للعميل، تبدأ بحسب الاستراتيجية. |
| Shipment Unit / Handoff | وحدة مادية لها سلسلة عهدة (من استلمها ومن سلّمها). |
| Order State Engine | حالة الطلب **تُحسب** من حالة البنود والمهام، لا من حركة كابتن بعينه. |
| استلام/تسليم جزئي + استثناءات + موافقة العميل | RETRY_PICKUP، RESCHEDULE، CANCEL_ITEM، WAIT_FOR_SUPPLIER (والبدائل SUBSTITUTE أُلغيت حاليًا). |
| Event-driven | ITEM_COLLECTED، COLLECTION_COMPLETED، WRAPPING_REQUESTED، DELIVERY_REQUESTED... |
| المتجر يرى الطلب التجاري فقط | والكابتن ينفّذ المهام فقط. |

---

## 3. مطابقة المقترح مع نظامنا الحالي

### 3.1 جدول المطابقة

| مفهوم المقترح | ما لدينا اليوم | الحكم |
|---|---|---|
| Order | جدول `orders` (حالة، مال، عميل، عنوان، روابط المتجر) | **موجود**، لكنه يحمل أيضاً حالة التنفيذ ورقم الكابتن. |
| Strategy = DIRECT | الطلب العادي (`leg = null`) | **موجود** ضمنياً. |
| Strategy = WRAPPING | طلبان مترابطان: leg=collection (`{رقم}-C`) و leg=delivery، يربطهما `parent_order_id`. التوصيل لا يُسند قبل تسليم الجمع (`assertGoodsHaveArrived`) | **موجود** بتصميم «صفّين». يعمل لكنه لا يعمّم. |
| Collection Plan | غير موجود ككيان. الطلب متعدد الموردين له عدة صفوف في `order_pickups` | **ناقص** (لا خطة، فقط قائمة محطات). |
| Supplier Task | `order_pickups`: `sequence`، `picked_up_at`، `items_collected`، `items_mismatch`، `amount_paid`، `collected_by_driver_id` | **نصف موجود**: لا `driver_id`، لا حالة، لا قبول/رفض، لا مهلة. |
| إسناد مهمة لكابتن مختلف | غير ممكن عبر أي مسار برمجي. `collected_by_driver_id` يساوي دائماً كابتن الطلب الوحيد | **ناقص (القفزة الكبرى)**. |
| Item-level quantities | `order_items` بلا أي كمية مجمّعة/مسلّمة/ناقصة. فقط عدّاد `items_collected` على الطلب والمحطة | **ناقص**. |
| Partial collection | موجود كعدّاد ونقص يُعلَّم `items_mismatch` ولا يوقف شيئاً، يذهب لفريق العمليات | **جزئي**: إشعار لا سير عمل. |
| الاستثناءات (Retry/Reschedule/Cancel item/Wait) | لا شيء. إما يكمل الكابتن أو يفشل الطلب كله | **ناقص**. |
| موافقة العميل على النقص | لا شيء | **ناقص**، ويحتاج قناة للمتجر (انظر 5.4). |
| Shipment Unit / Handoff | `OrderHandoverService`: كابتن يسلّم الطلب لكابتن آخر، فقط في حالتي picked_up و on_the_way | **جزئي جداً**: لا عهدة لكل وحدة، ولا تسليم في مرحلة الجمع. |
| Order State Engine (حالة مشتقة) | حالة مخزّنة، خطية، تتحرك عبر باب واحد `OrderService::transition()` | **ناقص**، لكن وجود الباب الواحد **ميزة كبيرة** لنا (انظر 6.1). |
| Event-driven | أحداث موجودة: `OrderCreated`، `OrderStatusChanged`، `OrderAssigned`، `OrderChanged` + Listeners | **موجود بمستوى الطلب**، لا بمستوى البند/المهمة. |
| المتجر يرى الطلب التجاري فقط | المتجر يسمع طلب التوصيل فقط، وطلب الجمع بلا `store_client_id` فيبقى صامتاً | **موجود فعلاً** (ميزة للتغليف). |
| الكابتن ينفّذ مهاماً لا طلبات | الكابتن يرى طلباً واحداً بقائمة محطات، ويعمل بـ endpoint لكل محطة | **جزئي**: الواجهة قريبة، الصلاحيات والبيانات لا. |

### 3.2 ما يؤكّده الكود حول «من يملك الطلب»

كل ما يلي مبني على `orders.driver_id` الواحد، وكل بند هو **نقطة تغيير** لأي خطة:

1. **الصلاحيات**: `OrderService::guardAgainstForeignDriver` و `Order::isAssignedTo` و `OrderHandoverService::guardIsCarriedBy`. كل فعل للكابتن (قبول، رفض، جمع محطة، في الطريق، تسليم، فشل، عرض) يمر عبرها.
2. **السعة**: `drivers.active_orders` تُحجز بـ UPDATE شرطي واحد في `DriverRepository::tryReserveCapacity` وتُفرَّغ في `releaseCapacityIfFinished` عندما يصير **الطلب** نهائياً. يعني: حصة واحدة لكل طلب.
3. **العرض والمهلة**: `accepted_at`، `offer_expires_at`، وظيفة `ExpireOrderOffer` المفهرسة بـ (طلب، كابتن، موعد)، و `OrderDecline`، و `suggestion_id` / `chosen_rank`.
4. **الحالة الخطية**: `picked_up_at`، `on_the_way_at`، `delivered_at`، `failed_at` أعمدة مفردة. و `markPickupCollected` ينقل الطلب كله إلى `picked_up` عند جمع آخر محطة **بغض النظر عمّن جمع أي محطة**.
5. **الطرق**: `route_plans` فريد على (`driver_id`, `version`)، ويُبنى من طلبات الكابتن ومحطاتها المتبقية (`inProgressWithPendingPickupsFor`).
6. **المال**:
   - `CaptainDeliveryEarning` فريد على `order_id`، أي أرباح واحدة لكل طلب، وتُدفع للكابتن الذي سلّم، ولا تُدفع على leg الجمع.
   - `CaptainLedgerEntry` يملك أصلاً `driver_id` و `order_id` و `order_pickup_id` ← **دفع المورد مسجَّل لكل محطة ولمن دفع. هذا جاهز للمهام.**
   - `StoreLedger` (رسوم المتجر) يُستدعى مرة لكل طلب عند التسليم/الفشل/الإلغاء بعد الاستلام.
   - `amount_paid` و `amount_collected` و `items_collected` على الطلب مجاميع.
7. **التغليف**: `assertGoodsHaveArrived`، `undeliveredCollectionLegs`، `liveSiblingLegs`، `paginateAwaitingCollection`، تتالي الإلغاء في `OrderService::cancel`، `RaiseStrandedWrappingOrder`، `NotifyAdminsDeliveryFailed`، وكل إحصاءات `whereNull('parent_order_id')`.
8. **الواجهات**: `CaptainOrderResource` (حالة واحدة + `next_statuses` + `steps` + `handover`)، إسناد لوحة التحكم على مستوى الطلب، `OrderSuggestion` على مستوى الطلب. (ملاحظة: محرّك الاقتراح `CandidateFinder` يستخدم **كل** محطات الطلب لا الأولى فقط، ويثبّت لكل كابتن أقرب محطة `anchor_pickup_id`. لكن القرار الناتج يبقى «كابتن واحد للطلب كله»، و`EstimatedRemainingEtaProvider` يرجع لإحداثيات أول محطة عند غياب GPS.)

> النتيجة: «مهام لكباتن مختلفين» ليست ميزة تُضاف، بل **تغيير في وحدة الإسناد**. لذلك قرار التصميم الأول هو: ما هي وحدة الإسناد؟ (انظر القسم 5.1).

---

## 4. تقييم المقترح: ما هو ممتاز وما يحتاج تعديلاً

### 4.1 ما نتبناه كما هو
- فصل Order عن Fulfillment.
- الكابتن ينفّذ Tasks.
- الأحداث كلغة بين الأجزاء (ITEM_COLLECTED...).
- المتجر لا يرى تفاصيل التنفيذ.
- حالة الطلب نتيجة (derived) لا أمر.
- القواعد الاثنتا عشرة تُعامَل كاختبارات قبول، تُترجم كل قاعدة إلى اختبار آلي.

### 4.2 ما نعدّله (تحسينات على المقترح)

| # | المقترح | المشكلة عندنا | التعديل |
|---|---|---|---|
| 1 | كيانات جديدة كاملة (Fulfillment, CollectionPlan, Task, ShipmentUnit, Handoff) | بناؤها كلها دفعة واحدة = إعادة كتابة، ومخاطرة على نظام يعمل ومُدمَج | **Task = تعميم `order_pickups`** لا جدول موازٍ. Fulfillment و CollectionPlan يبدآن **كمفهوم/عرض** ثم يصيران كيانات إذا احتجنا. |
| 2 | Order State Engine يشتق الحالة بالكامل | الحالة المخزنة مقروءة في عشرات الأماكن (webhooks، لوحة، إحصاءات، شاشات الكابتن) | **Projection-first**: المحرّك يحسب الحالة ثم **يكتبها** في `orders.order_status` عبر نفس `transition()`. نبقى بمصدر حقيقة واحد مكتوب، لا حقلين يختلفان. |
| 3 | WRAPPING كمرحلة داخل نفس الـ Fulfillment | عندنا تغليف يعمل بصفّين ومتّصل بالمتجر | لا نهدمه. المرحلة 4 فقط توحّده على المهام، بعد أن تثبت المهام نفسها. |
| 4 | Shipment Unit وسلسلة عهدة كاملة | لا حاجة تشغيلية مثبتة، وتكلفتها عالية (مسح، QR، حالات) | **نؤجلها**. نكتفي بـ Handoff كحدث على المهمة (من ← إلى، متى، أين) يغطي 90% من المطلوب. |
| 5 | HUB / CROSS_DOCK | لا يوجد مستودع عندنا | **نبني نقطة التوسّع فقط**: حقل `strategy` على التنفيذ وقيم DIRECT/WRAPPING. باقي القيم لاحقاً. |
| 6 | الاستثناءات الخمسة دفعة واحدة | بعضها يحتاج قراراً تجارياً (من يدفع البديل؟) | نبدأ بـ CANCEL_ITEM و RETRY_PICKUP (لا قرار مالي جديد)، ثم WAIT_FOR_SUPPLIER بعد قرار الأعمال (والبدائل SUBSTITUTE أُلغيت حاليًا). |
| 7 | موافقة العميل | العميل على المتجر لا عندنا، لا واجهة له في Logistic | الموافقة **تطلب عبر المتجر** (طلب موافقة + رد، بنفس أسلوب Outbox/Webhook). نصمّمها كطلب/جواب لا كشاشة. |
| 8 | حدث لكل خطوة | بلا ضمانات قد يتكرر أو يصل بترتيب خاطئ | كل أحداث المهمة **idempotent** (مفتاح فريد لكل (مهمة، نوع، ترقيم))، وأي مستهلك يتحمّل التكرار. |

---

## 5. قرارات التصميم الأساسية

### 5.1 وحدة الإسناد (القرار الأهم)

**الخيار أ، المهمة هي وحدة الإسناد**: كل مهمة (جمع من مورد، تسليم للعميل) لها `driver_id` وحالتها وعرضها ومهلتها. الطلب لا يملك كابتناً.
**الخيار ب، الطلب يبقى وحدة الإسناد** ويُضاف حقل «كابتن إضافي لمحطة».

**التوصية: أ**، مع **بقاء `orders.driver_id` كحقل مشتق للتوافق** (= كابتن مهمة التسليم، أو الكابتن الوحيد في الطلبات البسيطة) إلى أن تتحوّل كل القراءات. الخيار ب يُنتج حالات شاذة (من يتحمّل السعة؟ من يستلم أرباح؟) ويؤجّل المشكلة فقط.

### 5.2 السعة (Capacity)
- اليوم: حصة لكل طلب.
- المقترح: **حصة لكل مهمة نشطة**، لأن كابتن الجمع يشغل وقتاً ومسافة.
- قرار أعمال مطلوب: هل مهمة جمع صغيرة تعادل حصة كاملة؟ نقترح **وزناً قابلاً للضبط** (`capacity_weight`) افتراضه 1، لا تعديل في المنطق لاحقاً إن تغيّر القرار.
- يبقى `tryReserveCapacity` هو الباب الوحيد (UPDATE شرطي ذري)، لكن يستدعى بالمهمة لا بالطلب. و `CapacityReconciler` يعدّ المهام لا الطلبات.

### 5.3 المال
- **رسوم المتجر (`StoreLedger`)**: تبقى على مستوى **الطلب التجاري**، مرة واحدة، لا تتأثر بعدد المهام. هذا يطابق «المتجر يرى الطلب فقط».
- **أرباح الكابتن**: يجب أن تصير **لكل مهمة**. اليوم `captain_delivery_earnings` فريد على `order_id`. نحتاج مفتاحاً (`task_id`) وجدول/عمود جديد. ويلزم قرار أعمال: أجر الجمع (متى يُدفع؟ بعد تسليم المورد للتغليف؟) وأجر التوصيل.
- **دفع المورد**: جاهز لكل محطة (`CaptainLedgerEntry.order_pickup_id`). ننقله إلى `task_id` بشكل مطابق.
- ملاحظة مهمة: عندنا قاعدة قائمة «leg الجمع لا يدفع أرباحاً». يجب إعادة النظر فيها عند توحيد التغليف (المرحلة 4)، وتنفيذها كقرار صريح.

### 5.4 ما يراه المتجر
- لا تغيير في العقد. حالات الطلب الحالية (`pending ... delivered/failed/cancelled`) تبقى.
- الحالات الجزئية (جمع 2 من 3) **لا تظهر كحالات جديدة**، بل كحقول اختيارية إضافية في الحمولة (`fulfillment.collected_items / total_items`) تُضاف كحقول **اختيارية** فقط. عندئذ لا يُكسَر عقد مثبّت (`logistic:contract:pin`) ولا `StatusMap` في المتجر. أي حقل جديد يمر عبر تحديث العقد والتثبيت **في الطرفين**.
- موافقة العميل: webhook `approval_requested` ورد عبر endpoint جديد، بإصدار جديد من العقد. اختياري ومتأخر (المرحلة 3).

### 5.5 الإلغاء والفشل
- إلغاء الطلب يتتالى اليوم على leg مرتبط. في النموذج الجديد: إلغاء الطلب = إلغاء كل المهام الحية (نفس الدالة `OrderService::cancel`).
- قاعدة «بعد أن جمع الكابتن لا يلغي المتجر» (قفل الإلغاء أثناء النقل) تبقى. تُعرَّف الآن بدقة: **أي مهمة جمع مكتملة تقفل الإلغاء من جهة المتجر**.
- رسوم الإلغاء بعد الاستلام (`recordCancelledAfterPickup`) تبقى مرتبطة بـ `picked_up_at` الأولى (أول مهمة جمع).

---

## 6. المخاطر

### 6.1 مخاطر وإيجابيات ذات صلة بنظامنا
| الخطر | الأثر | التخفيف |
|---|---|---|
| ازدواج مصدر الحقيقة (حالة الطلب المخزنة مقابل المشتقة) | تضارب في اللوحة والمتجر | الحالة تُكتب فقط من مكان واحد (`transition()` بعد المحرّك). اختبار «الإسقاط يساوي المخزّن» في كل اختبار تكامل. |
| كسر عقد المتجر | طلبات المتجر تتوقف | لا حقول إلزامية جديدة. العقد المثبّت يفشل الاختبار قبل النشر. |
| تسرّب السعة (حصة تُحجز ولا تُفرَّغ) | كابتن «ممتلئ» وهو فارغ | تفريغ في كل مسار نهائي للمهمة + `CapacityReconciler` يعدّ المهام + اختبار لكل مسار. |
| سباقات (كابتنان على المهمة نفسها، أو إلغاء أثناء الجمع) | بيانات مكسورة | نفس نمط الحجز الشرطي الذري لـ `tryReserveCapacity` على المهمة، وقفل `AssignmentLock` على الكابتن. |
| أحداث مكررة أو بترتيب خاطئ | حالة خاطئة | مفتاح idempotency + ترقيم (مثل `revision` الموجود للـ webhooks). |
| قرارات مالية غير محسومة | أرباح خاطئة | لا إطلاق للمرحلتين 2 و 4 قبل حسم القرارات في القسم 9. |
| إحصاءات مضلَّلة | ترجع مضاعفة | كل استعلام تجاري يبقى `whereNull('parent_order_id')`. أي صفوف مهام لا تدخل عدّ الطلبات. |
| تطبيق الكابتن | يكسر إن تغيّرت العقود | endpoints الحالية تبقى. نضيف `tasks` للاستجابة، والتطبيق القديم يتجاهله. |

### 6.2 ما يخفف الخطر لدينا أصلاً
- **باب واحد** للانتقال (`OrderService::transition`) وفيه الأحداث والتاريخ وتفريغ السعة. المحرّك يُوضع خلفه ولا تُنشأ أبواب جديدة.
- `collected_by_driver_id` و`CaptainLedgerEntry.order_pickup_id` موجودان.
- `order_items.order_pickup_id` يربط البند بمحطته.
- نظام الأحداث والـ outbox والـ webhooks بترقيم (`revision`) جاهز.
- تغطية اختبارات جيدة على كل ما سبق (`PerStopCollectionTest`، `WrappedOrderTest`، `OrderAssignmentApiTest`، سكربت E2E الحي بـ 198 فحصاً).

---

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

> المبدأ: كل مرحلة **قابلة للنشر وحدها**، خلف **علَم تشغيل** (`feature flag`)، ولا تكسر عقد المتجر أو تطبيق الكابتن. كل مرحلة تنتهي بحزمة اختبارات ثابتة قبل أن تبدأ التي بعدها.

### المرحلة 0: الحسم والقياس (قبل أي كود)
- حسم قرارات القسم 9 (على الأقل القرارات 1 و 2 و 3).
- ترجمة القواعد الاثنتي عشرة إلى سيناريوهات قبول مكتوبة (given/when/then).
- قياس الواقع: كم طلباً متعدد الموردين؟ كم نسبة النقص؟ كم تغليفاً؟ كم مرة يتأخر مورد؟ لتحديد ما يستحق البناء أولاً. (استعلامات فقط على البيانات الحالية.)

### المرحلة 1: كميات البنود والاستثناءات الأساسية (بدون تغيير ملكية الكابتن)
**الهدف:** قيمة فورية على الواقع الحالي، ومعالجة «النقص يُعلَّم ولا يُعالَج».
- أعمدة على `order_items`: `quantity_collected`، `quantity_delivered`، `quantity_missing`، `item_status`.
- `order_pickups` تتلقى `status` (pending / collected / partial / failed / cancelled) و`failure_reason`.
- تجميع البنود: endpoint جمع المحطة يقبل كميات لكل بند (اختيارياً؛ ما سبق يعمل كما هو).
- استثناءان: **CANCEL_ITEM** (ينقص من الإجمالي ويعيد حساب المبلغ والرسوم عبر `Order::calculatedTotal()` الموجود) و**RETRY_PICKUP** (محطة فاشلة تعاد جدولتها).
- لوحة التحكم: عرض البنود المجمّعة/الناقصة لكل محطة، وإشعار للعمليات بدل الاكتفاء بالعَلَم.
- المتجر: حقول اختيارية للقراءة فقط عن النقص (لا تغيير في الحالات).
- **الاختبارات:** توسعة `PerStopCollectionTest`، حالات الإجمالي والرسوم (نسبة العميل `fee_customer_share`)، تعديل بعد التسليم (EditGuard)، وتطابق مجاميع الطلب مع مجاميع البنود.
- **الرجوع:** أعمدة اختيارية فقط، العَلَم يعيد السلوك القديم.

### المرحلة 2: مهمة جمع لكل كابتن (القفزة الكبرى)
**الهدف:** إسناد محطات الطلب الواحد لكباتن مختلفين.
- تعميم `order_pickups` إلى **Task**: أعمدة `driver_id`، `assigned_at`، `accepted_at`، `offer_expires_at`، `declined_count`، و`type` (collect / wrap / deliver لاحقاً).
- إسناد المهمة عبر `AssignmentService` نفسه: نفس القفل والحجز الذري، لكن **على المهمة**. أول تنفيذ يبقى **يدوياً من لوحة التحكم** (كل محطة بقائمة كباتن)، ثم محرّك الاقتراح يقترح **لكل مهمة** (يعيد استخدام `CandidateFinder` و`Ranker`، فهما يعملان أصلاً على نقاط الاستلام، فيُضيَّق النطاق إلى محطة المهمة وحدها بدل «كل محطات الطلب»).
- الصلاحيات: `guardAgainstForeignDriver` تتحقق من كابتن **المهمة** لا الطلب. ومسار `orders.driver_id` يبقى مشتقاً (= كابتن التوصيل، أو الوحيد).
- الطرق: `route_plans` تُبنى من **مهام الكابتن** لا من طلباته.
- أرباح لكل مهمة (بعد حسم القرار 3). تفريغ السعة عند اكتمال المهمة لا الطلب.
- شاشة الكابتن: تضيف `tasks` (مهامه في هذا الطلب)، والطلب كما هو للتطبيق القديم.
- لوحة التحكم: عمود «الكابتن» على كل مورد، وحالة كل محطة.
- **الاختبارات:** كابتنان على طلب واحد (جمع مختلف)، سباق على المهمة نفسها، رفض/انتهاء مهلة مهمة، إلغاء الطلب أثناء جمع مهمة، تسليم مهمة لكابتن آخر، تسرّب السعة، ومطابقة الأرباح مع دفتر الكابتن.
- **الرجوع:** العلم يعيد الإسناد بمستوى الطلب. الطلبات المجمَّعة بمهام تكمل طبيعياً (البيانات متوافقة للخلف).

### المرحلة 3: محرّك الحالة (Order State Engine) + أحداث المهام + موافقة العميل
- محرّك يحسب حالة الطلب من حالات المهام والبنود ويكتبها عبر `transition()` (الإسقاط).
- جدول الحالات المقترح: لا مهمة بدأت = `pending`/`assigned`؛ أول جمع مقبول = `accepted`؛ كل مهام الجمع اكتملت = `picked_up`؛ ثم `on_the_way`؛ ثم `delivered`/`failed`. ومع النقص **قرار الأعمال 4** (هل نسلّم جزئياً؟).
- أحداث المهام (`TaskCollected`، `CollectionCompleted`...) idempotent. المستمعون الحاليون يبقون على `OrderStatusChanged`.
- استثناءان إضافيان: **SUBSTITUTE** و**WAIT_FOR_SUPPLIER**، مع **طلب موافقة العميل** عبر المتجر (webhook + endpoint، بإصدار عقد جديد وبعلَم اختياري). (**أُلغيت حاليًا بقرار المالك — لا تُنفَّذ الآن**)
- **الاختبارات:** جدول حقيقة كامل (مصفوفة مهام ← حالة طلب متوقعة)، واختبار «المشتق = المخزّن»، واختبار عقد المتجر (لا حالات جديدة).

### المرحلة 4: توحيد التغليف على المهام
- استراتيجية WRAPPING تصير: مهام جمع ← مهمة تغليف (`type = wrap`، في مكان التغليف) ← مهمة توصيل، تحت **طلب واحد**.
- **تنفيذ بالتوازي أولاً:** الطلبات الجديدة بالنموذج الجديد، القديمة تبقى بصفّين حتى تنتهي. لا ترحيل بيانات حيّة. ثم يزال كود الـ legs عند خلوّ الجداول من طلبات حيّة.
- يُستبدل `assertGoodsHaveArrived` بشرط «مهمة التغليف مكتملة»، و`awaiting-collection` يصير عرضاً على المهام، وتتالي الإلغاء يصير داخل الطلب.
- **الاختبارات:** `WrappedOrderTest` تُعاد صياغتها على النموذج الجديد، وسكربت E2E الحي يُوسَّع بسيناريو تغليف بالمهام.

### المرحلة 5: التوسع (عند الطلب فقط)
- HUB / CROSS_DOCK كقيم جديدة لـ `strategy` ومهام جديدة.
- Shipment Unit وسلسلة العهدة إن ثبتت الحاجة (مسح/باركود).
- مؤشرات SLA لكل مهمة، وتنبيهات تأخر المورد.

---

## 8. ما يتأثر في كل مكوّن (قائمة تدقيق للتنفيذ)

| المكوّن | المرحلة 1 | المرحلة 2 | المرحلة 3 | المرحلة 4 |
|---|---|---|---|---|
| Logistic: الطلبات والمهام | أعمدة بنود/محطات | تعميم المحطة إلى مهمة | محرّك + أحداث | أنواع مهام + إزالة legs |
| الإسناد والسعة | — | مهمة بدل طلب | — | — |
| الطرق (`route_plans`) | — | مهام الكابتن | — | مهمة التغليف |
| المال (متجر/كابتن) | إعادة حساب عند حذف بند | أرباح لكل مهمة | — | قاعدة leg الجمع تُراجَع |
| Webhooks / العقد | حقول قراءة اختيارية | — | موافقة العميل (اختياري) | — |
| متجر kapitano_web | عرض النقص اختياري | — | `approval` + رد | `needs_wrapping` كما هو |
| لوحة kapitano-admin | بنود ونقص | كابتن لكل محطة | حالات المهام | شاشة التغليف |
| تطبيق الكابتن | كميات اختيارية | `tasks` | — | مهمة التغليف |
| اختبارات | توسعة | سباقات وسعة | جدول حالات | إعادة كتابة التغليف |

---

## 9. قرارات تحتاج قراراً من الأعمال (قبل البناء)

1. **أجر الكابتن لكل مهمة**: كم أجر الجمع؟ هل هو ثابت أم بالمسافة؟ ومتى يُستحق (عند التسليم للتغليف أم عند اكتمال الطلب)؟ *(يحجب المرحلتين 2 و 4).*
2. **السعة**: هل مهمة جمع = حصة كاملة أم جزء؟ *(يحجب المرحلة 2).*
3. **من يدفع تكلفة الجمع الإضافي** عندما تتعدد الكباتن لطلب واحد: المتجر أم العميل أم نحن؟ وهل يتأثر `fee` المخزَّن (`fee_customer_share`)؟
4. **التسليم الجزئي**: إن غاب بند، هل يُسلَّم الباقي أم ننتظر؟ وعلى من حدود الانتظار؟ ومتى يعتبر الطلب «فاشلاً جزئياً»؟
5. **البدائل (SUBSTITUTE)**: من يقترح، ومن يوافق، وماذا لو رفض العميل بعد أن اشتُري البديل؟ ومن يتحمّل ثمنه؟ (**أُلغيت حاليًا بقرار المالك — لا تُنفَّذ الآن**)
6. **مهلة الانتظار والموردين المتأخرين**: بعد كم دقيقة تُعاد المهمة لكابتن آخر أو تُلغى؟
7. **ظهور التفاصيل للمتجر**: هل يُسمح بإظهار «تم جمع 2 من 3» أم يكتفي بحالة الطلب؟
8. **التغليف**: هل مكان التغليف يمكنه رفض/تعديل البنود؟ ومن يتحمّل الفرق؟
9. **طلبات متعددة الكباتن للكابتن الواحد**: هل يجوز لكابتن واحد أن يأخذ مهمتي جمع ثم يوصل الطلب؟ (يؤثر على الأرباح والطرق.)

---

## 10. تحسينات إضافية مقترحة على المقترح

1. **Task generalizes Pickup**: لا جدول جديد للمهام بل توسعة `order_pickups` (يحفظ `order_items.order_pickup_id` و`CaptainLedgerEntry.order_pickup_id` و`collected_by_driver_id` وكل الاختبارات).
2. **Feature flags لكل مرحلة** وقدرة على الرجوع دون ترحيل بيانات.
3. **Idempotency + ترقيم لكل حدث مهمة**، بنفس فلسفة `revision` في الـ webhooks.
4. **مقاييس** من اليوم الأول: نسبة النقص، زمن الجمع لكل مورد، عدد إعادات المحاولة، زمن بين آخر جمع وبدء التوصيل، نسبة الاستثناءات. تُغذّي قرارات المرحلة 5 بالأرقام.
5. **سجل تدقيق** لكل استثناء (من فعل، متى، لماذا) على جدول التاريخ الموجود (`OrderStatusHistory`) أو جدول أحداث مهمة.
6. **اقتراح الكابتن لكل مهمة**: محرّك الاقتراح اليوم يبحث حول كل المحطات ويقرر كابتناً واحداً للطلب. في المرحلة 2 يُشغَّل لكل مهمة بنقطتها، وفحص التوافق الدفعي (`BatchCompatibilityService`) يُطبَّق على مهام الكابتن.
7. **قواعد القبول كاختبارات**: كل قاعدة من القواعد الاثنتي عشرة = اختبار باسم القاعدة، حتى يبقى المستند مرجعاً حياً.
8. **حماية تكلفة المتجر**: لا مهمة جديدة تظهر في `store-portal` ولا تدخل في أرصدته (يطابق قاعدة leg الجمع اليوم).
9. **سيناريوهات E2E حيّة** لكل مرحلة تُضاف إلى `tests/Live/logistic_real_flow_e2e.php` (طلب بمهمتين لكابتنين، نقص، تغليف).

---

## 11. ترتيب الأولويات المقترح

1. **الآن**: المرحلة 0 (قرارات 1 و 2 و 3 + قياس الواقع).
2. **أول تنفيذ**: المرحلة 1. تعالج وجعاً قائماً (نقص يُعلَّم ولا يُعالَج) دون أي مخاطرة معمارية.
3. **بعد حسم المال**: المرحلة 2.
4. المراحل 3 و 4 بحسب أثر القياس، والمرحلة 5 عند الطلب.

---

## 12. حدود هذا التحليل

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

---

## 13. ملاحق: ما كشفه فحص الإرسال والمال وعقد المتجر

### 13.1 أمور تغيّر الخطة
1. **الدفتر جاهز جزئياً للمهام**: `captain_ledger_entries` فيه `driver_id` و`order_pickup_id` ويُسجَّل دفع المورد لكل محطة ولمن دفع. هذا يؤكد قرار تعميم `order_pickups` (القسم 4.2) لأن الدفتر لن يحتاج تغييراً هيكلياً.
2. **التسليم بين كابتنين لا يحرّك المال**: «الدفتر يتبع من أنفق». نقد التسليم والأرباح يُكتبان عند التسليم للكابتن الذي يملك الطلب حينها. في نموذج المهام يجب تحديد صاحب كل قيد بالمهمة لا بحامل الطلب.
3. **الأرباح**: `captain_delivery_earnings` فريد على `order_id`، والموظفون يأخذون 0 (`salaried`) والمنطقة غير المحلولة تُسجَّل `unresolved`، و`markFailed` لا يكتب أرباحاً ولا قيد نقد. القرار 1 في القسم 9 يجب أن يغطي: هل يستحق الكابتن شيئاً عن جمع فاشل؟
4. **النقد على ساق الجمع**: `markDelivered` يسجّل `CashCollected` لأي ساق مبلغها أكبر من صفر، بما فيها ساق الجمع. غير مستعمل اليوم لأن مبلغها null، لكنه ثغرة محتملة عند توحيد التغليف.
5. **رسوم المتجر ثابتة على الطلب**: `StoreLedger` يتجاهل ساق الجمع ومن لا `store_client_id` له، ويكتب مرة لكل طلب. هذا يطابق قرارنا ببقائها على مستوى الطلب التجاري.
6. **الطرق والموقع الفعلي**: `route_plans` للكابتن كله (لا للطلب) وتُبنى من محطات متبقية وتسليمات. `EffectiveLocationResolver` يعتمد «الطلب الحالي» الواحد. سيحتاج تعميماً على «المهمة الحالية».
7. **التسليم بين الكباتن لا يرسل شيئاً للمتجر**، كذلك الرجوع إلى Pending بعد رفض/انتهاء عرض. أحداث المهام الجديدة ستتبع القاعدة نفسها: لا webhook للمتجر إلا عند تغيّر حالة الطلب التجاري.

### 13.2 عقد المتجر: ما يرتبط بكابتن واحد
- كتلة `captain {name, phone}` في كل webhook ومصدر العرض في المتجر (`OrderLogisticView`). مع عدة كباتن: تبقى = **كابتن مهمة التسليم** (وهذا يطابق `orders.driver_id` المشتق في 5.1).
- `timestamps.picked_up_at` و`eta_at` قيمة واحدة. تُعرَّف الآن: أول جمع مكتمل، وETA التسليم.
- قفل التعديل: تعديل الاستلام يُقفل عند `assigned` لأن «الكابتن اختير بقرب ذلك الاستلام». مع مهام الجمع: يُقفل **لكل محطة** عند إسناد مهمتها (قرار للأعمال).
- الـ webhooks تُبنى كلقطة وقت الإرسال، بتسلسل `revision`. تُلزِم الإبقاء على نفس الآلية.

### 13.3 ملاحظات موجودة اليوم (مستقلة عن المقترح، يُنصح بإصلاحها قريباً)
هذه تناقضات رصدها الفحص ولم أتحقق منها بالتشغيل:
1. **`StoreOrderEditPolicy`**: قائمة `PROGRESSION` لا تتضمن `accepted`، و`hasReached` ترجع true لأي حالة غير موجودة بها. النتيجة المتوقعة: طلب في حالة `accepted` يُعامَل كأنه تجاوز كل المراحل، فتُرفض كل مجموعات التعديل المقفلة (الاستلام والبنود والمال والتسليم والموعد)، بينما بيانات العميل والملاحظة تبقى قابلة للتعديل. دليل التكامل يقول إن الاستلام قابل للتعديل حتى `assigned` ولا يذكر `accepted`.
2. **دليل التكامل مقابل الكود**: الدليل (§5.4) يقول إن الإلغاء مسموح «من أي حالة غير نهائية» بينما الكود يرفضه في `picked_up` و`on_the_way` (409 `order_in_transit`). كود المتجر (`CancelGuard`) يطابق الكود.
3. **`StatusMap` في المتجر**: يحوّل فقط `picked_up` و`on_the_way` و`delivered`. أما `assigned` و`accepted` و`pending` فلا تغيّر شيئاً، خلافاً لجدول الدليل (§6).
4. **OpenAPI**: قائمة الحالات في `StoreOrderDetail` تحذف `accepted` مع أن الـ GET قد يعيدها.
5. **حدث `order.address_details`** معرَّف في الـ enum والعقد لكن لا شيء يرسله.
6. **تعليق قديم** في `statementTotalsFor`: يفترض أن النقد المحصَّل = `amount_to_collect` بينما يوجد الآن `amount_collected`.

> هذه نقاط تدقيق منفصلة وليست جزءاً من المقترح. أرشحها لتصحيح صغير سابق للمرحلة 1، لأن المرحلة 1 تتعامل مع حالات الطلب وحقوله.

### 13.4 اختبارات يجب أن تحمي كل مرحلة
الاختبارات الحالية التي تعرّف السلوك وتُعدَّل عند المرحلة 2 بالذات: `PerStopCollectionTest` (14)، `WrappedOrderTest` (36، تُعاد كتابتها في المرحلة 4)، `OrderHandoverTest` (15)، `CapacityE2ETest` و`ParallelAssignmentTest` (السعة والتسابق)، `DeliveryEarningTest` (16)، `StoreLedgerTest` و`StoreLedgerReconciliationTest`، `OfferAcceptanceTest` (15)، `OrderIntakeTest` و`StoreOrderUpdateTest` (عقد المتجر)، وفي المتجر `StatusConvergerTest` و`WebhookReceiverTest` و`CancelGuardTest`.

---

## 14. حالة التنفيذ (محدَّثة 2026-10-13)

### 14.1 ما نُفِّذ ودُمج في `dev` ودُفع

**المرحلة 1: كميات الأصناف والاستثناءات الأساسية (مكتملة)**
| البند | التفصيل |
|---|---|
| إصلاح تعارض قفل التعديل | `accepted` لم يكن ضمن تسلسل الحالات في `StoreOrderEditPolicy` فكان الطلب المقبول يرفض كل التعديلات. أُضيف مع اختبار جدولي. |
| كمية كل صنف مستلمة | `order_items.quantity_collected`. الكابتن يرسل `items[]` عند تأكيد المورّد. |
| إلغاء صنف | `POST /api/dashboard/orders/{order}/items/{item}/cancel`. يبقى الصنف مرئيًا بعلامة «ملغى» وسببه، وتُحسب كل المجاميع من الأصناف الحيّة، ويُعاد تسعير أجر التوصيل ويُعدَّل المبلغ المطلوب تحصيله. المورّد الذي فرغت أصنافه يُغلق ويخرج من المسار. |
| تعذّر الاستلام من مورّد وإعادة المحاولة | الكابتن: `PATCH …/pickups/{pickup}/failed` بسبب إلزامي، فيخرج المورّد من مساره وتُنبَّه الإدارة. المكتب: `POST …/pickups/{pickup}/retry` فيعود للمسار ويُحتسب `retry_count`. |
| سعر مختلف وملاحظة | `unit_price_paid` و`note` لكل صنف، و`note` للمورّد، و`orders.collection_note` على الطلب. |
| **إلزام السبب** | أي عدد مختلف، أو سعر صنف مختلف (سببه على الصنف نفسه)، أو مبلغ مدفوع يخالف المتوقع، يُرفض بـ 422 دون تسجيل شيء إن لم يُرفق السبب. يشمل المورّد الواحد والتأكيد الكامل للطلب. |
| اللوحة | عرض المستلم الناقص، وسعر الصنف المدفوع مقابل المطلوب، وملاحظات الكابتن، وأزرار إلغاء الصنف وإعادة الكابتن للمورّد. |

**المرحلة 2 (نسخة مُحسَّنة): تقسيم التجميع حسب المورّد**

اكتشاف أثناء التنفيذ يغيّر خطة المرحلة 2 (وهو تحسين على المقترح الأصلي): **ساق التجميع الحالي (`leg = collection`) هو بالفعل «مهمة» لكابتن واحد**، والنظام يسمح أصلًا بأكثر من ساق تجميع لتوصيل واحد (`collectionLegs` علاقة hasMany)، والتوصيل لا يُسند إلا بعد أن تصل **كل** سيقان التجميع (`assertGoodsHaveArrived`). إذن لتحقيق «مهام موردين لكباتن مختلفين» لا نحتاج جدول مهام جديدًا ولا تغيير وحدة الإسناد، بل **تقسيم ساق التجميع إلى ساق لكل مورّد**:

- `POST /api/dashboard/orders/{delivery}/split-collection` (صلاحية `orders.assign`): كل مورّد بعد الأول ينتقل مع أصنافه إلى ساق مستقلة (`-C2`، `-C3`).
- كل ساق: كابتن واحد، حصة سعة واحدة، خط زمني واحد، فلا يتغيّر الإسناد ولا السعة ولا الدفاتر ولا المسارات ولا الاختبارات القائمة.
- التوصيل يبقى محجوبًا حتى تُسلَّم **جميع** السيقان إلى مكان التغليف.
- لا تُقسَّم ساق يمسكها كابتن، أو استُلم منها شيء، أو تعذّر فيها مورّد.
- المتجر لا يعلم بشيء (السيقان الداخلية بلا `store_client_id`)، ولا مال جديدًا (الرسوم صفر، مسبقة الدفع).
- اللوحة: زر «تقسيم حسب المورّد» في طلب التوصيل المغلَّف، وشجرة السيقان تذكر مورّد كل ساق وكابتنها، وشريط الرحلة يقرأ كل السيقان.

**أثر ذلك على المراحل التالية:** جدول Task العام (توسعة `order_pickups` بـ `driver_id`) لم يعد شرطًا للمطلب الأساسي؛ يبقى مؤجَّلًا إلى أن نحتاج مهام لا تُعبَّر عنها بساق (مثل HUB أو استلام مورّد بكابتن آخر في طلب غير مغلَّف بلا نقطة تجميع).

### 14.2 قرارات المرحلة 0 التي اتُّخذت افتراضيًا (تحتاج تأكيدك)

| القرار | الافتراض المنفَّذ | لماذا |
|---|---|---|
| أجر الكابتن على ساق التجميع | لا أجر (كما كان) | لم تُحسم الصيغة. يبقى `DeliveryEarningService` يتجاهل `collection`. يُغيَّر لاحقًا بقرار واضح. |
| السعة لكل ساق | حصة واحدة لكل ساق | يطابق السلوك القائم دون تعديل. |
| تغيّر سعر الصنف عند المورّد | **لا يغيّر ما يدفعه العميل**، ويُسجَّل فرقًا للمكتب | قرار الأعمال: من يتحمّل الفرق؟ |
| إلزام السبب | في كل حالة اختلاف | طلبك. |

### 14.3 المتبقي من الخطة

1. **التقسيم التلقائي عند الإنشاء** (إعداد على المتجر أو الطلب): اليوم التقسيم يدوي من اللوحة.
2. **إسناد ذكي لكل ساق** من محرّك الاقتراح (يعمل أصلًا على كل ساق كطلب).
3. **المرحلة 3:** محرّك الحالة المشتقة.
4. **المرحلة 4:** توحيد التغليف تحت طلب واحد (إن بقي مبرِّرها بعد التقسيم).
5. **أجر الكابتن للتجميع** بعد حسم القرار.
6. **تطبيق الكابتن:** يحتاج حقل السبب (`note`) وأزرار «تعذّر الاستلام» وإرسال الكميات والأسعار. خارج المستودعين.

---

## 15. المرحلة 3: الافتراضات المقترحة وما نُفِّذ منها (2026-10-14)

الأسئلة 4 و5 و6 من القسم 9 كانت تحجب المرحلة 3. هذه إجاباتها **المقترحة** (تنتظر تأكيدك أو تصحيحك)، وما نُفِّذ منها.

| السؤال | الافتراض المقترح | الحالة |
|---|---|---|
| 4. التسليم الجزئي عند غياب بند | **يُسلَّم ما استُلم، ولا يُنتظر المفقود** ما لم يقرر المكتب غير ذلك. تقبل الإدارة النقص بندًا بندًا فيُخفَّض البند إلى ما استُلم (أو يُلغى إن لم تصل قطعة) ويُعاد تسعير أجر التوصيل ويتبع المبلغ المطلوب تحصيله ما استُلم. | **منفَّذ** |
| 6. مهلة المورّد المتأخر | **20 دقيقة** (`DISPATCH_SUPPLIER_WAIT_MINUTES`) من وقت جاهزية البضاعة (`ready_at`) أو من قبول الكابتن. بعدها **تنبيه للإدارة فقط**، مرة في اليوم لكل مورّد. لا إلغاء ولا إعادة إسناد تلقائيان، لأن القرار (انتظار، إعادة الكابتن لاحقًا، نقل الرحلة لكابتن آخر، إلغاء الأصناف) يحتاج من يتصل بالمورّد. | **منفَّذ** |
| 5. البدائل (SUBSTITUTE) وموافقة العميل | **أُلغيت حاليًا بقرار المالك**: لا اقتراح بدائل من الكابتن أو المكتب، ولا سؤال للعميل، ولا حدث ولا نقطة نهاية بين Logistic والمتجر. الصنف غير المتوفر يُعالَج كنقص (قبول النقص أو إلغاء الصنف). | **أُزيلت** |

### ما بُني في المرحلة 3

- **قبول النقص:** `POST /api/dashboard/orders/{order}/items/{item}/accept-shortage` (صلاحية `orders.cancel`). يجري قبل أن يكون الطلب في طريقه إلى العميل (بعد ذلك يكون المبلغ الذي أُخبر به قد صار عند الباب). لا يُقبل على ساق التجميع بل على طلب التوصيل.
- **ربط نسختَي كل بند في الطلب المغلَّف** (`order_items.counterpart_id`): قائمة التوصيل (ما يراه العميل والمتجر) وقائمة التجميع (حسب المورّد) هما نسختان من البنود نفسها، ولم يكن في النظام ما يربط كل بند بنظيره. العدّ الذي يأخذه الكابتن عند المورّد يُسجَّل على ساق التجميع ويصل بهذا الربط إلى بند التوصيل الذي يدفع العميل ثمنه. الرابط يبقى سليمًا بعد تقسيم التجميع. الطلبات المغلَّفة التي فُتحت قبل هذا التغيير بلا رابط ولا تُسوَّى تلقائيًا (تُرفض بـ 422 برسالة واضحة).
- **تنبيه تأخر المورّد:** `orders:watch-supplier-delays` كل 5 دقائق.
- **اللوحة:** شارة «ناقص N» على البند، وزر «قبول النقص» مع حوار السبب والتحذير من إعادة حساب الأجر، وعرض «N من M المطلوبة» مع السبب.

### ما تبقى بعد هذا

1. ~~البدائل وموافقة العميل~~ **أُلغيت حاليًا بقرار المالك** (أُزيلت من Logistic واللوحة، وحُذف فرع المتجر).
2. **إلغاء بند قبل الجمع على طلب مغلَّف** (اليوم يُرفض)، وهو ممكن الآن بفضل الربط.
3. **محرّك الحالة المشتقة:** بعد التقسيم والنقص والاستثناءات، الحالة المخزنة لطلب التوصيل ما زالت تنتقل بفعل كابتنه وحده، وهي كافية. يُؤجَّل المحرّك إلى أن يظهر مطلب لا تغطيه، لأن بناءه الآن إعادة كتابة بلا مكسب.
4. **أجر الكابتن للتجميع** (قرار الأعمال رقم 1).

---

## 16. الحالة النهائية للخطة (2026-10-14)

### أُنجز في هذه الجولة

| البند | الحالة |
|---|---|
| إلغاء بند قبل الجمع على طلب مغلَّف | **منفَّذ.** يُلغى البند على طلب التوصيل، فينتقل الإلغاء إلى نظيره في ساق التجميع، ويُفرَّغ مورّده، وتُلتقَط الساق إن كان آخر مورّد بقي. |
| ربط الطلبات المغلَّفة القديمة | **منفَّذ.** `orders:link-wrapped-items` (يعمل أيضًا داخل migration) يربط بنود الطلبات المفتوحة قبل الربط، بحسب ماهية البند لا موضعه، فيصمد أمام التقسيم. |

### ما تبقى، ولماذا

1. **أجر الكابتن للتجميع:** لم يُحسم قرار الأعمال (قيمته وموعد استحقاقه)، فيبقى صفرًا كما كان.
2. **تطبيق الكابتن:** يحتاج إضافة حقل السبب (`note`) و`unit_price_paid` وزر «تعذّر الاستلام». خارج المستودعات الثلاثة.
3. ~~إخطار العميل بالبديل داخل المتجر~~ أُلغي مع إلغاء البدائل.
4. **محرّك الحالة المشتقة، وتوحيد التغليف تحت طلب واحد، وHub/Cross-Dock/Shipment Unit:** لم تُبنَ عمدًا. الحالة المخزنة لطلب التوصيل كافية بعد التقسيم والاستثناءات، ولا ميزة اليوم تحتاج هذه الأجزاء. تُبنى حين يظهر مطلب لا يُغطّى بدونها.
5. **اختباران للتزامن:** `ParallelAssignmentTest` و`ParallelOrderNumberTest` تتجمدان على هذا الجهاز (والأولى تتجمد على `dev` أيضًا). تُراجَعان في CI.

---

## 17. إغلاق الخطة (2026-10-14)

| البند | الحالة |
|---|---|
| أجر الكابتن للتجميع | **منفَّذ كمفتاح يضبطه العمل:** `COLLECTION_LEG_EARNING` (قيمة ثابتة لكل رحلة تجميع تُسلَّم، للكابتن المستقل فقط، والموظف بلا أجر لكل رحلة). الافتراضي **صفر** فلا يتغيّر شيء حتى يُحسم الرقم. التقسيم يعني أجرًا لكل رحلة كابتن. |
| **محرّك الحالة (Order State Engine)** | **منفَّذ كإسقاط للقراءة فقط (`fulfillment`)** على تفاصيل الطلب: حالة واحدة للطلب كله تتبع **كل** سيقان التجميع (`awaiting_collection`، `collecting`، `collection_failed`، `ready_to_deliver`، `delivery_assigned`، `delivering`...)، وتقدّم الاستلام، وما سيصل مقابل ما طُلب، وما يحتاج تدخّلًا. لا يُخزَّن ولا يستبدل الحالة المخزنة، فلا يمكن أن يخالف الصفوف التي يُشتق منها. |
| سلسلة العهدة (Shipment/Handoff) | **منفَّذة كمشتقة** داخل `fulfillment.custody`: من استلم من كل مورّد، ومن سلّم لمحل التغليف، ومن استلم من المحل، ومن سلّم للعميل، مرتبة زمنيًا ومن بيانات مسجَّلة أصلًا. لا جدول جديد. |
| الاستراتيجيات (DIRECT / WRAPPING) | ظاهرة في `fulfillment.strategy`. HUB وCROSS_DOCK لم تُبنَ: لا مستودع في النظام. |
| وثيقة لفريق تطبيق الكابتن | `docs/captain-app-fulfillment-changes.md`: كل ما تغيّر في الـ API (وأيّها يكسر بناءً قديمًا: إلزام السبب). |
| اللوحة | بطاقة «التنفيذ» في صفحة الطلب. |

### قرارات تصميم أُخذت وتُبرَّر
- **لم يُوحَّد التغليف تحت طلب واحد (المرحلة 4):** بعد تقسيم التجميع صار كل ساق مهمة كابتن بالفعل، والتوصيل ينتظر الكل. توحيدها تحت صف واحد كان سيُعيد كتابة الإسناد والسعة والأرباح والدفاتر والمسارات والاختبارات دون ميزة جديدة، ويكسر قرار «صف لكل كابتن» الذي بُني عليه النظام (انظر `docs/feature-10-wrapped-orders.md`). المطلب العملي كله (مهام لكباتن مختلفين، عهدة، استثناءات، موافقة العميل) تحقق بدونه.
- **لم تُخزَّن الحالة المشتقة:** نسخة ثانية للحقيقة تعني إمكانية الاختلاف. تُحسب عند القراءة.

### ما يبقى خارج هذه المستودعات أو ينتظر قرارًا
1. تعديل تطبيق الكابتن (الوثيقة أعلاه).
2. ~~دمج فرع المتجر~~ لم يعد هناك فرع للمتجر ولا ملف SQL للبدائل: لا يلزم شيء على المتجر.
3. رقم `COLLECTION_LEG_EARNING` (قرار أعمال).
4. ~~طريقة سؤال العميل عن البديل~~ أُلغيت مع إلغاء البدائل.
5. اختباران للتزامن يتجمدان على هذا الجهاز (`ParallelAssignmentTest`، `ParallelOrderNumberTest`)؛ يُراجعان في CI.

---

## 18. الإسناد لكل مورّد (2026-10-14)

بقرارك: الإسناد صار **لكل مورّد وليس لكل طلب**، دون تغيير جذري.

- **الوحدة:** كل مورّد = رحلة استلام (leg) هي طلب مستقل الصف يُسند بالإسناد المعتاد (اقتراحات، عرض بمهلة، سعة، أرباح). لا جدول Task جديد.
- **أي طلب متعدد الموردين:** `POST /api/dashboard/orders/{order}/split-collection`.
  - طلب **مغلَّف:** تُقسَّم رحلة تجميعه إلى رحلة لكل مورّد (كما كان).
  - طلب **عادي (جديد):** يبقى هو نفسه (uuid، الرقم، العميل، المال، مراجع المتجر) ويصير **رحلة التوصيل** ونقطة استلامه الوحيدة هي **نقطة التجميع (Hub)**، وينتقل كل مورّد مع بنوده إلى رحلة استلام تتجه إلى الـ Hub. البنود تُربط بنظيراتها فيصل النقص إلى سلة العميل.
- **نقطة التجميع:** `DISPATCH_HUB_NAME/ADDRESS/LAT/LNG` في `.env`. بدونها يُرفض التقسيم برسالة واضحة (`hub_not_configured`) وتقول اللوحة ذلك.
- **متى:** الطلب معلّق، بلا كابتن، ولم يُستلم شيء ولم يتعذّر مورّد.
- **التوصيل ينتظر الكل:** كابتن واحد يوصّل من الـ Hub بعد وصول كل الرحلات.
- **عند الإنشاء:** `consolidate_at_hub: true` في نموذج الإنشاء يفتح الطلب هكذا مباشرة.
- **حماية:** المتجر لم يعد يستطيع تغيير نقاط الاستلام أو البنود لطلب يُنفَّذ على عدة قطع (كان ذلك يكسر الربط).
- **اللوحة:** بطاقة «الإسناد لكل مورّد» تسرد كل مورّد برحلته وحالتها وكابتنها وزر «إسناد كابتن».
- **ما لم يتغيّر:** الطلب العادي بمورّد واحد، والطلب العادي متعدد الموردين الذي لا يُقسَّم (يبقى كابتن واحد يمرّ عليهم).

