28 KiB
خطة إضافة «طلبات الطعام» إلى منصة سيرو — المعمارية والدوكر والتنفيذ
الحالة: قيد التنفيذ — المراحل صفر–الرابعة (التأسيس، الكتالوج، الطلب، التوصيل، المال) مبنية على فرع
feature/food-delivery-module. انظرdocker/README.md §7لتفاصيل ما هو موجود فعلياً والتنبيهات المالية غير المؤكدة بعد (لا حجز حقيقي، عامل تحويل العملة، لا صرف آلي لأرباح السائقين). المرحلة الخامسة فقط (التقسية والإطلاق) لم تُبنَ. التاريخ: 2026-07-30 (آخر تحديث للتنفيذ: 2026-08-01) المرجع المعماري: 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. تفصيل هذا في §٦. |
ما لن نفعله (قرارات مرفوضة صراحةً):
- لن نبني تطبيق Flutter خامساً للزبون — الطعام يدخل داخل
siro_riderكتبويب. - لن ننشئ جدول مستخدمين جديداً في
siro_food؛ نخزّنpassenger_idكمرجع منطقي فقط. - لن نضع الطعام داخل حاوية
phpالحالية — لأن أي خطأ فادح في الطعام سيستهلك حوض fpm نفسه الذي يخدم الرحلات. - لن نفتح بورت السوكيت للعالم مباشرة (انظر §٤ — درس مؤلم مدفوع الثمن سابقاً).
٣. طبقة الدوكر — الشكل النهائي
٣.١ الحاويات الجديدة
تُضاف إلى نفس 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 أكواد الخصم (منفصلة عن أكواد الرحلات)
قواعد مخطط غير قابلة للتفاوض:
- تجميد الأسعار:
order_itemsيحملunit_priceوoption_price_jsonمنسوخين وقت الإنشاء. تغيير المطعم لسعره لاحقاً يجب ألا يعيد كتابة تاريخ طلب مكتمل. - المال بالأعداد الصحيحة: كل المبالغ
BIGINTبأصغر وحدة نقدية، لاFLOATمطلقاً. - مفتاح تعطيل التكرار (idempotency): عمود
client_order_uuid UNIQUEعلىorders— الضغط المزدوج على «تأكيد» أو إعادة محاولة الشبكة يجب ألا ينتج طلبين ولا خصمين. - الحالة كسلسلة محكومة:
pending → merchant_accepted → preparing → ready → courier_assigned → picked_up → deliveredوفروعcancelled_by_*وrejected. الانتقالات تُفرَض في دالة واحدةfood_transition_status()— لاUPDATE orders SET statusمبعثرة في الملفات. - الفهارس منذ اليوم الأول:
(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+ الحمولة) — التطبيق الحالي يفكّه بهذا الشكل، وأي شكل جديد سيكسر المُحلِّل المشترك. - تعدد اللغات: نصوص الحالة تُعاد كمفاتيح، والترجمة في التطبيق.
الأمن — البنود التي أُخذت من إخفاقات وحدة مواصلاتي، فلا تتكرر:
- IDOR أولاً وقبل كل شيء: كل نقطة تأخذ
order_idتتحقق أن الطلب يخص الفاعل (زبونه أو مطعمه أو سائقه). يُكتب اختبار سلبي واحد على الأقل لكل نقطة قبل الدمج. - تفويض الدور على مستوى البوابة:
connect_merchant.phpيرفض توكن الراكب حتى لو كان صالحاً. - الخوادم لا تثق بالأسعار: السعر النهائي يُحسب في الخادم من
menu_items— إجمالي يرسله العميل يُتجاهل ويُسجَّل كإشارة احتيال. - تحديد المعدل: حد أشدّ على
order/create.php(منع فيض الطلبات الوهمية) وعلىmerchant_ops/*. - رفع صور القائمة: تحقق من النوع والحجم، إعادة ترميز، أسماء عشوائية، ونشر من مسار لا ينفّذ PHP.
- بيانات شخصية: عنوان الزبون ورقمه يظهران للسائق فقط بعد
courier_assignedويُحجبان بعدdelivered.
٦. التوصيل — إعادة استخدام أسطول الرحلات
هذا أهم قرار منتجي/تقني في الخطة.
القرار: لا أسطول ثانٍ. نستخدم نفس السائقين مع علم can_deliver وحالة تفرّغ، اعتماداً على بنية geo:drivers:available القائمة في Redis.
الآلية:
- عند
ready(أو قبله بوقت التحضير التقديري)، يستدعي الباك إند بحثاً جغرافياً حول موقع المطعم — نفس نمطgeoradiusفي loction_server/find_drivers_redis.php، لكن على مفتاحgeo:couriers:availableالذي يُملأ بالسائقين ذويcan_deliver=1وليسوا في رحلة. - العرض يُرسل لسائق واحد في كل مرة بمهلة قصيرة (15–20 ثانية)، ويُسجَّل كل عرض في
courier_assignments. الصمت = رفض ضمني وانتقال للتالي. هذا يمنع «سباق القبول» الذي ينتج طلباً بسائقين. - القفل:
SET food:order:{id}:lock <courier_id> NX EX 20— القابل الأول فقط يفوز، ذرّياً. - سائق في رحلة نقل لا يظهر لعروض التوصيل والعكس — حالة السائق مصدر حقيقة واحد في Redis، لا علمان متنافسان.
- الأثر على أرباح السائق: التوصيل يدخل نفس دفتر الأرباح ونفس تلميح الأرباح المعتمد في تطبيق السائق، لا شاشة أرباح موازية.
المخاطرة الواجب مراقبتها: في ساعة الذروة تتنافس الوجبات والرحلات على الأسطول نفسه. المؤشر الحارس: نسبة الطلبات التي لم تجد سائقاً خلال 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فعّالان لحاويات الطعام منذ اليوم الأول — الخدمة غير المسجَّلة تبدو سليمة حتى تكذب عليك في أول عطل.
١٣. المخاطر المفتوحة والقرارات التي تحتاج حسماً
- تنازع الأسطول بين الرحلات والتوصيل في الذروة — مقاسة بمؤشر، والحل يؤجَّل حتى يثبت الرقم لزومه.
- مصدر السعر النهائي: مثبَّت في محرك التسعير القائم؛ أي استثناء يُطلب لاحقاً يجب رفضه.
- هوية صاحب المطعم: جدول
merchant_usersمستقل بدورmerchant. القرار البديل (توسيع جدول المستخدمين الرئيسي) مرفوض لأنه يخلط نطاقات التفويض في قاعدة الرحلات. - حدّ الاعتماد: هل تُنشر المطاعم بعد اعتماد إداري يدوي؟ الافتراض في هذه الخطة: نعم، اعتماد يدوي إلزامي في الإطلاق الأول — والقرار قابل للمراجعة من صاحب المنتج.
- بند مفتوح خارج نطاق هذه الوحدة لكنه يمسّها: مسائل تعرّض بيانات شخصية موثّقة في 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