feat: N1d/N2/N3 + قانون فلاتر وإعادة ضبط تطبيق السائق

الباك إند:
- تعيين أول أدمن صار يُنشئ المستخدم إن لم يوجد — كان يشترط دخولاً سابقاً،
  وهي بيضة ودجاجة تمنع الدخول إلى لوحة أي مستأجر جديد أصلاً.
- OTP_DEV_MODE كان يفشل مفتوحاً (`!== 'false'`): غياب المتغيّر أو خطأ مطبعي
  يترك الإنتاج برمز ثابت يفتح كل حساب، ويحرس السحب المالي كذلك. صار
  `=== 'true'` في الإعداد وفي قارئَيه، مع تحذير عند الإقلاع.
- N2: التحقّق من اللوغو عند الرفع (PNG/JPEG · 512+ · مربّع · سقف 5MB) عبر
  قراءة الترويسة بلا اعتمادية — بدل اكتشاف أيقونة ممطوطة بعد النشر.

N3: السكربت يولّد build_config.dart بأعلام const (طبقات docs/22 §1.5)،
ويضبط bundle IDs واسم التطبيق والأيقونات/splash، و--build يشغّل Shorebird.

فلاتر:
- docs/23: قانون مُلزِم للتطبيقين (البنية · Cubit · طبقة الشبكة · الأعلام).
- السائق: أُزيل Dart القديم (GetX) مع الإبقاء على الأصيل والإضافات وشهادات
  التوقيع وShorebird وFirebase الخاص به، وأُعيد هيكلته مطابقاً للراكب.
