Files
tripz-llc/docs/30-siro-port-plan.md
T

131 lines
18 KiB
Markdown

# 30 — نقل تطبيقات سيرو (GetX) لتحل محل واجهات Tripz + دراسة دوكرة PHP سيرو
> قرار المالك 2026-07-21. الباك إند NestJS **مجمّد كما هو** — كل التكيّف يحدث في طبقة استقبال البيانات داخل Flutter.
> المنهج: توثيق المجموعات هنا أولاً، ثم تنفيذ مجموعة واحدة في كل مرة مع برهان على جهاز حقيقي.
---
## 0) القرار وتقييمه الصادق
**القرار:** نسخ `siro_rider` و`siro_driver` (GetX، مجرّبان ميدانياً 90–95%) إلى `apps/rider` و`apps/driver` ليحلا محل إعادة البناء Cubit/Bloc، مع تحويل طبقة البيانات فقط إلى عقد باك إند Tripz.
**ملاحظة صدق تقني (حتى لا نبني القرار على سبب خاطئ):** البطء الملحوظ على الجهاز عبر كابل USB سببه الغالب **وضع debug نفسه** (JIT + asserts + logging + الكابل)، وليس Bloc/Cubit كإطار — في build release الفرق بين الإطارين هامشي. القرار سليم لكن لسببه الحقيقي:
| الحجة الحقيقية | الدليل المتحقق |
|---|---|
| تطبيقات سيرو **مكتملة ومجرّبة على مستخدمين حقيقيين** | rider = 234 ملف dart، driver = 281 ملف |
| إعادة البناء ما زال أمامها ذيل UX طويل (مرحلة Q كلها) | rider الجديد = 77 ملف، driver = 53 ملف |
| المالك يعرف كود سيرو شبراً شبراً → سرعة تطوير أعلى | خبرة مباشرة |
| طبقة الشبكة في سيرو **مركزية فعلاً** → التحويل ممكن بدون مسّ الواجهات | `crud.dart` (625 سطر) يستورده 46 ملفاً في rider و71 في driver؛ الروابط كلها في `links.dart` (547 سطر) |
**ما لا يتغير:** الباك إند (A–K + ما بعدها) · قرار الخرائط المباشرة ([docs/25](25-infrastructure-topology.md) + ذاكرة maps-direct) · قرار الهويتين المنفصلتين راكب/سائق (commit `9f7bc31`) · نموذج المال (محفظتان) · مبدأ Redis-first.
---
## 1) الحقائق المتحقق منها (فُحصت من الكود 2026-07-21)
### تطبيقا سيرو
- **الحالة:** GetX + `get_storage`، شبكة `http` + `dio`، خرائط `intaleq_maps ^2.2.1` (نفس ما يستخدمه Tripz — **صفر عمل هجرة خرائط**).
- **`crud.dart`** يحوي أصلاً: فحص صلاحية JWT محلياً + تجديد تلقائي + SSL pinning + NetGuard + إبلاغ أخطاء مركزي. البنية الذهنية مطابقة لما يحتاجه عقد Tripz — التغيير في التفاصيل لا في الفلسفة.
- **`links.dart`** يوزّع النداءات على **أربع عائلات سيرفرات لكل دولة** (API رئيسي · paymentServer · location API · locationServerSide سوكيت · routesOsm). في Tripz تنهار كلها إلى **اثنين**: `tripz-api.intaleqapp.com` + `map-saas.intaleqapp.com` مباشرة.
- **سوكيت:** `socket_io_client 1.0.2` — بروتوكول Socket.IO v2 قديم.
- **دفع:** التطبيق ينادي PayMob **مباشرة من Flutter** (`controller/payment/paymob*.dart`).
- **هويات سيرو:** `com.siro.rider` / `com.siro.siro_driver` + shorebird خاصة بسيرو (`44245793…`, `f8e9c087…`) — **لا تُستخدم في Tripz**.
- **حزم محلية:** rider يحمل fork `get`/`get_storage` داخله (`./packages/`) — مكتفٍ ذاتياً. **driver يشير خارج المستودع** (`../../Intaleq/packages/get`) — يجب ضمّها داخله عند النسخ.
### باك إند Tripz (الطرف الثابت)
- 38 controller تغطي كل الدورة (auth·trips·dispatch·wallet·credit·payments·payouts·ratings·chat·notifications·documents·vehicles·rewards·geofence·transit…).
- realtime = NestJS `socket.io v4` + redis-adapter، بلا `allowEIO3`.
- **المرجع الحي لعقد النداءات: `backend/scripts/e2e-test.mjs`** — يمرّ على الدورة كاملة (دخول→سائق→رحلة→حالات→WS→دفع→عمولة→تقييم→payout) بالأشكال الفعلية للطلب والرد. هذا هو "قاموس الترجمة" عند تحويل كل وحدة.
---
## 2) فجوات العقد الست (كل واحدة مصيدة معروفة سلفاً)
1. **المغلف والترويسات:** ردود PHP بأشكالها الحرة ↔ ردود NestJS DTO. الترويسات الجديدة إلزامية: `Authorization` JWT + `x-tenant-id` (slug) + `x-app-role` (قرار الهويتين) + لاحقاً `x-device-id` وتوقيع الطلبات. المعرّفات UUID لا int، والمال عشري `num` (1.10 JOD تكسر أي `int`).
2. **انهيار عائلات السيرفرات:** كل `switch (currentCountry)` في `links.dart` يُستبدل: الباك إند واحد متعدد المستأجرين (الدولة تُحل من `tenant.countryPack` على السيرفر)، والخرائط/المسارات مباشرة لـ map-saas بترويسة `x-api-key` (العقد المثبت: reverse يرجع **مصفوفة**، route في حقل `points`، لا `lang`).
3. **السوكيت غير متوافق أصلاً:** عميل v2 لا يتصل بخادم v4 — **ترقية `socket_io_client` إلى ^2/^3 إلزامية** + إعادة رسم أسماء الأحداث + قناة رفع مواقع السائق تتحول من `driver_socket.php` (بورت 2020) إلى realtime gateway الموحّد.
4. **الدفع يعود للسيرفر:** نداءات PayMob المباشرة من Flutter تُعاد عبر `payments.controller` (بوابة + سجل محولات) — المفاتيح لا تسكن التطبيق.
5. **الهويات التجارية:** تُستبدل هويات سيرو بهويات Tripz المنشورة (rider `com.mobileapp.store.ride` / shorebird `496cb3ac…`). ⚠️ **هوية driver متعارضة بين المنصتين (`com.sefer_driver` أندرويد / `com.sefer.driver` iOS) — قرار المالك مطلوب قبل أي إصدار driver.** Firebase وshorebird والإضافات الأصلية تؤخذ من مستودع Tripz الحالي، لا من سيرو.
6. **حزم fork المحلية:** تُضم `get`/`get_storage` داخل `apps/driver/packages/` أسوة بـ rider، حتى يبقى المستودع مكتفياً ذاتياً.
---
## 3) خطة التنفيذ — وحدة وحدة (G0…G7)
**المنهج الثابت لكل مجموعة:**
(أ) التقاط نداء سيرو الحالي (link + payload + شكل الرد) → (ب) مطابقته بعقد Tripz من `e2e-test.mjs` وswagger → (ج) التعديل **في طبقة البيانات فقط** (crud/links/models) وترك الواجهات ومنطق الـ controllers كما هو ما أمكن → (د) **برهان على جهاز حقيقي** قبل الانتقال للتالية. البناء والاختبار على السيرفر لا على الماك (القاعدة الثابتة).
| # | المجموعة | المحتوى | البرهان |
|---|---|---|---|
| **G0** | الاستيراد والترجمة | أرشفة الكود الحالي بفرع `archive/cubit-rebuild` (لا حذف — فيه إصلاحات تُحمل، §4) · نسخ تطبيقي سيرو إلى `apps/` · تبديل الهويات (bundle/firebase/shorebird من Tripz) · ضم fork الحزم · ترقية `socket_io_client` · حذف Twilio وبقايا لا تلزم | `flutter analyze` صفر أخطاء + الإقلاع لشاشة الدخول على الجهاز |
| **G1** | المصادقة | قلب `crud.dart` + `links.dart` للعقد الجديد (المغلف، الترويسات الأربع، تجديد refresh token) · دخول هاتف+OTP | دخول حقيقي بـ OTP على الجهاز، والتوكن يتجدد |
| **G2** | الهوم والخريطة | map-saas مباشر (العقد المثبت §2.2) · السائقون القريبون من الباك إند | خريطة + عنوان عكسي + سيارات قريبة تظهر |
| **G3** | دورة الرحلة (راكب) | تسعيرة → طلب → مطابقة → حالات الرحلة → أحداث WS v4 | رحلة كاملة على جهازين حتى `completed` (ملاحظة: R1 sweeper ناقص على الباك — حالة البحث قد لا تنتهي من السيرفر؛ يُختبر بوجود سائق) |
| **G4** | تطبيق السائق | دخول بـ `x-app-role: driver` · online/offline · رفع المواقع إلى gateway الموحّد · استقبال العروض · حالات الرحلة | نفس رحلة G3 من طرف السائق، والموقع يظهر عند الراكب |
| **G5** | المال | محفظة الراكب · رصيد السائق والعمولة · شحن عبر الباك إند (PayMob) · payout بـ OTP | شحن تجريبي + خصم عمولة رحلة + طلب payout |
| **G6** | الإشعارات والتقييم والدردشة | FCM (⚠️ العقد الصحيح `POST /notifications/token` مع `platform` — مسار `/devices/fcm-token` كان 404 صامتاً) · التقييم الإجباري `GET /trips/rating/pending` · الدردشة | إشعار يصل والتطبيق مغلق + تقييم إجباري يظهر |
| **G7** | التصليب والتنظيف | SSL pinning على شهادة tripz-api · ثم تفعيل `PAYMENTS_REQUIRE_SIGNATURE` و`AUTH_REQUIRE_DEVICE_BINDING` على السيرفر · حذف الكود الميت (PHP-era) · مواءمة مع خطة الفئات lite/pro/max (docs/22 §1.5) | e2e يمر والفلاغان مفعّلان |
---
## 4) ما يُحمل من إعادة البناء الحالية قبل الأرشفة (لا يعاد اكتشافه)
- **عقد map-saas المثبت بالتجربة:** `x-api-key` ترويسة (query مرفوض 400) · reverse مصفوفة مرتبة بالأقرب (`name`/`name_ar`/`address`) · route في `points` — موجود في `apps/rider/lib/core/api/antlaq_api.dart` و`features/home/data/maps_repository.dart`.
- **درس FCM:** القناة كانت ميتة بسبب اختلاف المسار — العقد الصحيح موثق في G6.
- **قرار الهويتين** ومفتاح `MAP_API_KEY` عبر `--dart-define` (المفتاح الحالي مفتاح سيرو؛ مفتاح Tripz مقيّد بالبصمة يصدر لاحقاً).
---
## 5) دراسة دوكرة سيرو PHP (مسار موازٍ مستقل عن النقل)
### 5.1 الجرد (فُحص 2026-07-21 — لا يوجد أي Docker حالياً في مستودع سيرو)
`backend/` (API الرئيسي) · `payment_server/v2` · `loction_server/` (Workerman + PHPSocketIO — `driver_socket.php` بورت 2020 WS + 2021 HTTP داخلي، Redis-only مبطّن كل 500ms) · `passenger_server/` · `ride_server/` · `siro_admin/` · `siromove.com/` + MySQL + Redis.
### 5.2 الشكل المقترح — compose واحدة لكل نسخة
```
nginx (موجّه لكل الخدمات) ─┬─ php-fpm ← backend + passenger + ride + payment + admin (vhosts)
├─ php-cli ← driver_socket.php (Workerman يدير عمّاله بنفسه، restart: always)
├─ mysql (volume دائم + نسخ احتياطي)
└─ redis
.env واحدة لكل عميل: الدومين · مفاتيح OTP/دفع · كلمات سر القواعد · مفتاح map-saas
```
- استنساخ عميل جديد = `git clone` + `.env` + `docker compose up -d` + استيراد السكيمات الثلاث (`schema_primary/ride/tracking` + `WalletDB` + `locationDB`) — **هدف واقعي: نسخة عاملة في يوم، وتسليم عميل في أسبوع** (الوقت الباقي للعلامة والتطبيقات والمتاجر).
- كل عميل على سيرفره الخاص = عزل كامل بالتعريف (لا حاجة لهندسة multi-tenant في PHP).
### 5.3 السعة: هدف 10,000 رحلة في الذروة
حساب مبدئي: 10k رحلة/ساعة ≈ 2.8 رحلة/ثانية ≈ 60–120 rps على API (بمعدل 20–40 نداء للرحلة) + سوكيت المواقع (Redis-only ومبطّن أصلاً — ليس عنق الزجاجة). عنق الزجاجة المتوقع: MySQL وحوض php-fpm. سيرفر 4–8 vCPU يفترض أن يكفي.
**لكن القاعدة قاعدة: برهان لا تقدير.** مجلد `stress_test/` موجود في مستودع سيرو — يُشغَّل على نسخة docker مستنسخة نظيفة، ويُلتزم فقط بالرقم الذي يثبته الاختبار. (للمقارنة: باك إند Tripz أثبت ~1.15M رحلة/يوم على السيرفر المشترك.)
### 5.4 🔴 شرط غير قابل للتفاوض قبل أول عميل: طقم إصلاح ما قبل الاستنساخ
**استنساخ الكود = استنساخ الثغرات.** ثغرة payout في نسخة عميل = سرقة مال حقيقية باسمنا. من `docs/21-siro-audit.md` (17 عيباً)، عيوب P0 التي تُصلح مرة واحدة في الأصل قبل أي نسخة:
1. IDOR في `request_payout.php` (driverId من الطلب لا من JWT) — سحب رصيد الغير.
2. لا حجز رصيد عند طلب payout — طلبات متزامنة = صرف مزدوج.
3. تناقض رسم 3500 (يُشترط جمعاً ويُخصم طرحاً، مكتوب مرتين) — تسريب مال.
4. `finalizePayout` خمس كتابات بلا معاملة — فشل منتصفي يترك payout نصف مصفّى.
5. `driverWallet.amount` نوع `varchar(10)` — مال كنص و`SUM()` على نصوص.
### 5.5 قرارات الأداء المحسومة (سؤال المالك 2026-07-21)
- **حاويات مقابل PHP عارٍ:** على لينكس الحاوية = عمليات عادية بنفس النواة (namespaces/cgroups، لا محاكاة) — حمل CPU/ذاكرة ≈ صفر، والفرق الكلي 0–3% إذا وُضعت بيانات MySQL على volume مسمّى (يتجاوز overlayfs) وفُعّل opcache. **القرار: دوكرة، بلا تردد** — الأداء متكافئ وفائدة الاستنساخ حاسمة.
- **حاوية واحدة مقابل تقسيم:** الأداء **متطابق** (نفس العمليات بالحالتين) — الفرق تشغيلي بحت لصالح التقسيم (إعادة تشغيل خدمة وحدها، حدود موارد، لوغات منفصلة). حاوية-واحدة-فيها-كل-شيء anti-pattern. **القرار: التقسيم بالدور** — 6 حاويات: nginx / php-fpm واحدة لكل الخدمات الطلبية / socket_driver (2020) / socket_passenger (3030) / mysql / redis.
- **JS مقابل PHP:** لكل اتصال، Node أخف (event loop دائم بلا إقلاع لكل طلب) من نموذج fpm (عملية لكل طلب). لكن عند 10–20 ألف رحلة/ساعة كلاهما بعيد عن الحد — عنق الزجاجة MySQL وضبط fpm لا اللغة. ومسار سيرو الساخن (المواقع) أصلاً على Workerman = نفس نموذج Node بالضبط. مرجع: باك إند Tripz (نموذج JS) قاس ~1.15M رحلة/يوم على الصندوق المشترك؛ رقم PHP يقيسه الاختبار.
### 5.6 حالة التنفيذ (2026-07-21)
بُني كل شيء في مستودع سيرو، جاهز للتشغيل على السيرفر (الماك للكتابة فقط):
- `Siro/docker/` — compose (6 خدمات بحدود ذاكرة لصندوق 6 أنوية/12GB) + صورتا PHP (fpm + cli مع ext-event للسوكيتات) + nginx نقطة-واحدة (`/backend` + `/v2/main` بنفس أشكال مسارات اللايف — التطبيق يبدّل الدومين فقط) + قوالب env + **runbook كامل بالأوامر في `docker/README.md`**.
- `Siro/stress_test/rate_test.js` — جديد: معدل مستمر (رحلات/ساعة) بدل الدفعة، JWT داخل Node (أزال اختناق execSync للـ PHP لكل رحلة)، p50/p95/p99 لكل نقطة + زمن العرض + الرحلة كاملة، مع ramp وحكم نجاح/فشل (<1% فشل و p95<500ms).
- **قاعدة الصدق في القياس:** سيناريو الاختبار أخف من الرحلة الحقيقية 3–5× → لتبنّي "10k حقيقية" يجب عبور **30k** سيناريو. الفحص خارج الذروة، بجولات 5–10 دقائق، مرتين. المُلتزم به للعملاء = الرقم العابر فقط.
- المؤجل بقرار المالك: كل مسار الإيجار/الاستئجار على باك إند Tripz — التركيز الحالي على دوكرة PHP وتجربتها بتطبيق سيرو الحالي (تغيير base URLs فقط عبر `.env` التطبيق وإعادة بناء واحدة).
### 5.7 التموضع الاستراتيجي (حتى لا نشغّل منتجين للأبد)
- **نسخ PHP للعملاء = المنتج النقدي المرحلي**: سريع، مجرّب، سيرو نفسها هي الدعاية. **تجميد وظيفي عليه** — إصلاحات أمان فقط، لا ميزات جديدة على PHP.
- **Tripz NestJS = المنصة النهائية**: كل نسخة عميل مرشحة للهجرة لاحقاً كمستأجر (نفس فكرة "سيرو أول مستأجر"). بهذا لا نصير أسرى صيانة أسطول PHP متباعد النسخ.
- سيرو نفسها أول من يُعاد تشغيله بالـ compose (سيرفر واحد يتحمل الذروة) — فتكون هي البرهان الحي أمام العملاء.
---
## 6) أسئلة مفتوحة للمالك
1. **هوية driver المنشورة** — `com.sefer_driver` أم `com.sefer.driver`؟ يلزم قبل G0 للسائق (الراكب لا ينتظرها).
2. **أي المسارين أولاً** — النقل (G0) أم compose سيرو (5.2)؟ يمكن التوازي: النقل عمل Flutter محلي والدوكرة عمل سيرفر.
3. تسمية فرع الأرشفة `archive/cubit-rebuild` — موافق؟