Files
Siro/docs/10_food_orders/FOOD_ORDERS_PLAN_AR.md
T

28 KiB
Raw Blame History

خطة إضافة «طلبات الطعام» إلى منصة سيرو — المعمارية والدوكر والتنفيذ

الحالة: مقترح للتنفيذ — لم يُكتب أي كود بعد. التاريخ: 2026-07-30 المرجع المعماري: docker/docker-compose.yml و docs/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 ثانٍ — ملفّان يعنيان شبكتين وحيرة تشغيلية):

  # ‏وحدة الطعام — 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 يُضاف توجيه مسار الطعام إلى حوض fpm الخاص به:

    # كل ما تحت /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 compose run --rm socket_food composer install

٤. طبقة البيانات — قاعدة siro_food

٤.١ التسجيل في طبقة الاتصال

يُضاف مدخل واحد إلى الخريطة في backend/core/Database/Database.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، لكن على مفتاح 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 بمعامل خاص بالطعام، لا بمعادلة جديدة مكرّرة — تكرار منطق التسعير هو المصدر التاريخي لتذبذب الأسعار في هذا المشروع.
  • التسعير مثبّت لحظة عرض السلة: عرض السعر يُوقَّع ويصلح لمدة 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 وبنفس قاعدة القراءة الصادقة المعتمدة في 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 يجب ألا تتكرر في أي نقطة نهاية للطعام — خصوصاً في نقاط تُعيد عناوين وأرقاماً.

١٤. قائمة تحقق قبل أول دمج

  • 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