- طبقة الشبكة ترسل x-device-id وتجدّد التوكن عند 401 مرة واحدة فقط.
- الحزم موحّدة بين التطبيقين، والـAPI https حصراً في الاثنين.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-18 13:06:46 +03:00
co-authored by Claude Fable 5
parent 3ab74a13a8
commit de4bbd00ac
271 changed files with 1333 additions and 81068 deletions
+6 -2
View File
@@ -110,8 +110,9 @@ Shorebird يرقّع Dart فقط. بتوحيد Kotlin/الصلاحيات/NDK ت
| N1a | **تعليق المستأجر يسري فعلياً** (2026-07-18). | ✅ كان `tenant.status` يُكتب ويُعرض **بلا أي فرض** — أي أن «تعطيل المستأجر» زخرفة والمستأجر غير الدافع يظل يشتغل. الفرض الآن في `JwtStrategy` (نقطة واحدة، نفس حجّة ربط الجهاز D2) + `AuthService.resolveTenant` (يخنق `sendOtp` و`verifyOtp` معاً). القراءة مكيَّشة (Redis أولاً) والإبطال يجعل التعليق فورياً؛ وتعذّر القراءة **يمرّر** لا يقطع المنصة كلها (G). |
| N1b | **النظرة الشاملة**: `GET /admin/overview?days=` — رحلات · GMV · **إيراد المنصة (العمولة)** لكل مستأجر. | ✅ استعلام واحد مجمَّع لكل المستأجرين (لا N+1). `revenue` = مجموع العمولة وهو الرقم الذي يهمّ عند التسعير، لا الـGMV. |
| N1c | **خدمة اللوحات الثلاث على الإنترنت**. | ✅ تُخدم من **نفس أصل الـAPI** عبر `useStaticAssets`: `/panel/superadmin` · `/panel/admin` · `/panel/service` — بلا CORS وبلا دومين/شهادة ثانية، وترث TLS القائم (docs/20). المجلد مربوط bind من compose فتعديل HTML يظهر بتحديث الصفحة بلا إعادة بناء. |
| N2 | **رفع اللوغو + توليد الأيقونات/splash**. | 🔵 الباك إند ✅ (`POST /admin/tenants/:id/logo` + `GET /tenant/logo/:slug` **عام لأصل الهوية فقط** — لا يخدم مجلد التخزين كاملاً حتى لا تُسرَّب صور الوثائق). توليد الأيقونات نفسه في N3. |
| N3 | **سكربت توليد التطبيق الواحد**: bundle ID · أيقونات/splash من اللوغو · FCM · **إضافات Kotlin/C++ الأصيلة** (overlay · method channels · NDK) · بناء · **Shorebird**. | 🔵 `scripts/generate-tenant-app.sh` — يجلب المانيفست ويضبط الهيكل؛ خطوات فلاتر موسومة [Q] تُوصَل عند بناء المجموعة Q (المشروع غير موجود بعد). **يجب أن يولّد `BuildConfig` بأعلام `const` حسب §1.5** — هذا ما يجعل الطبقات حقيقية لا تجميلية. |
| N1d | **تعيين أول أدمن للمستأجر**. | ✅ `POST /platform/users/role` صار **يُنشئ المستخدم إن لم يوجد**. كان يشترط دخولاً سابقاً، وهذه بيضة ودجاجة: مستأجر مزوَّد حديثاً بلا مستخدمين، فلا سبيل لصنع أول أدمن — أي أن لوحته لا يمكن الدخول إليها إطلاقاً. زر «تعيين أدمن» في لوحة السوبر-أدمن. |
| N2 | **رفع اللوغو + توليد الأيقونات/splash**. | ✅ الرفع **مع تحقّق** (`image-meta.ts`: PNG/JPEG · حدّ أدنى 512×512 · مربّع ±5٪ · سقف 5MB) — لوغو صغير أو مستطيل كان يُنتج أيقونة ممطوطة في كل تطبيقات المستأجر ولا يُكتشف إلا بعد بناءٍ ونشر. لم نُدخل `sharp`: التوليد نفسه شغل `flutter_launcher_icons` في N3، والباك إند لا يحتاج إلا التحقّق (قراءة ترويسة بلا اعتمادية). زر رفع في اللوحة. |
| N3 | **سكربت توليد التطبيق الواحد**: bundle ID · أيقونات/splash من اللوغو · FCM · **إضافات Kotlin/C++ الأصيلة** (overlay · method channels · NDK) · بناء · **Shorebird**. | 🔵 `scripts/generate-tenant-app.sh` — يجلب المانيفست ويضبط الهيكل؛ خطوات فلاتر موسومة [Q] تُوصَل عند بناء المجموعة Q (المشروع غير موجود بعد). ✅ يولّد الآن `lib/core/build_config.dart` بأعلام `const` (§1.5) · يضبط `applicationId` و`PRODUCT_BUNDLE_IDENTIFIER` و`android:label` · يجلب اللوغو ويكتب إعدادات `flutter_launcher_icons`/`flutter_native_splash` · و`--build` اختياري يشغّل Shorebird. |
| N4 | **لوحة أدمن المستأجر (ويب)**: سائقون (اعتماد) · رحلات · مراجعة وثائق · سحوبات (تحويل/فشل). | ✅ `dashboards/admin-web/index.html` + نقاط سرد إدارية (`/drivers/admin/list` · `/trips/admin/list` · `/payouts/admin/list` خلف RolesGuard admin/dispatcher). التعرفة والتقارير تُضاف مع L. |
| N5 | **لوحة خدمة العملاء (ويب)**: بحث مستخدم برقمه (فهرس أعمى) · رحلاته · تفاصيل رحلة بالمعرّف. | ✅ `dashboards/service-web/index.html` + `GET /admin/users/search` · `/trips/admin/list?rider=`. الشكاوى تحتاج وحدة complaints (لا توجد بعد — بند لاحق). |
@@ -138,6 +139,9 @@ Shorebird يرقّع Dart فقط. بتوحيد Kotlin/الصلاحيات/NDK ت
### المجموعة Q — تطبيق فلاتر (إعادة بناء كاملة) 📱
> **إعادة استعمال الأصيل الثابت** (Kotlin/iOS من التطبيق القديم Ride/Tripz)، **وإعادة بناء Dart من الصفر** بـCubit+Bloc (لا GetX).
> راجع [[tripz-mobile-store-identity]] لمعرّفات المتاجر التي يجب الحفاظ عليها.
>
> 📜 **القانون المُلزِم: [23-flutter-conventions](23-flutter-conventions.md)** — يُقرأ كاملاً قبل أي كود Dart.
> **الحالة (2026-07-18)**: هيكل التطبيقين متطابق حرفياً (`core/` + `features/auth` + `features/home`)؛ Dart القديم (GetX) أُزيل من السائق مع **الإبقاء على** الأصيل والإضافات وشهادات التوقيع وShorebird وFirebase. طبقة الشبكة تُرسل `x-device-id` وتجدّد التوكن عند 401 مرة واحدة. الحزم موحّدة بين التطبيقين والـAPI صار https حصراً (`usesCleartextTraffic=false` في الاثنين).
| # | البند |
|---|-------|
+192
View File
@@ -0,0 +1,192 @@
# 23 — قانون تطبيقات فلاتر (الراكب والسائق)
> **هذا المستند مُلزِم.** كل كود Dart في `apps/rider` و`apps/driver` يتبعه. أي انحراف يُصحَّح لا يُبرَّر.
> اقرأه كاملاً قبل كتابة أول سطر في أي جلسة جديدة — هو العقد الذي يجعل جلستين مختلفتين تُنتجان نفس البنية.
آخر تحديث: 2026-07-18. ذو صلة: [22 §1.5](22-full-product-roadmap.md) (الطبقات) · [19](19-entitlements-licensing.md) (الاستحقاقات) · [[tripz-mobile-store-identity]].
---
## 0. المبادئ الخمسة (إن تعارض شيء معها فهي تُرجَّح)
1. **التطبيقان توأمان في البنية، مختلفان في الميزات.** نفس المجلدات ونفس التسميات ونفس طبقة الشبكة. من يعرف الراكب يعرف السائق فوراً.
2. **السيرفر هو مصدر الحقيقة.** التطبيق يعرض ما يُعطى. لا منطق تسعير ولا حساب عمولة ولا قرار مطابقة في Dart — أبداً.
3. **الحالة صريحة، لا سحر.** Cubit افتراضاً، Bloc عند تعدّد الأحداث. لا GetX ولا متغيّرات عامة قابلة للتغيير.
4. **الميزة تُقفل من نقطة واحدة** (تسجيل الموديول)، لا بـ`if` مبعثرة في الشاشات.
5. **الأصيل مُعاد استعماله لا مُعاد بناؤه**، وهو **موحّد عبر كل المستأجرين** (شرط Shorebird).
---
## 1. بنية المجلدات — مُلزِمة حرفياً
```
apps/<rider|driver>/lib/
├── main.dart # نقطة الدخول: تهيئة ثم runApp — لا منطق
├── app.dart # MaterialApp + الثيم + الراوتر + المزوّدون العامّون
├── core/
│ ├── build_config.dart # ⚠️ مُولَّد — لا يُحرَّر (N3)
│ ├── config.dart # إعداد وقت التشغيل (--dart-define)
│ ├── di.dart # get_it: تسجيل التبعيات
│ ├── router.dart # go_router: كل المسارات
│ ├── theme.dart # الثيم من ألوان المستأجر
│ ├── api/
│ │ ├── api_client.dart # Dio + الاعتراضات (توكن · مستأجر · توقيع · جهاز)
│ │ └── api_exception.dart
│ └── storage/
│ └── token_store.dart # تخزين آمن للتوكنات
└── features/<feature>/
├── cubit/ # <feature>_cubit.dart + <feature>_state.dart
├── data/ # <feature>_repository.dart + models/
└── view/ # الشاشات + widgets/
```
**قواعد لا تُخالَف:**
- **الميزة لا تستورد من ميزة أخرى.** التشارك يصعد إلى `core/` أو إلى موديول مشترك. استيراد `features/trip/…` من داخل `features/auth/…` ممنوع.
- `view/` لا يستورد `data/` مباشرة — يمرّ بالـCubit دائماً.
- ملف واحد = صنف عام واحد.
- لا مجلد `utils/` عام (مقبرة الشيفرة). الدالة تعيش قرب ما تخدمه.
## 2. التسمية
| العنصر | القاعدة | مثال |
|---|---|---|
| ملفات | `snake_case.dart` | `trip_repository.dart` |
| أصناف | `PascalCase` | `TripCubit` |
| الميزة (مجلد) | مفرد، إنجليزي | `features/trip/` |
| الحالة | `<Feature>State` بحقل `status` | `TripState` |
| الاستحقاق | `Features.<camelCase>` | `Features.marketIntel` |
**النصوص المرئية للمستخدم عربية**؛ أسماء الرموز والملفات إنجليزية. لا نصّ عربي مكتوب داخل widget مباشرة — يمرّ بطبقة الترجمة.
## 3. إدارة الحالة
**Cubit افتراضاً.** Bloc فقط حين تتعدّد الأحداث الواردة على نفس الحالة (مثل شاشة الرحلة الحيّة: سوكت + موقع + إجراء المستخدم معاً).
قواعد:
- الحالة **غير قابلة للتغيير** (`copyWith`)، وتحمل `status` من `enum` — لا `bool isLoading` متناثرة.
- الـCubit **لا يعرف Flutter**: لا `BuildContext`، لا `SnackBar`، لا تنقّل. يُصدر حالة، والواجهة تتصرّف.
- الـCubit لا ينادي `Dio` مباشرة — يمرّ بـ`Repository` دائماً.
- خطأ الشبكة يتحوّل إلى رسالة عربية **داخل الـCubit**، والواجهة تعرض `state.error` كما هو.
```dart
// النمط المعتمد
enum TripStatus { idle, loading, active, error }
class TripState {
final TripStatus status;
final Trip? trip;
final String? error;
const TripState({this.status = TripStatus.idle, this.trip, this.error});
TripState copyWith({...}) => ...;
}
```
## 4. طبقة الشبكة — نقطة واحدة إجبارياً
كل نداء يمرّ بـ`core/api/api_client.dart`. لا `http.get` في أي مكان آخر.
الاعتراضات المطلوبة، بهذا الترتيب:
1. `x-tenant-id` = `BuildConfig.tenantSlug`.
2. `Authorization: Bearer` من `TokenStore`.
3. `x-device-id` — بصمة الجهاز (يفرضها السيرفر عند `AUTH_REQUIRE_DEVICE_BINDING=true`، docs/17 D2).
4. **توقيع HMAC** للنقاط المالية (docs/17 I6) — يُحسب على الجسم **كما يُرسَل** بلا إعادة تسلسل، وإلا فشل توقيع سليم.
5. تجديد التوكن عند 401 **مرة واحدة** ثم تسجيل خروج — لا حلقة تجديد لانهائية.
> ⚠️ التوقيع وربط الجهاز مطفآن على السيرفر الآن ويُفعَّلان **بعد** أن يرسلهما التطبيق. رتّب: أرسل أولاً، ثم فعّل السيرفر.
**التخزين**: التوكنات في `flutter_secure_storage` حصراً — لا `SharedPreferences` للتوكن.
## 5. أعلام الميزات (القلب — راجع docs/22 §1.5)
```dart
// ✅ الصحيح — const، يُحذف الكود من الـbinary
if (Features.marketIntel) { ... }
// ❌ ممنوع — يُبقي كود الميزة في الملف فيمكن استخراجه
if (featureMap['market_intel'] == true) { ... }
```
**التسجيل المشروط هو القاعدة**، والفحص المتناثر استثناء:
```dart
// core/di.dart — نقطة الإقفال الوحيدة
if (Features.chat) {
getIt.registerLazySingleton<ChatRepository>(() => ChatRepository(getIt()));
}
// core/router.dart
if (Features.chat) routes.add(GoRoute(path: '/chat', builder: ...));
```
**القاعدة الحاكمة**: `الفعّال = BuildConfig ∩ المانيفست الريموت ∩ استحقاقات السيرفر`.
التطبيق **لا يمنح** صلاحية — يخفي فقط. من فكّك التطبيق وفعّل كل شيء يصطدم بـ403 من `FeatureGuard`.
## 6. الخرائط
- **`flutter_map` + بلاطات انطلق** — لا `google_maps_flutter`.
- البلاطات: التطبيق → انطلق **مباشرة** (حجم كبير، بلا أسرار).
- الجيوكودنغ/المسارات/الأماكن: التطبيق → **سيرفرنا** → انطلق (يحمي المفتاح ويكيّش ويقيس الحصص).
## 7. ما يُعاد استعماله من التطبيق القديم — لا يُعاد بناؤه
| المكوّن | لماذا |
|---|---|
| `android/` · `ios/` | تحمل هوية النشر والصلاحيات والتوقيع |
| `secure_string_operations` · `trip_overlay_plugin` · `bubble-master` | إضافات أصيلة مكتوبة ومختبَرة |
| `firebase.json` · `firebase_options.dart` · `google-services.json` | مشروع FCM القائم |
| `shorebird.yaml` | **app_id منشور** — تغييره يقطع تحديثات OTA عن المستخدمين الحاليين |
| `key/` | **شهادات توقيع** — فقدانها يعني تعذّر تحديث التطبيق المنشور نهائياً |
| الخطوط (mohanad · josefin · digit) | هوية بصرية قائمة |
**معرّفات المتجر** في [[tripz-mobile-store-identity]] — تُنسخ ولا تُكتب من الذاكرة.
⚠️ معرّفا السائق متضاربان بين المنصّتين (`com.sefer_driver` مقابل `com.sefer.driver`) — **يؤكّدهما المالك قبل أي إصدار**.
## 8. الطبقة الأصيلة موحّدة
لا تُفرَّع `android/` أو `ios/` بين المستأجرين أو الطبقات. المتغيّر الوحيد المسموح آلياً: `applicationId` · `PRODUCT_BUNDLE_IDENTIFIER` · `android:label` · الأيقونات — وكلها يضبطها `scripts/generate-tenant-app.sh`.
السبب: Shorebird يرقّع Dart فقط. أصيلٌ موحّد ⇒ ترقية الباقة patch فوري. أصيلٌ متفرّع ⇒ كل ترقية إصدار متجر ومراجعة.
## 9. الحزم المعتمدة
| الغرض | الحزمة |
|---|---|
| الحالة | `flutter_bloc` |
| الحقن | `get_it` |
| الشبكة | `dio` |
| التنقّل | `go_router` |
| الخرائط | `flutter_map` + `latlong2` |
| التخزين الآمن | `flutter_secure_storage` |
| الزمن الحقيقي | `socket_io_client` |
| الإشعارات | `firebase_messaging` + `flutter_local_notifications` |
إدخال حزمة خارج هذا الجدول قرارٌ يُوثَّق هنا أولاً. **تُثبَّت أحدث الإصدارات المستقرّة**، ويُشغَّل `flutter pub outdated` قبل كل مجموعة عمل.
## 10. الفروق المشروعة بين التطبيقين
| | الراكب | السائق |
|---|---|---|
| الميزة المحورية | طلب رحلة وتتبّعها | استقبال العروض وتنفيذها |
| الموقع | عند الاستعمال | **خلفية دائمة** + متحكّم مواقع |
| الأصيل الخاص | — | overlay العرض · bubble · حيوية الوجه |
| المال | الدفع | المحفظة · الرصيد · السحب |
**متحكّم موقع السائق** (نمط سيرو، مثبت عملياً): تسجيل كل ٣ ثوانٍ · رفع دفعات كل دقيقتين · وعيٌ بالبطارية (تخفيف عند ٢٠٪) · سوكت للبثّ الحي ودفعات للحفظ الدائم. لا تُعِد اختراعه.
## 11. قواعد العمل اليومية
- **لا بناء ولا اختبار على الماك** — يُشغّلهما المالك (راجع [[no-build-or-test-on-mac]]). الوكيل يكتب فقط.
- التعليقات تشرح **لماذا** لا **ماذا**، وبالعربية.
- ملف `build_config.dart` مُولَّد: تعديله يُمحى عند أول توليد.
- كل ميزة جديدة تبدأ بسؤال: **هل لها علم استحقاق؟** إن كانت مدفوعة فالعلم أولاً، ثم الكود.
---
## 12. قائمة تحقّق قبل اعتبار ميزة «منجزة»
- [ ] الحالة عبر Cubit/Bloc، غير قابلة للتغيير، بـ`status` من enum
- [ ] الـCubit بلا `BuildContext` وبلا `Dio` مباشر
- [ ] كل النداءات عبر `ApiClient`
- [ ] الميزة المدفوعة محروسة بـ`Features.x` **وبتسجيل مشروط**
- [ ] النصوص عربية وعبر طبقة الترجمة
- [ ] لا استيراد متقاطع بين الميزات
- [ ] السيرفر يفرض ما يخفيه التطبيق (لا حماية بالواجهة وحدها)