# خطة إضافة «طلبات الطعام» إلى منصة سيرو — المعمارية والدوكر والتنفيذ > الحالة: **قيد التنفيذ** — المراحل صفر–الرابعة (التأسيس، الكتالوج، الطلب، التوصيل، المال) مبنية على فرع `feature/food-delivery-module`. انظر `docker/README.md §7` لتفاصيل ما هو موجود فعلياً والتنبيهات المالية غير المؤكدة بعد (لا حجز حقيقي، عامل تحويل العملة، لا صرف آلي لأرباح السائقين). المرحلة الخامسة فقط (التقسية والإطلاق) لم تُبنَ. > التاريخ: 2026-07-30 (آخر تحديث للتنفيذ: 2026-08-01) > المرجع المعماري: [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 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`