Update: 2026-07-31 03:00:06

This commit is contained in:
Hamza-Ayed
2026-07-31 03:00:06 +03:00
parent 7a576b7327
commit f94a9478ac
13 changed files with 1609 additions and 20 deletions
+333
View File
@@ -0,0 +1,333 @@
# خطة إضافة «طلبات الطعام» إلى منصة سيرو — المعمارية والدوكر والتنفيذ
> الحالة: مقترح للتنفيذ — لم يُكتب أي كود بعد.
> التاريخ: 2026-07-30
> المرجع المعماري: [docker/docker-compose.yml](../../docker/docker-compose.yml) و [docs/30-siro-port-plan.md](../30-siro-port-plan.md)
---
## ١. الخلاصة التنفيذية (القرار في عشرة أسطر)
- «طلبات الطعام» تُضاف كـ **وحدة (module) مستقلة داخل نفس منصة سيرو**، لا كمشروع منفصل ولا كخدمة مصغّرة (microservice) بقاعدة مستخدمين خاصة بها.
- الحاويات: نضيف **حاويتين فقط** إلى نفس `docker compose`: حاوية `php_food` (fpm للطلبات المتزامنة) وحاوية `socket_food` (عملية Workerman دائمة لحالة الطلب اللحظية).
- المشترَك يبقى مشترَكاً: **نفس nginx، نفس MySQL، نفس Redis، نفس JWT، نفس المحفظة** — لأن هذا هو ما يجعل التطبيق «متكاملاً» بدل أن يكون تطبيقين ملصوقين.
- المعزول يبقى معزولاً: **قاعدة بيانات `siro_food` مستقلة**، مساحة أسماء مفاتيح Redis مستقلة `food:*`، وحدود ذاكرة ولوغات مستقلة.
- الفائدة الحقيقية من العزل ليست الأداء (الحاوية على لينكس = عملية عادية)، بل: انهيار الطعام لا يُسقط الرحلات، ونشر الطعام لا يتطلب إعادة تشغيل الباك إند، وأي عميل وايت-ليبل يشغّل الطعام أو يطفئه بمتغيّر واحد.
---
## ٢. المبدأ الحاكم: ما الذي يُشارَك وما الذي يُعزل؟
القاعدة التي بُني عليها هذا التصميم — وهي نفس فلسفة `docker-compose.yml` الحالية «التقسيم بالدور، لا حاوية-واحدة-فيها-كل-شيء ولا تفتيت microservices»:
| المكوّن | القرار | السبب |
|---|---|---|
| هوية المستخدم (JWT) | **مشترك** | الراكب نفسه هو الزبون نفسه. حسابان لشخص واحد = كارثة منتج ودعم فني. |
| المحفظة والدفع | **مشترك** (`payment_server/v2`) | رصيد واحد للراكب يُنفَق على الرحلة والوجبة. تكرار المحفظة يعني تسويتين ماليتين متضاربتين. |
| الإشعارات (FCM) | **مشترك** (`core/Services/FcmService.php`) | مسار الرسائل مُشخَّص ومُسجَّل بالفعل. |
| Redis | **مشترك، بمساحة أسماء منفصلة** | نفس المثيل، لكن كل مفاتيح الطعام تبدأ بـ `food:` — لا تصادم مع حالة الرحلة. |
| قاعدة البيانات | **معزولة: `siro_food`** | نفس نمط `siro_transit`. يمنع أن يقفل جدول طلبات مزدحم استعلامات الرحلات. |
| كود PHP وحاوية fpm | **معزولة: `backend/food/` + `php_food`** | نشر مستقل، حد ذاكرة مستقل، وانهيار مستقل. |
| السوكيت | **معزول: `socket_food`** | العملية الدائمة لا تُخلَط أبداً مع fpm (نفس سبب فصل `socket_driver`). |
| أسطول التوصيل | **مشترك مع تمييز بالدور** | نفس السائقين، مع علم `can_deliver`. تفصيل هذا في §٦. |
**ما لن نفعله (قرارات مرفوضة صراحةً):**
1. لن نبني تطبيق Flutter خامساً للزبون — الطعام يدخل داخل `siro_rider` كتبويب.
2. لن ننشئ جدول مستخدمين جديداً في `siro_food`؛ نخزّن `passenger_id` كمرجع منطقي فقط.
3. لن نضع الطعام داخل حاوية `php` الحالية — لأن أي خطأ فادح في الطعام سيستهلك حوض fpm نفسه الذي يخدم الرحلات.
4. لن نفتح بورت السوكيت للعالم مباشرة (انظر §٤ — درس مؤلم مدفوع الثمن سابقاً).
---
## ٣. طبقة الدوكر — الشكل النهائي
### ٣.١ الحاويات الجديدة
تُضاف إلى نفس `docker/docker-compose.yml` (لا ملف compose ثانٍ — ملفّان يعنيان شبكتين وحيرة تشغيلية):
```yaml
# ‏وحدة الطعام — fpm مستقلة عن fpm الرحلات عمداً:
# ‏انهيار الطعام يجب ألا يبتلع حوض العمليات الذي يخدم الرحلات.
php_food:
build:
context: ./php
dockerfile: Dockerfile.fpm # نفس الصورة تماماً — لا صيانة مزدوجة
args:
PHP_VERSION: "${PHP_VERSION:-8.2}"
volumes:
- ../backend:/var/www/backend # يحتاج core/ و functions.php المشتركة
- ./php/opcache.ini:/usr/local/etc/php/conf.d/zz-opcache.ini:ro
- ./php/food-pool.conf:/usr/local/etc/php-fpm.d/zz-pool.conf:ro
- ./keys:/keys:ro
env_file: .env
depends_on: [mysql, redis]
mem_limit: 1g
restart: unless-stopped
# ‏سوكيت الطعام — WS بورت 4040 + HTTP داخلي 4041
socket_food:
build:
context: ./php
dockerfile: Dockerfile.socket
args:
PHP_VERSION: "${PHP_VERSION:-8.2}"
command: ["php", "food_socket.php", "start"]
working_dir: /app
volumes:
- ../food_server:/app
- ./keys:/keys:ro
env_file: .env
ports:
# ‏لا نفتح 4040 للعالم: Workerman نصّ صريح والتطبيق يطلب TLS فتتجمّد المصافحة.
# ‏nginx على المضيف يستمع 4040 بالشهادة ويمرّر إلى 14040 هنا.
- "127.0.0.1:14040:4040"
# ‏و4041 داخلي فقط: الباك إند يناديه عبر http://socket_food:4041
depends_on: [redis]
mem_limit: 512m
restart: unless-stopped
```
### ٣.٢ تعديل nginx (حاوية البوابة)
في [docker/nginx/default.conf](../../docker/nginx/default.conf) يُضاف توجيه مسار الطعام إلى حوض fpm الخاص به:
```nginx
# كل ما تحت /backend/food/ يذهب إلى حوض fpm المستقل
location ~ ^/backend/food/.*\.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_pass php_food:9000; # ← لا php:9000
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 60s;
}
```
> ترتيب البلوكات مهم: هذا البلوك يجب أن يسبق `location ~ \.php$` العام، وإلا ابتلعه الأخير وذهبت الطلبات إلى الحوض الخطأ بصمت.
وتُضاف نقطة حالة الحوض `/(fpm-status-food)` بنفس قيود العنوان الحالية، وإلا سنراقب حوضاً واحداً ونظن أننا نراقب اثنين.
### ٣.٣ nginx المضيف (TLS للسوكيت) — إلزامي لا اختياري
يُضاف مقطع في `nginx/siro-sockets-tls.conf` على المضيف: استماع `4040` بالشهادة → تمرير إلى `127.0.0.1:14040`. هذا **ليس تحسيناً**: تكرار خطأ الماضي (نشر بورت السوكيت خاماً) ينتج مصافحة مجمّدة و timeout في التطبيق بلا أي سطر خطأ يدل عليه.
### ٣.٤ جدار الحماية
لا تعتمد على UFW لحجب بورت الطعام: `docker-proxy` يلتفّ على قواعد UFW. الحجب يتم بربط البورت بـ `127.0.0.1` في compose كما هو مكتوب أعلاه — وهذا كافٍ وحده.
### ٣.٥ المتغيّرات الجديدة في `docker/.env.example`
```
FOOD_ENABLED=true
DB_FOOD_NAME=siro_food
DB_FOOD_HOST=mysql
DB_FOOD_USER=siro_food
DB_FOOD_PASS=
FOOD_SOCKET_URL=http://socket_food:4041
FOOD_COMMISSION_PERCENT=15
FOOD_DELIVERY_BASE_FEE=
FOOD_MAX_ACTIVE_ORDERS_PER_USER=3
```
### ٣.٦ فخ النشر الذي يجب تفاديه مسبقاً
`vendor/` و `composer.lock` مُستثنيان من git في بعض مسارات المشروع — وهذا ما قتل `payment_server/v2` على نشر نظيف سابقاً. لذلك:
- `food_server/composer.lock` **يُلتزم به في git إلزامياً**.
- تُضاف إلى [docker/README.md](../../docker/README.md) خطوة صريحة:
`docker compose run --rm socket_food composer install`
---
## ٤. طبقة البيانات — قاعدة `siro_food`
### ٤.١ التسجيل في طبقة الاتصال
يُضاف مدخل واحد إلى الخريطة في [backend/core/Database/Database.php](../../backend/core/Database/Database.php):
```php
'food' => [
'name' => 'DB_FOOD_NAME',
'host' => 'DB_FOOD_HOST',
'user' => 'DB_FOOD_USER',
'pass' => 'DB_FOOD_PASS',
],
```
وقاعدة صارمة تُوثَّق وتُراجَع في كل PR — نفس قاعدة transit: **ممنوع `Database::get('main')` داخل `backend/food/`**. أي حاجة لبيانات الراكب تُقرأ عبر دالة خدمة واحدة معرّفة في `food/functions.php`، لا باستعلامات متفرقة عبر القواعد.
### ٤.٢ المخطط `schema_food.sql` (الجداول الأساسية)
```
merchants المتجر/المطعم: الاسم، الموقع (lat/lng)، الحالة، نسبة العمولة، أوقات العمل
merchant_users حساب دخول صاحب المطعم (هوية منفصلة عن الراكب — دور merchant)
menu_categories أقسام القائمة داخل المطعم
menu_items الصنف: السعر، الصورة، متاح/غير متاح، وقت التحضير التقديري
item_options الإضافات والخيارات (حجم، إضافات) وسعر كل خيار
orders الطلب: passenger_id، merchant_id، courier_id، الحالة، الإجماليات، العنوان
order_items أصناف الطلب بسعر **مجمّد وقت الطلب** (لا JOIN على menu_items للسعر)
order_status_log كل انتقال حالة مع الوقت والفاعل — مصدر الحقيقة للنزاعات
order_payments مرجع معاملة المحفظة/الدفع + حالة التسوية
courier_assignments محاولات إسناد الطلب للسائق: عُرض/قُبل/رُفض/انتهت المهلة
merchant_payouts مستحقات المطاجر ودورات التسوية
food_promo_codes أكواد الخصم (منفصلة عن أكواد الرحلات)
```
**قواعد مخطط غير قابلة للتفاوض:**
1. **تجميد الأسعار**: `order_items` يحمل `unit_price` و `option_price_json` منسوخين وقت الإنشاء. تغيير المطعم لسعره لاحقاً يجب ألا يعيد كتابة تاريخ طلب مكتمل.
2. **المال بالأعداد الصحيحة**: كل المبالغ `BIGINT` بأصغر وحدة نقدية، لا `FLOAT` مطلقاً.
3. **مفتاح تعطيل التكرار (idempotency)**: عمود `client_order_uuid UNIQUE` على `orders` — الضغط المزدوج على «تأكيد» أو إعادة محاولة الشبكة يجب ألا ينتج طلبين ولا خصمين.
4. **الحالة كسلسلة محكومة**: `pending → merchant_accepted → preparing → ready → courier_assigned → picked_up → delivered` وفروع `cancelled_by_*` و `rejected`. الانتقالات تُفرَض في دالة واحدة `food_transition_status()` — لا `UPDATE orders SET status` مبعثرة في الملفات.
5. **الفهارس منذ اليوم الأول**: `(passenger_id, created_at)`، `(merchant_id, status)`، `(courier_id, status)`، ومكاني على `merchants(lat,lng)`.
---
## ٥. طبقة الـ API — البنية والمسارات
نتبع نمط «بوابة لكل جمهور» المستخدَم في `backend/transit/` حرفياً، لأنه ما يجعل الوحدة تبدو جزءاً أصيلاً من المشروع لا طُعماً غريباً.
```
backend/food/
├── connect_app.php بوابة الزبون (JWT الراكب)
├── connect_merchant.php بوابة المطعم (JWT بدور merchant)
├── connect_courier.php بوابة السائق (JWT السائق)
├── connect_admin.php بوابة الإدارة (JWT admin/super_admin)
├── functions.php منطق مشترك: انتقال الحالة، الحسابات، صياغة الردود
├── schema_food.sql
├── merchant/ browse.php details.php menu.php search.php
├── cart/ validate.php quote.php
├── order/ create.php status.php cancel.php rate.php history.php
├── merchant_ops/ incoming.php accept.php reject.php ready.php items_toggle.php
├── courier/ offer_respond.php picked_up.php delivered.php active.php
├── admin/ merchants.php orders.php payouts.php settings.php
└── cron_*.php انتهاء مهلة الطلبات، التسويات، تنبيهات التأخير
```
كل بوابة تكرّر نفس الافتتاحية المثبتة في المشروع: `core/bootstrap.php` ثم `RateLimiter` ثم `JwtService::authenticate()` ثم `Database::get('food')` مع رد `503` نظيف عند فشل الاتصال.
**قواعد التعامل مع API الحالي — مأخوذة من سلوكه الفعلي:**
- لا تعتمد `$_GET`؛ المدخلات تُقرأ كما يقرؤها باقي المشروع (جسم الطلب)، وترويسة `X-Device-FP` مطلوبة.
- شكل الرد يطابق مغلّف الرسائل القائم (`status` + `message` + الحمولة) — التطبيق الحالي يفكّه بهذا الشكل، وأي شكل جديد سيكسر المُحلِّل المشترك.
- تعدد اللغات: نصوص الحالة تُعاد كمفاتيح، والترجمة في التطبيق.
**الأمن — البنود التي أُخذت من إخفاقات وحدة مواصلاتي، فلا تتكرر:**
1. **IDOR أولاً وقبل كل شيء**: كل نقطة تأخذ `order_id` تتحقق أن الطلب يخص الفاعل (زبونه أو مطعمه أو سائقه). يُكتب اختبار سلبي واحد على الأقل لكل نقطة قبل الدمج.
2. **تفويض الدور على مستوى البوابة**: `connect_merchant.php` يرفض توكن الراكب حتى لو كان صالحاً.
3. **الخوادم لا تثق بالأسعار**: السعر النهائي يُحسب في الخادم من `menu_items` — إجمالي يرسله العميل يُتجاهل ويُسجَّل كإشارة احتيال.
4. **تحديد المعدل**: حد أشدّ على `order/create.php` (منع فيض الطلبات الوهمية) وعلى `merchant_ops/*`.
5. **رفع صور القائمة**: تحقق من النوع والحجم، إعادة ترميز، أسماء عشوائية، ونشر من مسار لا ينفّذ PHP.
6. **بيانات شخصية**: عنوان الزبون ورقمه يظهران للسائق **فقط** بعد `courier_assigned` و**يُحجبان** بعد `delivered`.
---
## ٦. التوصيل — إعادة استخدام أسطول الرحلات
هذا أهم قرار منتجي/تقني في الخطة.
**القرار:** لا أسطول ثانٍ. نستخدم نفس السائقين مع علم `can_deliver` وحالة تفرّغ، اعتماداً على بنية `geo:drivers:available` القائمة في Redis.
**الآلية:**
1. عند `ready` (أو قبله بوقت التحضير التقديري)، يستدعي الباك إند بحثاً جغرافياً حول موقع المطعم — نفس نمط `georadius` في [loction_server/find_drivers_redis.php](../../loction_server/find_drivers_redis.php)، لكن على مفتاح `geo:couriers:available` الذي يُملأ بالسائقين ذوي `can_deliver=1` وليسوا في رحلة.
2. العرض يُرسل لسائق واحد في كل مرة بمهلة قصيرة (15–20 ثانية)، ويُسجَّل كل عرض في `courier_assignments`. الصمت = رفض ضمني وانتقال للتالي. هذا يمنع «سباق القبول» الذي ينتج طلباً بسائقين.
3. القفل: `SET food:order:{id}:lock <courier_id> NX EX 20` — القابل الأول فقط يفوز، ذرّياً.
4. سائق في رحلة نقل لا يظهر لعروض التوصيل والعكس — حالة السائق مصدر حقيقة واحد في Redis، لا علمان متنافسان.
5. الأثر على أرباح السائق: التوصيل يدخل نفس دفتر الأرباح ونفس تلميح الأرباح المعتمد في تطبيق السائق، لا شاشة أرباح موازية.
**المخاطرة الواجب مراقبتها:** في ساعة الذروة تتنافس الوجبات والرحلات على الأسطول نفسه. المؤشر الحارس: نسبة الطلبات التي لم تجد سائقاً خلال 5 دقائق. إن تجاوزت ١٠٪ نُفعّل تخصيص جزء من الأسطول للتوصيل في نطاق زمني/جغرافي — لكن **لا نبني هذا التعقيد قبل أن يثبت الرقم أنه لازم**.
---
## ٧. المال — الدفع والعمولة والتسوية
- الدفع يمر عبر `payment_server/v2` نفسه؛ الطعام لا يفتح قناة دفع جديدة.
- **الحجز ثم الالتقاط**: عند إنشاء الطلب يُحجز المبلغ من المحفظة (`hold`)، ويُلتقط عند `delivered`، ويُفكّ الحجز فوراً عند `rejected` أو `cancelled`. أي مسار إلغاء لا يفكّ الحجز يعني مالاً محتجزاً بلا سبب — وهذا أسرع طريق لفقدان ثقة المستخدم.
- الدفع نقداً عند الاستلام: يُحصّله السائق، فيُقيَّد ديناً على محفظته ويُسوّى مع المطعم في `merchant_payouts` — نفس آلية تسوية النقد القائمة للرحلات.
- تفكيك كل طلب مسجَّل صراحةً: `items_total + delivery_fee + service_fee − discount`، ونصيب المنصة = `commission_percent` من `items_total` فقط (لا من رسوم التوصيل)، ونصيب السائق من رسوم التوصيل.
- رسوم التوصيل تُحسب في **محرك التسعير القائم** [backend/pricing-engine](../../backend/pricing-engine) بمعامل خاص بالطعام، لا بمعادلة جديدة مكرّرة — تكرار منطق التسعير هو المصدر التاريخي لتذبذب الأسعار في هذا المشروع.
- التسعير **مثبّت لحظة عرض السلة**: عرض السعر يُوقَّع ويصلح لمدة 10 دقائق. لا يجوز أن يتغير الإجمالي بين شاشة التأكيد وشاشة الدفع.
---
## ٨. الزمن الحقيقي — سوكيت الطعام
- قنوات الاشتراك: `food:order:{id}` (الزبون)، `food:merchant:{id}` (لوحة المطعم)، `food:courier:{id}` (السائق).
- المصدر الوحيد للحقيقة هو قاعدة البيانات؛ السوكيت **ناقل إشعار لا مخزن حالة**. عند إعادة الاتصال يسحب التطبيق `order/status.php` ويُصحّح نفسه — هذا ما يمنع «الطلب معلّق للأبد» بعد انقطاع شبكة.
- موقع السائق أثناء التوصيل يُبثّ من نفس تدفق المواقع القائم؛ لا مسار تتبّع ثانٍ.
- كل حدث سوكيت **مصحوب بإشعار FCM** لحالات المفصل (قُبل، جاهز، خرج للتوصيل، وصل) — لأن التطبيق في الخلفية لا يملك سوكيتاً حياً.
- تشغيل السوكيت **داخل الحاوية فقط**: لا يُطلق أبداً على المضيف مباشرة (المضيف لا يصل إلى Redis داخل الشبكة، والنتيجة انقطاع صامت). إعادة التشغيل: `docker compose restart socket_food`.
---
## ٩. طبقة التطبيقات
| التطبيق | العمل المطلوب |
|---|---|
| `siro_rider` | تبويب «طعام»: تصفح المطاعم، القائمة، السلة، الدفع، تتبّع الطلب، السجل والتقييم. خلف علم `FOOD_ENABLED` يأتي من إعدادات الخادم — لا نسخة تطبيق جديدة لإطفائه. |
| `siro_driver` | نوع مهمة جديد «توصيل» داخل تدفّق العروض القائم: بطاقة عرض، استلام من المطعم، تسليم، إثبات تسليم. |
| لوحة المطعم | **ويب متجاوب داخل `dashboard/`** لا تطبيق أصلي. صاحب المطعم يعمل على شاشة المحل، والويب يُنشر فوراً بلا دورة متجر. هذا يوفّر أشهر عمل. |
| `siro_admin` | إدارة المطاعم والاعتماد، مراقبة الطلبات، التسويات، إعدادات العمولة والرسوم. |
قاعدة توحيد الواجهة: الطعام يستخدم نفس نظام الألوان والمكوّنات وطبقة الشبكة الحالية في التطبيق. أي مكوّن «مقتبس من تطبيق طعام آخر» بمظهر مختلف يجعل الميزة تبدو ملصقة.
---
## ١٠. خطة التنفيذ على مراحل
كل مرحلة تنتهي بشيء **قابل للتشغيل والاختبار**، لا بكود على الرف.
**المرحلة صفر — الأساس (بلا منطق منتج)**
حاويتان جديدتان في compose، مسار nginx، `Database::get('food')`، `schema_food.sql`، بوابة `connect_app.php` ترد على `ping`. معيار الإنجاز: `docker compose up -d` يرفع ثماني خدمات، ونداء ping يرد 200 من الحوض الجديد (يُتحقق من الحوض عبر `fpm-status-food`).
**المرحلة الأولى — الكتالوج (للقراءة فقط)**
المطاعم والأقسام والأصناف، تصفح وبحث، لوحة الإدارة لإنشاء مطعم. معيار الإنجاز: مطعم حقيقي واحد بقائمة كاملة يظهر في التطبيق.
**المرحلة الثانية — الطلب بلا مال**
السلة، التسعير من الخادم، إنشاء الطلب، آلة الحالة، لوحة المطعم، السوكيت والإشعارات. الدفع نقداً فقط. معيار الإنجاز: طلب حقيقي يمر `pending → delivered` وسجل الحالات مكتمل.
**المرحلة الثالثة — التوصيل**
`geo:couriers:available`، حلقة العروض والقفل، مهام السائق، تتبّع الموقع. معيار الإنجاز: ٢٠ طلباً تجريبياً بلا طلب واحد بسائقين ولا طلب يتيم.
**المرحلة الرابعة — المال**
حجز/التقاط المحفظة، العمولة، تسويات المطاعم، تسوية نقد السائق، تقارير الإدارة. معيار الإنجاز: مطابقة مالية لمئة طلب تجريبي بفرق صفر.
**المرحلة الخامسة — التقسية والإطلاق**
مراجعة أمنية (تركيزها IDOR والتفويض)، اختبار ضغط، سجلات ومؤشرات، إطلاق تدريجي على منطقة واحدة ومطاعم محدودة.
---
## ١١. الاختبار ومعايير القبول
- **اختبار ضغط** بنفس أدوات [stress_test](../../stress_test) وبنفس قاعدة القراءة الصادقة المعتمدة في `docker/README.md`: الرقم المُلتزَم به هو الرقم الذي عبر الاختبار فعلاً، لا أكثر. الهدف الابتدائي: ١٠٠٠ طلب/ساعة بـ p95 < 500ms، مع التحقق أن **زمن استجابة الرحلات لم يتأثر** أثناء الحمل — هذا هو اختبار العزل الحقيقي.
- **حالات حافة إلزامية**: ضغط مزدوج على التأكيد، انقطاع الشبكة بين الحجز والإنشاء، رفض المطعم بعد الدفع، صنف نفد أثناء التحضير، إلغاء الزبون بعد استلام السائق، سائق تعطّل تطبيقه وهو حامل الطلب.
- **اختبارات سلبية للتفويض** لكل نقطة نهاية — تُدمج مع الكود لا بعده.
- **مراقبة**: لوحة تحمل الأربعة أرقام التي تصف صحة الخدمة فعلاً — نسبة قبول المطاعم، زمن التحضير، زمن إيجاد سائق، نسبة الإلغاء ومصدره.
---
## ١٢. النشر والتراجع
- `FOOD_ENABLED=false` يخفي الميزة من التطبيق كلياً بلا نشر جديد — هذا هو مفتاح التراجع الأول والأسرع.
- التراجع الكامل: `docker compose stop php_food socket_food`. الرحلات لا تتأثر إطلاقاً — وهذا بالضبط ما اشتريناه بالعزل.
- لا تُشغَّل ترحيلات مخطط الطعام على قواعد الرحلات؛ `siro_food` منفصلة تماماً ونسخها الاحتياطي منفصل.
- **تنبيه بيئي**: توجد مهمة مجدولة تلتزم وتدفع كل تعديل تلقائياً على `main`. أي عمل على وحدة الطعام يجب أن يجري على فرع مستقل، وإلا وصل كود نصف مكتمل إلى `main` برسالة التزام آلية لا تصف شيئاً.
- تسجيل الأخطاء: تأكد أن `error_log` و `access_log` فعّالان لحاويات الطعام منذ اليوم الأول — الخدمة غير المسجَّلة تبدو سليمة حتى تكذب عليك في أول عطل.
---
## ١٣. المخاطر المفتوحة والقرارات التي تحتاج حسماً
1. **تنازع الأسطول** بين الرحلات والتوصيل في الذروة — مقاسة بمؤشر، والحل يؤجَّل حتى يثبت الرقم لزومه.
2. **مصدر السعر النهائي**: مثبَّت في محرك التسعير القائم؛ أي استثناء يُطلب لاحقاً يجب رفضه.
3. **هوية صاحب المطعم**: جدول `merchant_users` مستقل بدور `merchant`. القرار البديل (توسيع جدول المستخدمين الرئيسي) مرفوض لأنه يخلط نطاقات التفويض في قاعدة الرحلات.
4. **حدّ الاعتماد**: هل تُنشر المطاعم بعد اعتماد إداري يدوي؟ الافتراض في هذه الخطة: **نعم**، اعتماد يدوي إلزامي في الإطلاق الأول — والقرار قابل للمراجعة من صاحب المنتج.
5. **بند مفتوح خارج نطاق هذه الوحدة لكنه يمسّها**: مسائل تعرّض بيانات شخصية موثّقة في [08_security](../08_security) يجب ألا تتكرر في أي نقطة نهاية للطعام — خصوصاً في نقاط تُعيد عناوين وأرقاماً.
---
## ١٤. قائمة تحقق قبل أول دمج
- [ ] `docker compose config` يمرّ، والحاويتان تعملان بحدود ذاكرة معلنة
- [ ] بلوك nginx للطعام **قبل** البلوك العام، ومُتحقق منه بنداء فعلي
- [ ] nginx المضيف يخدم 4040 بـ TLS → 14040
- [ ] `food_server/composer.lock` ملتزم به في git
- [ ] لا استدعاء لـ `Database::get('main')` داخل `backend/food/`
- [ ] كل نقطة تأخذ `order_id` تملك اختبار تفويض سلبياً
- [ ] كل المبالغ أعداد صحيحة، و`client_order_uuid` فريد
- [ ] انتقالات الحالة تمر جميعها عبر `food_transition_status()` فقط
- [ ] العمل على فرع مستقل لا على `main`