From 95fea546f51d903ff2f097edfb6b37b5ec8753dd Mon Sep 17 00:00:00 2001 From: Hamza Date: Thu, 16 Jul 2026 15:22:06 +0300 Subject: [PATCH] =?UTF-8?q?first=20commit:=20=D9=85=D9=86=D8=B5=D8=A9=20Tr?= =?UTF-8?q?ipz=20=E2=80=94=20=D8=AE=D8=B7=D8=B7=20=D9=83=D8=A7=D9=85=D9=84?= =?UTF-8?q?=D8=A9=20+=20=D8=B3=D9=83=D8=A7=D9=81=D9=88=D9=84=D8=AF=20?= =?UTF-8?q?=D8=A8=D8=A7=D9=83=20=D8=A5=D9=86=D8=AF=20NestJS/Docker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/00-15: دراسة، بنية، محرك تعرفة، تسعير، نموذج استئجار، تكاملات، بيانات، realtime، خطة، devops، لاندنج، مخاطر، اصطلاحات سيرفر، تدفق نشر - backend/: NestJS 11 على Docker (health + tenants + عزل tenant_id + بادئة tripz_ + Redis DB 3) - apps/rider, apps/driver, dashboards/admin-web, dashboards/superadmin-web (هياكل) - sync-to-server.sh + .gitignore Co-Authored-By: Claude Opus 4.8 --- .gitignore | 64 +++++++++++ README.md | 59 ++++++++++ apps/driver/README.md | 3 + apps/rider/README.md | 3 + backend/.dockerignore | 8 ++ backend/.env.example | 32 ++++++ backend/Dockerfile | 21 ++++ backend/README.md | 48 +++++++++ backend/docker-compose.yml | 74 +++++++++++++ backend/nest-cli.json | 8 ++ backend/package.json | 53 +++++++++ backend/src/app.module.ts | 40 +++++++ backend/src/common/tenant/tenant.context.ts | 17 +++ backend/src/common/tenant/tenant.decorator.ts | 10 ++ .../src/common/tenant/tenant.middleware.ts | 20 ++++ backend/src/common/usage/usage.interceptor.ts | 30 ++++++ backend/src/config/configuration.ts | 41 +++++++ backend/src/config/data-source.ts | 22 ++++ .../src/database/entities/tenant.entity.ts | 50 +++++++++ .../migrations/1721145600000-InitTenants.ts | 32 ++++++ backend/src/main.ts | 30 ++++++ .../src/modules/health/health.controller.ts | 15 +++ backend/src/modules/health/health.module.ts | 5 + .../src/modules/tenants/tenants.controller.ts | 27 +++++ backend/src/modules/tenants/tenants.module.ts | 13 +++ .../src/modules/tenants/tenants.service.ts | 41 +++++++ backend/src/worker.ts | 17 +++ backend/tsconfig.build.json | 4 + backend/tsconfig.json | 23 ++++ dashboards/admin-web/README.md | 3 + dashboards/superadmin-web/README.md | 4 + docs/00-overview.md | 48 +++++++++ docs/01-architecture.md | 61 +++++++++++ docs/02-backend-plan.md | 81 ++++++++++++++ docs/03-mobile-plan.md | 64 +++++++++++ docs/04-tariff-engine.md | 69 ++++++++++++ docs/05-pricing-billing.md | 50 +++++++++ docs/06-tenant-model.md | 59 ++++++++++ docs/07-integrations.md | 59 ++++++++++ docs/08-data-model.md | 75 +++++++++++++ docs/09-realtime.md | 51 +++++++++ docs/10-roadmap.md | 58 ++++++++++ docs/11-devops-cicd.md | 57 ++++++++++ docs/12-landing-gtm.md | 39 +++++++ docs/13-risks-decisions.md | 38 +++++++ docs/14-server-conventions.md | 50 +++++++++ docs/15-deploy-flow.md | 49 +++++++++ docs/project-tree.md | 101 ++++++++++++++++++ sync-to-server.sh | 55 ++++++++++ 49 files changed, 1881 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 apps/driver/README.md create mode 100644 apps/rider/README.md create mode 100644 backend/.dockerignore create mode 100644 backend/.env.example create mode 100644 backend/Dockerfile create mode 100644 backend/README.md create mode 100644 backend/docker-compose.yml create mode 100644 backend/nest-cli.json create mode 100644 backend/package.json create mode 100644 backend/src/app.module.ts create mode 100644 backend/src/common/tenant/tenant.context.ts create mode 100644 backend/src/common/tenant/tenant.decorator.ts create mode 100644 backend/src/common/tenant/tenant.middleware.ts create mode 100644 backend/src/common/usage/usage.interceptor.ts create mode 100644 backend/src/config/configuration.ts create mode 100644 backend/src/config/data-source.ts create mode 100644 backend/src/database/entities/tenant.entity.ts create mode 100644 backend/src/database/migrations/1721145600000-InitTenants.ts create mode 100644 backend/src/main.ts create mode 100644 backend/src/modules/health/health.controller.ts create mode 100644 backend/src/modules/health/health.module.ts create mode 100644 backend/src/modules/tenants/tenants.controller.ts create mode 100644 backend/src/modules/tenants/tenants.module.ts create mode 100644 backend/src/modules/tenants/tenants.service.ts create mode 100644 backend/src/worker.ts create mode 100644 backend/tsconfig.build.json create mode 100644 backend/tsconfig.json create mode 100644 dashboards/admin-web/README.md create mode 100644 dashboards/superadmin-web/README.md create mode 100644 docs/00-overview.md create mode 100644 docs/01-architecture.md create mode 100644 docs/02-backend-plan.md create mode 100644 docs/03-mobile-plan.md create mode 100644 docs/04-tariff-engine.md create mode 100644 docs/05-pricing-billing.md create mode 100644 docs/06-tenant-model.md create mode 100644 docs/07-integrations.md create mode 100644 docs/08-data-model.md create mode 100644 docs/09-realtime.md create mode 100644 docs/10-roadmap.md create mode 100644 docs/11-devops-cicd.md create mode 100644 docs/12-landing-gtm.md create mode 100644 docs/13-risks-decisions.md create mode 100644 docs/14-server-conventions.md create mode 100644 docs/15-deploy-flow.md create mode 100644 docs/project-tree.md create mode 100755 sync-to-server.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0988fed --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +# ==== Tripz — نصوص فقط، لا ملفات كبيرة ولا أسرار ولا مخرجات بناء ==== + +# ---- Secrets / env ---- +.env +.env.* +!.env.example +*.pem +*.key +*.keystore +*.jks +**/google-services.json +**/GoogleService-Info.plist + +# ---- Node / NestJS ---- +node_modules/ +dist/ +build/ +coverage/ +*.log +npm-debug.log* +yarn-error.log* +.pnpm-store/ + +# ---- Flutter / Dart ---- +.dart_tool/ +.packages +.pub-cache/ +.pub/ +**/build/ +**/.flutter-plugins +**/.flutter-plugins-dependencies +**/ios/Pods/ +**/ios/.symlinks/ +**/android/.gradle/ +**/android/app/debug/ +**/android/app/profile/ +**/android/app/release/ +*.iml + +# ---- Docker / data volumes (تُدار على السيرفر لا في git) ---- +**/pgdata/ +**/redisdata/ +*.dump +*.sql.gz + +# ---- OS / IDE ---- +.DS_Store +Thumbs.db +.idea/ +.vscode/ +*.swp + +# ---- Big/binary assets (لا تُرفع لـ git — نصوص فقط) ---- +*.zip +*.tar +*.tar.gz +*.mbtiles +*.pbf +*.apk +*.aab +*.ipa +*.mp4 +*.mov +*.psd diff --git a/README.md b/README.md new file mode 100644 index 0000000..1af4b36 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# Tripz — منصة نقل ذكي متعددة المستأجرين (White-Label) + +> منصة SaaS لتأجير تطبيقات نقل بعلامة تجارية خاصة لكل مستأجر (مكتب تكسي / أسطول / مشغّل مرخّص) في دول متعددة. +> تُبنى لتكسر نموذج Onde عبر أربع فجوات: **الشفافية السعرية، الاستضافة داخل الدولة (السيادة)، خرائط انطلق الذاتية (بلا فاتورة خرائط)، والعربية أولاً**. + +هذا الفولدر هو **حجر الأساس التخطيطي** للمشروع قبل كتابة أي كود. كل ملف هنا يجيب عن سؤال واحد: ماذا نبني، وكيف، وبأي ترتيب. + +--- + +## القرارات المحسومة (لا نقاش فيها عند البناء) + +| الطبقة | القرار | لماذا | +|--------|--------|-------| +| الباك إند | **NestJS 11 + TypeORM + PostgreSQL/PostGIS + Redis** | باك إند انطلق مبني به أصلاً وفيه Tenant + Usage جاهزان | +| الـ Realtime | **Socket.IO Gateway (@nestjs/websockets) + redis-adapter** | لغة واحدة، فريق واحد، توسّع أفقي | +| الموبايل | **Flutter + flutter_bloc (Cubit افتراضياً، Bloc لدورة الرحلة والعروض)** | قابلية اختبار وتدقيق لمنتج يُرخّص + قرب من عقلية GetX | +| التوجيه/الحقن | **go_router + get_it** | معيار صناعي مقبول | +| الخرائط | **انطلق (Martin tiles + geocoding + routing ذاتي)** | الخندق التنافسي: بلا اعتماد على Google | +| الاستئجار | **قاعدة واحدة + tenant_id (قياسي)، ونسخة معزولة (وضع سيادة)** | أرخص تشغيلاً + منتج مضاد لثغرة Onde | + +تفاصيل كل قرار في [docs/02-backend-plan.md](docs/02-backend-plan.md) و[docs/03-mobile-plan.md](docs/03-mobile-plan.md). + +--- + +## خريطة الوثائق + +ابدأ من الأعلى للأسفل. كل ملف مستقل لكنه يشير للبقية. + +| # | الملف | يجيب عن | +|---|-------|---------| +| 00 | [نظرة عامة](docs/00-overview.md) | الرؤية، الجمهور، التمايز، ما ليس ضمن النطاق | +| 01 | [البنية](docs/01-architecture.md) | كيف تتحدث الأنظمة معاً (مخطط عام) | +| 02 | [الباك إند](docs/02-backend-plan.md) | وحدات NestJS، الطبقات، الحرّاس | +| 03 | [الموبايل](docs/03-mobile-plan.md) | هيكل فلاتر، الحالة، الشاشات، الـ flavors | +| 04 | [محرك التعرفة](docs/04-tariff-engine.md) | كيف نسعّر الرحلة (JSON قواعد) | +| 05 | [تسعير المنتج والفوترة](docs/05-pricing-billing.md) | الباقات وخوارزمية فاتورة المستأجر | +| 06 | [نموذج الاستئجار](docs/06-tenant-model.md) | tenant_id، وضع السيادة، حزم الدول | +| 07 | [التكاملات](docs/07-integrations.md) | الدفع، OTP/SMS، واجهة المنظّم، Webhooks | +| 08 | [نموذج البيانات](docs/08-data-model.md) | الجداول الأساسية والعلاقات | +| 09 | [الطبقة الحية](docs/09-realtime.md) | قنوات Socket، الحضور، الإسناد | +| 10 | [خطة التنفيذ](docs/10-roadmap.md) | P0→P3 بالأسابيع وشروط الخروج | +| 11 | [النشر والأتمتة](docs/11-devops-cicd.md) | CI، flavors، fastlane، البيئات | +| 12 | [اللاندنج والبيع](docs/12-landing-gtm.md) | الموقع، قنوات البيع بلا إعلانات | +| 13 | [المخاطر والقرارات المفتوحة](docs/13-risks-decisions.md) | ما يحتاج حسم المالك | +| 14 | [اصطلاحات السيرفر والعزل](docs/14-server-conventions.md) | **قرارات تشغيلية محسومة:** البادئة، Redis DB، سيرفر مشترك، سيرو | +| 🌳 | [شجرة المستودع](docs/project-tree.md) | البنية الكاملة للمجلدات والملفات | + +--- + +## البدء السريع (عند جهوزية القرار) + +1. اقرأ [00-overview](docs/00-overview.md) → [01-architecture](docs/01-architecture.md). +2. احسم [القرارات المفتوحة](docs/13-risks-decisions.md) (الاسم، سوق الانطلاق، دور سيرو). +3. نفّذ [P0](docs/10-roadmap.md#p0) — تأسيس الـ Monorepo. +4. تابع الشجرة المرجعية في [project-tree](docs/project-tree.md). + +--- + +**الحالة:** تخطيط · **آخر تحديث:** 2026-07-16 · **الاسم المؤقت:** Tripz diff --git a/apps/driver/README.md b/apps/driver/README.md new file mode 100644 index 0000000..0f30199 --- /dev/null +++ b/apps/driver/README.md @@ -0,0 +1,3 @@ +# Tripz — تطبيق السائق (Flutter) +كود موحّد + flavor لكل مستأجر. **OffersBloc** لتدفق العروض. راجع docs/03. +الحالة: هيكل placeholder — يُنشأ فعلياً في P0/P1. diff --git a/apps/rider/README.md b/apps/rider/README.md new file mode 100644 index 0000000..a6337a0 --- /dev/null +++ b/apps/rider/README.md @@ -0,0 +1,3 @@ +# Tripz — تطبيق الراكب (Flutter) +كود موحّد + flavor لكل مستأجر. الحالة: Cubit افتراضياً، **TripBloc** لدورة الرحلة. راجع docs/03. +الحالة: هيكل placeholder — يُنشأ فعلياً في P0/P1. diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..965e316 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,8 @@ +node_modules +dist +npm-debug.log +.env +.git +.gitignore +test +**/*.spec.ts diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..191d694 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,32 @@ +# ==== Tripz backend env (شغّل: cp .env.example .env) ==== +# ملاحظة: السيرفر مشترك — كل شيء معزول ببادئة tripz و Redis DB غير الافتراضي (راجع docs/14) + +NODE_ENV=development +API_PORT=4010 # منفذ غير شائع لتفادي التصادم على السيرفر المشترك + +# ---- PostgreSQL (قاعدة مستقلة + بادئة جداول) ---- +DB_HOST=postgres +DB_PORT=5432 +DB_NAME=tripz +DB_USER=tripz +DB_PASSWORD=change_me_strong +DB_TABLE_PREFIX=tripz_ +DB_SYNC=false # لا synchronize في الإنتاج — هجرات فقط + +# ---- Redis (DB رقم 3 غير الافتراضي 0 + بادئة مفاتيح) ---- +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_DB=3 +REDIS_KEY_PREFIX=tripz: + +# ---- BullMQ ---- +QUEUE_PREFIX=tripz_ + +# ---- Auth ---- +JWT_SECRET=change_me_jwt_secret +JWT_EXPIRES=15m +JWT_REFRESH_EXPIRES=30d + +# ---- Maps (انطلق) ---- +MAPS_TILES_URL=http://martin:3000 +MAPS_PROVIDER=antlaq diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..ef998df --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,21 @@ +# ==== Tripz backend — multi-stage Docker build ==== +# البناء والتشغيل كله داخل Docker (لا تثبيت محلي على الماك) + +# ---- builder ---- +FROM node:22-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# ---- runtime ---- +FROM node:22-alpine AS runtime +WORKDIR /app +ENV NODE_ENV=production +COPY package*.json ./ +RUN npm install --omit=dev +COPY --from=builder /app/dist ./dist +EXPOSE 4010 +# نقطة الدخول الافتراضية: الـ API. الـ worker يُشغَّل بأمر مختلف من compose. +CMD ["node", "dist/main.js"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..d77d710 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,48 @@ +# Tripz Backend — NestJS (Docker فقط) + +> **لا تثبيت محلي على الماك.** الكود يُكتب محلياً، والبناء والتشغيل والهجرات **كلها داخل Docker على السيرفر**. + +## التشغيل على السيرفر +```bash +cd backend +cp .env.example .env # ثم عدّل كلمات المرور والأسرار +docker compose up -d --build # يبني ويشغّل: postgres/postgis, redis, martin, api, worker +``` + +## الهجرات (داخل حاوية الـ api) +```bash +docker compose exec api npm run migration:run +``` + +## فحص سريع +```bash +curl http://:4010/api/health # {"status":"ok",...} +# توثيق Swagger: http://:4010/api/docs +``` + +## العزل على السيرفر المشترك (راجع docs/14) +- Postgres: قاعدة `tripz`، بادئة جداول `tripz_` (مثال: `tripz_tenants`). +- Redis: **DB رقم 3** غير الافتراضي + بادئة مفاتيح `tripz:`. +- منافذ مضيف غير قياسية (API 4010، Postgres 55432) لتفادي التصادم. +- أسماء حاويات/شبكة/فوليوم ببادئة `tripz-`. + +## البنية الحالية (P0) +``` +src/ +├── main.ts # bootstrap API (+ Swagger) +├── worker.ts # نقطة دخول BullMQ (هيكل) +├── app.module.ts # ConfigModule + TypeORM(entityPrefix) + Throttler + TenantMiddleware +├── config/ # configuration.ts + data-source.ts (هجرات) +├── common/ +│ ├── tenant/ # context (AsyncLocalStorage) + middleware + @Tenant() +│ └── usage/ # UsageInterceptor (أساس الفوترة) +├── database/ +│ ├── entities/ # tenant.entity.ts +│ └── migrations/ # InitTenants +└── modules/ + ├── health/ # GET /api/health + └── tenants/ # GET /api/tenant/config/:slug ، admin/tenants +``` + +## التالي (P1) +users · auth(OTP) · drivers · trips(★ آلة حالة) · matching(Redis GEO) · tariff · realtime(Socket.IO) · maps(وكيل انطلق). diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..b1f2e5e --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,74 @@ +# ==== Tripz stack — كل شيء معزول ببادئة tripz على السيرفر المشترك ==== +# التشغيل على السيرفر: docker compose up -d --build +# أسماء الحاويات/الشبكة/الفوليوم كلها ببادئة tripz لتفادي التصادم. + +name: tripz + +services: + postgres: + image: postgis/postgis:16-3.4 + container_name: tripz-postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${DB_NAME:-tripz} + POSTGRES_USER: ${DB_USER:-tripz} + POSTGRES_PASSWORD: ${DB_PASSWORD:-change_me_strong} + volumes: + - tripz-pgdata:/var/lib/postgresql/data + ports: + - "${DB_EXPOSE_PORT:-55432}:5432" # منفذ مضيف غير قياسي (عزل) + networks: [tripz-net] + + redis: + image: redis:7-alpine + container_name: tripz-redis + restart: unless-stopped + # ملاحظة: نعزل منطقياً بـ DB index 3 + keyPrefix، لا بنسخة redis منفصلة. + command: ["redis-server", "--save", "60", "1"] + volumes: + - tripz-redisdata:/data + networks: [tripz-net] + + martin: + image: ghcr.io/maplibre/martin:latest + container_name: tripz-martin + restart: unless-stopped + # خادم بلاطات انطلق (يُهيّأ لاحقاً بمصدر البيانات). موجود من الآن للربط. + environment: + DATABASE_URL: postgres://${DB_USER:-tripz}:${DB_PASSWORD:-change_me_strong}@postgres:5432/${DB_NAME:-tripz} + depends_on: [postgres] + networks: [tripz-net] + + api: + build: . + container_name: tripz-api + restart: unless-stopped + env_file: .env + environment: + DB_HOST: postgres + REDIS_HOST: redis + command: ["node", "dist/main.js"] + depends_on: [postgres, redis] + ports: + - "${API_PORT:-4010}:4010" + networks: [tripz-net] + + worker: + build: . + container_name: tripz-worker + restart: unless-stopped + env_file: .env + environment: + DB_HOST: postgres + REDIS_HOST: redis + command: ["node", "dist/worker.js"] + depends_on: [postgres, redis] + networks: [tripz-net] + +volumes: + tripz-pgdata: + tripz-redisdata: + +networks: + tripz-net: + name: tripz-net diff --git a/backend/nest-cli.json b/backend/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/backend/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..0604ab0 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,53 @@ +{ + "name": "tripz-backend", + "version": "0.1.0", + "description": "Tripz — multi-tenant ride-hailing platform API (NestJS)", + "private": true, + "scripts": { + "build": "nest build", + "start": "nest start", + "start:dev": "nest start --watch", + "start:prod": "node dist/main.js", + "worker": "node dist/worker.js", + "typeorm": "typeorm-ts-node-commonjs -d src/config/data-source.ts", + "migration:generate": "npm run typeorm -- migration:generate", + "migration:run": "npm run typeorm -- migration:run", + "migration:revert": "npm run typeorm -- migration:revert", + "lint": "eslint \"src/**/*.ts\" --fix", + "test": "jest" + }, + "dependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/config": "^4.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/platform-socket.io": "^11.0.0", + "@nestjs/swagger": "^11.0.0", + "@nestjs/throttler": "^6.2.0", + "@nestjs/typeorm": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "bullmq": "^5.12.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "dotenv": "^16.4.5", + "ioredis": "^5.4.1", + "pg": "^8.12.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "socket.io": "^4.7.5", + "typeorm": "^0.3.20" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.0", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.12", + "@types/node": "^22.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.0", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "typescript": "^5.5.0" + } +} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts new file mode 100644 index 0000000..3f24410 --- /dev/null +++ b/backend/src/app.module.ts @@ -0,0 +1,40 @@ +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ThrottlerModule } from '@nestjs/throttler'; +import configuration from './config/configuration'; +import { TenantMiddleware } from './common/tenant/tenant.middleware'; +import { HealthModule } from './modules/health/health.module'; +import { TenantsModule } from './modules/tenants/tenants.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ isGlobal: true, load: [configuration] }), + + TypeOrmModule.forRootAsync({ + inject: [ConfigService], + useFactory: (cfg: ConfigService) => ({ + type: 'postgres', + host: cfg.get('db.host'), + port: cfg.get('db.port'), + database: cfg.get('db.name'), + username: cfg.get('db.user'), + password: cfg.get('db.password'), + // بادئة الجداول (tripz_) لعزل السيرفر المشترك — راجع docs/14 + entityPrefix: cfg.get('db.tablePrefix'), + synchronize: cfg.get('db.synchronize'), + autoLoadEntities: true, + }), + }), + + ThrottlerModule.forRoot([{ ttl: 60000, limit: 120 }]), + + HealthModule, + TenantsModule, + ], +}) +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(TenantMiddleware).forRoutes('*'); + } +} diff --git a/backend/src/common/tenant/tenant.context.ts b/backend/src/common/tenant/tenant.context.ts new file mode 100644 index 0000000..8ccd5b1 --- /dev/null +++ b/backend/src/common/tenant/tenant.context.ts @@ -0,0 +1,17 @@ +import { AsyncLocalStorage } from 'async_hooks'; + +export interface TenantStore { + tenantId: string; + userId?: string; + role?: string; +} + +/** + * سياق المستأجر لكل طلب — يُملأ من TenantMiddleware ويُقرأ في المستودعات + * لفرض النطاق على tenant_id تلقائياً (راجع docs/06). + */ +export const tenantContext = new AsyncLocalStorage(); + +export function currentTenantId(): string | undefined { + return tenantContext.getStore()?.tenantId; +} diff --git a/backend/src/common/tenant/tenant.decorator.ts b/backend/src/common/tenant/tenant.decorator.ts new file mode 100644 index 0000000..4c88638 --- /dev/null +++ b/backend/src/common/tenant/tenant.decorator.ts @@ -0,0 +1,10 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { currentTenantId } from './tenant.context'; + +/** + * @Tenant() — يحقن معرّف المستأجر الحالي في معاملات المتحكّم. + */ +export const Tenant = createParamDecorator( + (_data: unknown, _ctx: ExecutionContext): string | undefined => + currentTenantId(), +); diff --git a/backend/src/common/tenant/tenant.middleware.ts b/backend/src/common/tenant/tenant.middleware.ts new file mode 100644 index 0000000..bfb178e --- /dev/null +++ b/backend/src/common/tenant/tenant.middleware.ts @@ -0,0 +1,20 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import { tenantContext } from './tenant.context'; + +/** + * يستخرج معرّف المستأجر من الترويسة (أو الـ subdomain لاحقاً) ويضعه في السياق + * لبقية دورة حياة الطلب. لا استعلام يمر بلا tenantId (راجع docs/06). + */ +@Injectable() +export class TenantMiddleware implements NestMiddleware { + use(req: Request, _res: Response, next: NextFunction) { + const headerTenant = + (req.headers['x-tenant-id'] as string) || + (req.headers['x-tenant'] as string) || + ''; + + // السوبر-آدمن قد لا يحمل مستأجراً محدداً — يُعالَج بحارس منفصل لاحقاً. + tenantContext.run({ tenantId: headerTenant }, () => next()); + } +} diff --git a/backend/src/common/usage/usage.interceptor.ts b/backend/src/common/usage/usage.interceptor.ts new file mode 100644 index 0000000..638dca2 --- /dev/null +++ b/backend/src/common/usage/usage.interceptor.ts @@ -0,0 +1,30 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, + Logger, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { currentTenantId } from '../tenant/tenant.context'; + +/** + * يقيس الاستخدام لكل مستأجر (أساس الفوترة الشهرية — راجع docs/05). + * حالياً يسجّل فقط؛ لاحقاً يكتب في جدول usage / يدفع لطابور BullMQ. + */ +@Injectable() +export class UsageInterceptor implements NestInterceptor { + private readonly logger = new Logger('Usage'); + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const req = context.switchToHttp().getRequest(); + const tenantId = currentTenantId() ?? 'none'; + return next.handle().pipe( + tap(() => { + // TODO(P1): عدّ الرحلات المكتملة و GMV بدل تسجيل كل طلب. + this.logger.debug(`tenant=${tenantId} ${req.method} ${req.url}`); + }), + ); + } +} diff --git a/backend/src/config/configuration.ts b/backend/src/config/configuration.ts new file mode 100644 index 0000000..cc4d895 --- /dev/null +++ b/backend/src/config/configuration.ts @@ -0,0 +1,41 @@ +/** + * إعداد مركزي يقرأ متغيرات البيئة مع افتراضات العزل (راجع docs/14). + */ +export default () => ({ + env: process.env.NODE_ENV ?? 'development', + apiPort: parseInt(process.env.API_PORT ?? '4010', 10), + + db: { + host: process.env.DB_HOST ?? 'postgres', + port: parseInt(process.env.DB_PORT ?? '5432', 10), + name: process.env.DB_NAME ?? 'tripz', + user: process.env.DB_USER ?? 'tripz', + password: process.env.DB_PASSWORD ?? 'change_me_strong', + // بادئة الجداول لعزل Tripz عن باقي البرامج على نفس السيرفر + tablePrefix: process.env.DB_TABLE_PREFIX ?? 'tripz_', + synchronize: process.env.DB_SYNC === 'true', + }, + + redis: { + host: process.env.REDIS_HOST ?? 'redis', + port: parseInt(process.env.REDIS_PORT ?? '6379', 10), + // DB رقم غير الافتراضي 0 لعزل Tripz عن باقي البرامج + db: parseInt(process.env.REDIS_DB ?? '3', 10), + keyPrefix: process.env.REDIS_KEY_PREFIX ?? 'tripz:', + }, + + queue: { + prefix: process.env.QUEUE_PREFIX ?? 'tripz_', + }, + + jwt: { + secret: process.env.JWT_SECRET ?? 'change_me_jwt_secret', + expires: process.env.JWT_EXPIRES ?? '15m', + refreshExpires: process.env.JWT_REFRESH_EXPIRES ?? '30d', + }, + + maps: { + tilesUrl: process.env.MAPS_TILES_URL ?? 'http://martin:3000', + provider: process.env.MAPS_PROVIDER ?? 'antlaq', + }, +}); diff --git a/backend/src/config/data-source.ts b/backend/src/config/data-source.ts new file mode 100644 index 0000000..ffad4d0 --- /dev/null +++ b/backend/src/config/data-source.ts @@ -0,0 +1,22 @@ +import 'reflect-metadata'; +import { DataSource } from 'typeorm'; +import { config as loadEnv } from 'dotenv'; + +loadEnv(); + +/** + * مصدر بيانات TypeORM — يُستخدم للهجرات و runtime. + * entityPrefix يضمن أن كل جداول Tripz تبدأ بـ tripz_ لعزلها على السيرفر المشترك. + */ +export const AppDataSource = new DataSource({ + type: 'postgres', + host: process.env.DB_HOST ?? 'postgres', + port: parseInt(process.env.DB_PORT ?? '5432', 10), + database: process.env.DB_NAME ?? 'tripz', + username: process.env.DB_USER ?? 'tripz', + password: process.env.DB_PASSWORD ?? 'change_me_strong', + entityPrefix: process.env.DB_TABLE_PREFIX ?? 'tripz_', + synchronize: false, + entities: [__dirname + '/../database/entities/*.entity.{ts,js}'], + migrations: [__dirname + '/../database/migrations/*.{ts,js}'], +}); diff --git a/backend/src/database/entities/tenant.entity.ts b/backend/src/database/entities/tenant.entity.ts new file mode 100644 index 0000000..45fcdbc --- /dev/null +++ b/backend/src/database/entities/tenant.entity.ts @@ -0,0 +1,50 @@ +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +export type TenantMode = 'shared' | 'sovereign'; +export type TenantPlan = 'launch' | 'brand' | 'fleet' | 'sovereign'; + +/** + * المستأجر = مكتب تكسي / أسطول / مشغّل. الجدول الفعلي: tripz_tenants + * (البادئة من entityPrefix). راجع docs/06 و docs/08. + */ +@Entity('tenants') +export class Tenant { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + name: string; + + @Column({ unique: true }) + slug: string; + + @Column({ name: 'country_pack', default: 'jo' }) + countryPack: string; + + @Column({ type: 'varchar', default: 'launch' }) + plan: TenantPlan; + + @Column({ type: 'varchar', default: 'shared' }) + mode: TenantMode; + + @Column({ type: 'jsonb', default: {} }) + branding: Record; + + @Column({ type: 'jsonb', default: {} }) + features: Record; + + @Column({ default: 'active' }) + status: string; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +} diff --git a/backend/src/database/migrations/1721145600000-InitTenants.ts b/backend/src/database/migrations/1721145600000-InitTenants.ts new file mode 100644 index 0000000..015b790 --- /dev/null +++ b/backend/src/database/migrations/1721145600000-InitTenants.ts @@ -0,0 +1,32 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * أول هجرة: جدول المستأجرين + تفعيل PostGIS. + * اسم الجدول الفعلي tripz_tenants (البادئة تُضاف عبر entityPrefix، لكن الهجرة + * تكتب الاسم صراحةً لأنها SQL خام). + */ +export class InitTenants1721145600000 implements MigrationInterface { + public async up(q: QueryRunner): Promise { + await q.query(`CREATE EXTENSION IF NOT EXISTS postgis`); + await q.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`); + await q.query(` + CREATE TABLE IF NOT EXISTS tripz_tenants ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + name varchar NOT NULL, + slug varchar NOT NULL UNIQUE, + country_pack varchar NOT NULL DEFAULT 'jo', + plan varchar NOT NULL DEFAULT 'launch', + mode varchar NOT NULL DEFAULT 'shared', + branding jsonb NOT NULL DEFAULT '{}', + features jsonb NOT NULL DEFAULT '{}', + status varchar NOT NULL DEFAULT 'active', + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() + ) + `); + } + + public async down(q: QueryRunner): Promise { + await q.query(`DROP TABLE IF EXISTS tripz_tenants`); + } +} diff --git a/backend/src/main.ts b/backend/src/main.ts new file mode 100644 index 0000000..9f77f29 --- /dev/null +++ b/backend/src/main.ts @@ -0,0 +1,30 @@ +import 'reflect-metadata'; +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + const cfg = app.get(ConfigService); + + app.setGlobalPrefix('api'); + app.useGlobalPipes( + new ValidationPipe({ whitelist: true, transform: true }), + ); + app.enableCors(); + + const swagger = new DocumentBuilder() + .setTitle('Tripz API') + .setDescription('منصة نقل ذكي متعددة المستأجرين') + .setVersion('0.1.0') + .addBearerAuth() + .build(); + SwaggerModule.setup('api/docs', app, SwaggerModule.createDocument(app, swagger)); + + const port = cfg.get('apiPort') ?? 4010; + await app.listen(port, '0.0.0.0'); + Logger.log(`Tripz API on :${port} (docs at /api/docs)`, 'Bootstrap'); +} +bootstrap(); diff --git a/backend/src/modules/health/health.controller.ts b/backend/src/modules/health/health.controller.ts new file mode 100644 index 0000000..3e284d1 --- /dev/null +++ b/backend/src/modules/health/health.controller.ts @@ -0,0 +1,15 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; + +@ApiTags('health') +@Controller('health') +export class HealthController { + @Get() + check() { + return { + status: 'ok', + service: 'tripz-api', + time: new Date().toISOString(), + }; + } +} diff --git a/backend/src/modules/health/health.module.ts b/backend/src/modules/health/health.module.ts new file mode 100644 index 0000000..fa9d30b --- /dev/null +++ b/backend/src/modules/health/health.module.ts @@ -0,0 +1,5 @@ +import { Module } from '@nestjs/common'; +import { HealthController } from './health.controller'; + +@Module({ controllers: [HealthController] }) +export class HealthModule {} diff --git a/backend/src/modules/tenants/tenants.controller.ts b/backend/src/modules/tenants/tenants.controller.ts new file mode 100644 index 0000000..e4c219e --- /dev/null +++ b/backend/src/modules/tenants/tenants.controller.ts @@ -0,0 +1,27 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { TenantsService } from './tenants.service'; +import { Tenant } from '../../database/entities/tenant.entity'; + +@ApiTags('tenants') +@Controller() +export class TenantsController { + constructor(private readonly tenants: TenantsService) {} + + // للتطبيق: إعداد المستأجر الحالي عند الإقلاع. + @Get('tenant/config/:slug') + config(@Param('slug') slug: string) { + return this.tenants.config(slug); + } + + // للسوبر-آدمن: إدارة كل المستأجرين (يُحمى بحارس دور لاحقاً). + @Get('admin/tenants') + list() { + return this.tenants.findAll(); + } + + @Post('admin/tenants') + create(@Body() body: Partial) { + return this.tenants.create(body); + } +} diff --git a/backend/src/modules/tenants/tenants.module.ts b/backend/src/modules/tenants/tenants.module.ts new file mode 100644 index 0000000..ed2f6e8 --- /dev/null +++ b/backend/src/modules/tenants/tenants.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Tenant } from '../../database/entities/tenant.entity'; +import { TenantsService } from './tenants.service'; +import { TenantsController } from './tenants.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Tenant])], + controllers: [TenantsController], + providers: [TenantsService], + exports: [TenantsService], +}) +export class TenantsModule {} diff --git a/backend/src/modules/tenants/tenants.service.ts b/backend/src/modules/tenants/tenants.service.ts new file mode 100644 index 0000000..6af73a6 --- /dev/null +++ b/backend/src/modules/tenants/tenants.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Tenant } from '../../database/entities/tenant.entity'; + +@Injectable() +export class TenantsService { + constructor( + @InjectRepository(Tenant) + private readonly repo: Repository, + ) {} + + findAll(): Promise { + return this.repo.find(); + } + + findBySlug(slug: string): Promise { + return this.repo.findOne({ where: { slug } }); + } + + create(data: Partial): Promise { + return this.repo.save(this.repo.create(data)); + } + + /** + * الإعداد الديناميكي الذي يجلبه تطبيق فلاتر عند الإقلاع (GET /tenant/config). + * كل ما يمكن جعله ديناميكياً (نصوص، ميزات، ألوان، دفع) يأتي من هنا — راجع docs/06. + */ + async config(slug: string) { + const t = await this.findBySlug(slug); + if (!t) return null; + return { + slug: t.slug, + name: t.name, + countryPack: t.countryPack, + plan: t.plan, + branding: t.branding, + features: t.features, + }; + } +} diff --git a/backend/src/worker.ts b/backend/src/worker.ts new file mode 100644 index 0000000..3c68259 --- /dev/null +++ b/backend/src/worker.ts @@ -0,0 +1,17 @@ +import 'reflect-metadata'; +import { Logger } from '@nestjs/common'; + +/** + * نقطة دخول الـ worker (BullMQ) — مهام غير متزامنة: انتهاء صلاحية العروض، + * الإشعارات، التسويات، تجميع usage للفوترة (راجع docs/09). + * حالياً هيكل فقط؛ المعالِجات تُضاف في P1. + */ +async function bootstrap() { + const log = new Logger('Worker'); + const prefix = process.env.QUEUE_PREFIX ?? 'tripz_'; + const redisDb = process.env.REDIS_DB ?? '3'; + log.log(`Tripz worker up. queuePrefix=${prefix} redisDb=${redisDb}`); + // TODO(P1): سجّل معالِجات BullMQ هنا. + setInterval(() => void 0, 1 << 30); +} +bootstrap(); diff --git a/backend/tsconfig.build.json b/backend/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/backend/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..234f702 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "noFallthroughCasesInSwitch": false, + "esModuleInterop": true, + "resolveJsonModule": true + } +} diff --git a/dashboards/admin-web/README.md b/dashboards/admin-web/README.md new file mode 100644 index 0000000..dbdb345 --- /dev/null +++ b/dashboards/admin-web/README.md @@ -0,0 +1,3 @@ +# Tripz — لوحة المستأجر (My Hub) +لوحة إدارة **لكل مستأجر**: سائقوه، مشغّلوه، تعرفته، تقاريره، فاتورته. راجع docs/02 (admin) و docs/05. +الحالة: placeholder — تُبنى في P1/P2. diff --git a/dashboards/superadmin-web/README.md b/dashboards/superadmin-web/README.md new file mode 100644 index 0000000..10e9eda --- /dev/null +++ b/dashboards/superadmin-web/README.md @@ -0,0 +1,4 @@ +# Tripz — لوحة السوبر-آدمن (مالك المنصة) +لوحة **مالك المنصة** فوق كل المستأجرين: إنشاء/تعطيل مستأجر، متابعة كل التطبيقات، +GMV والفواتير عبر المنصة، حالة السيرفرات، إطلاق نكهات جديدة. مختلفة عن admin-web (لكل مستأجر). +الحالة: placeholder — تُبنى بعد استقرار الباك إند. diff --git a/docs/00-overview.md b/docs/00-overview.md new file mode 100644 index 0000000..d879251 --- /dev/null +++ b/docs/00-overview.md @@ -0,0 +1,48 @@ +# 00 — نظرة عامة (الرؤية والنطاق) + +## الرؤية بجملة واحدة +منصة نقل ذكي تُؤجَّر كـ **White-Label** لمشغّلي التكسي والأساطيل المرخّصة في دول متعددة، ببيانات تبقى داخل الدولة، وتعرفة شفافة، وخرائط ذاتية بلا فاتورة، وعربية أولاً. + +## من نخدم (الجمهور) +- **المستأجر (Tenant):** مكتب تكسي / أسطول / مشغّل مرخّص يريد تطبيقاً بعلامته دون بناء فريق تقني. +- **مستخدموه:** الراكب، السائق، المشغّل (Dispatcher)، صاحب الشركة (Admin). +- **نحن:** مزوّد التقنية — لسنا مشغّل نقل ولا مسؤولين أمام هيئات النقل؛ المستأجر هو المرخّص. + +## الواجهات الأربع (المخرجات البرمجية) +1. **تطبيق الراكب** (Flutter) — `apps/rider`. +2. **تطبيق السائق** (Flutter) — `apps/driver`. +3. **لوحة المستأجر / My Hub** (Web) — `dashboards/admin-web` — إدارة **مستأجر واحد**. +4. **لوحة السوبر-آدمن** (Web) — `dashboards/superadmin-web` — **مالك المنصة** يدير كل المستأجرين والتطبيقات وينشئها ويتابعها. + +> فرق حاسم: `admin-web` لكل مستأجر (بياناته فقط)، و`superadmin-web` فوق الجميع (منظور المنصة). + +## التمايز الأربعة (كل عرض بيعي يُبنى على واحد منها) +1. **الشفافية السعرية** — النسبة معلنة ومكتوبة، حد أدنى $0.02/رحلة (Onde: سرية + $0.10). +2. **السيادة** — استضافة داخل بلد المستأجر أو على خوادمه (وضع Enterprise). +3. **خرائط انطلق الذاتية** — بلا فاتورة Google Maps؛ سطر بيع: «خرائط غير محدودة مشمولة». +4. **العربية أولاً** — RTL حقيقي، دعم واتساب، محتوى ووثائق عربية. + +## ما **داخل** النطاق (P1–P2 — الجوهر الضيق اللامع) +- دورة الرحلة الكاملة: طلب (فوري/مسبق) ← إسناد أقرب سائق ← تتبع حي ← دفع كاش ← تقييم. +- تطبيق السائق: تسجيل بالوثائق، عروض الطلبات، الأرباح. +- محرك تعرفة (4 أوضاع + نوافذ زمنية + surge بسقف + مناطق ثابتة). +- Dispatch هاتفي للمشغّل. +- لوحة إدارة (My Hub) للمستأجر. +- OTP/SMS ذاتي، دردشة رحلة، تقييم. + +## ما **خارج** النطاق مبدئياً (P3 — يُباع كترقيات مدفوعة) +- المحفظة (Wallet) المتقدمة، Super App (التوصيل)، حجز الويب، Gamification، BI متقدم، حسابات الشركات المتقدمة. +- **قاعدة صارمة:** أي ميزة خارج P1/P2 لا تُبنى قبل أول عميل يدفع مقابلها. + +## مبادئ التصميم الحاكمة +- **منتج موحّد لا مشاريع مخصصة:** كود واحد لكل المستأجرين؛ التخصيص = إعداد + هوية بصرية، لا كود جديد. +- **مستأجر جديد في يوم عمل واحد:** الهدف الصلب لكل قرار معماري ونشري. +- **كل ما يمكن جعله ديناميكياً يأتي من API** (نصوص، ميزات، دفع، تعرفة) — لا يتطلب إصدار متجر. +- **الامتثال ميزة تُباع لا عبء:** واجهة المنظّم الحكومي مُصمَّمة منذ البداية. + +## المرجع التنافسي (لماذا نبني هذا) +- **Onde:** يخدم 200+ شركة، 9M طلب/شهر. نموذج: إعداد $4.5k–23k + $99–259/شهر + نسبة سرية (حد أدنى $0.10/طلب). نقاط ضعفه = فرصتنا (قفل تعاقدي، حد أدنى مرتفع، لا استضافة محلية، تعريب ضعيف). +- **TaxiF (الأردن) و يلا غو (سوريا):** كلاهما على Onde (مؤكَّد بفحص APK). كسر Onde يكسرهما معاً. +- **TaxiCaller:** نموذج «لكل مركبة» ($20–28/مركبة) — لا يكبر مع GMV، ولا تعريب. + +← التالي: [01-architecture](01-architecture.md) diff --git a/docs/01-architecture.md b/docs/01-architecture.md new file mode 100644 index 0000000..8f2741a --- /dev/null +++ b/docs/01-architecture.md @@ -0,0 +1,61 @@ +# 01 — البنية العامة + +## المخطط العلوي + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ عملاء التطبيق (Flutter) │ +│ راكب سائق مشغّل (Dispatch) إدارة (Web/Admin) │ +└──────┬──────────┬──────────────┬────────────────────┬────────────┘ + │ REST │ REST + WS │ REST + WS │ REST + ▼ ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ API Gateway / NestJS │ +│ Guards (Auth, Tenant, Roles) → Interceptors (Usage, Logging) │ +│ ┌──────────┬──────────┬──────────┬──────────┬──────────────┐ │ +│ │ auth │ trips │ tariff │ dispatch │ billing │ │ +│ │ tenants │ drivers │ payments │ maps │ notifications│ │ +│ └──────────┴──────────┴──────────┴──────────┴──────────────┘ │ +│ WebSocket Gateway (Socket.IO) │ +└──────┬─────────────────┬────────────────┬───────────────┬────────┘ + ▼ ▼ ▼ ▼ +┌────────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────────┐ +│ PostgreSQL │ │ Redis │ │ انطلق │ │ BullMQ │ +│ + PostGIS │ │ presence/ │ │ Maps: │ │ (jobs queue) │ +│ tenant_id │ │ matching/ │ │ Martin/ │ │ إشعارات/تقارير│ +│ everywhere │ │ pub-sub │ │ geocode/ │ │ /تسويات │ +└────────────┘ └──────────────┘ │ routing │ └──────────────┘ + └──────────┘ + ▲ + │ Adapters (country pack يفعّلها) +┌──────┴───────────────────────────────────────────────────────────┐ +│ الدفع (كاش/CliQ/زين/سيرياتيل/MTN/Binance) · SMS/OTP · واجهة المنظّم │ +└──────────────────────────────────────────────────────────────────┘ +``` + +## الطبقات (من الخارج للداخل) +1. **العملاء (Flutter):** 4 واجهات من كود موحّد + flavor لكل مستأجر. تفاصيل: [03-mobile-plan](03-mobile-plan.md). +2. **API Gateway (NestJS):** كل طلب يمر بـ Guards ثم Interceptors. تفاصيل: [02-backend-plan](02-backend-plan.md). +3. **الطبقة الحية (Socket.IO):** موقع السائق، العروض، التتبع، لوحة dispatch. تفاصيل: [09-realtime](09-realtime.md). +4. **التخزين:** PostgreSQL/PostGIS (مصدر الحقيقة) + Redis (حالة لحظية) + BullMQ (مهام غير متزامنة). +5. **الخرائط:** انطلق ذاتي (بلاطات/ترميز/توجيه). طبقة تجريد تسمح بتبديل المزوّد. +6. **التكاملات:** محوّلات (Adapters) يفعّلها الـ country pack. تفاصيل: [07-integrations](07-integrations.md). + +## تدفق نموذجي — «طلب رحلة» +``` +1. الراكب يطلب → POST /trips (Guard: Auth+Tenant → Interceptor: Usage++) +2. الباك إند → tariff.quote() يحسب السعر المقفول +3. الباك إند → matching (Redis GEO) يجد أقرب سائقين مؤهلين +4. Socket → يبث العرض للسائقين المرشحين (Bloc عرض السائق) +5. سائق يقبل → trip.assign() → Postgres + Socket للراكب (Bloc دورة الرحلة) +6. تتبع حي → Socket كل 3–5 ثوانٍ (موقع السائق مجمّع) +7. إنهاء → tariff.finalize() → دفع كاش → تقييم +8. BullMQ → تسوية GMV + قياس usage للفوترة الشهرية +``` + +## حدود المسؤولية +- **مصدر الحقيقة الدائم:** PostgreSQL فقط. Redis حالة عابرة قابلة لإعادة البناء. +- **قياس الاستخدام (Usage):** Interceptor يسجّل كل رحلة مكتملة → يغذّي [الفوترة](05-pricing-billing.md). +- **العزل بين المستأجرين:** `tenant_id` إلزامي في كل استعلام عبر نطاق على مستوى الـ Repository. تفاصيل: [06-tenant-model](06-tenant-model.md). + +← السابق: [00-overview](00-overview.md) · التالي: [02-backend-plan](02-backend-plan.md) diff --git a/docs/02-backend-plan.md b/docs/02-backend-plan.md new file mode 100644 index 0000000..aa73ab1 --- /dev/null +++ b/docs/02-backend-plan.md @@ -0,0 +1,81 @@ +# 02 — خطة الباك إند (NestJS) + +## الحزمة التقنية +- **NestJS 11** + TypeScript + **TypeORM** + **PostgreSQL 16 / PostGIS**. +- **Redis** (حضور، مطابقة GEO، pub/sub) + **BullMQ** (مهام). +- **Socket.IO** عبر `@nestjs/websockets` + `socket.io-redis-adapter`. +- **Swagger** للتوثيق، **class-validator** للتحقق، **Throttler** لتحديد المعدل. +- يُعاد استخدام نواة **Tenant + Usage interceptor** من باك إند انطلق كمكتبة مشتركة. + +## نمط الطلب (كل request يمر بهذه السلسلة) +``` +Request + → Guard: JwtAuthGuard (من أنت؟) + → Guard: TenantGuard (أي مستأجر؟ يحقن tenantId في السياق) + → Guard: RolesGuard (هل يُسمح لدورك؟) + → Pipe: ValidationPipe (DTO صالح؟) + → Controller → Service (منطق العمل) + → Interceptor: UsageInterceptor (سجّل الاستخدام للفوترة) + → Interceptor: LoggingInterceptor +Response +``` + +## الوحدات (Modules) + +| الوحدة | المسؤولية | نقاط نهاية أساسية | +|--------|-----------|-------------------| +| `auth` | تسجيل/دخول/OTP/JWT/refresh | `POST /auth/otp`, `/auth/verify`, `/auth/refresh` | +| `tenants` | إدارة المستأجرين، الإعداد الديناميكي، country pack | `GET /tenant/config` | +| `users` | الراكب/السائق/المشغّل/الأدمن، الأدوار | `GET /me`, `PATCH /me` | +| `drivers` | تسجيل بالوثائق، حالة الاتصال، الأرباح | `POST /drivers/apply`, `GET /drivers/earnings` | +| `trips` | دورة الرحلة الكاملة، الحالات، السجل | `POST /trips`, `PATCH /trips/:id/status` | +| `matching` | إيجاد أقرب سائق مؤهل (Redis GEO) | داخلي (يستدعيه trips) | +| `tariff` | حساب/تثبيت/إنهاء السعر — [04](04-tariff-engine.md) | `POST /tariff/quote` | +| `dispatch` | لوحة المشغّل، طلبات الهاتف، التوزيع اليدوي | `POST /dispatch/orders` | +| `maps` | وكيل انطلق (بلاطات/ترميز/توجيه/snapping) | `GET /maps/geocode`, `/maps/route` | +| `payments` | محوّلات الدفع، التسويات — [07](07-integrations.md) | `POST /payments/charge` | +| `billing` | فوترة المستأجر الشهرية، GMV، الباقات — [05](05-pricing-billing.md) | `GET /billing/invoices` | +| `pricing-zones` | مناطق سعر ثابت (PostGIS polygons) | `GET /zones` | +| `notifications` | FCM، SMS، قوالب | داخلي + `POST /notify/test` | +| `regulator` | تصدير/بث للمنظّم الحكومي — [07](07-integrations.md) | `GET /regulator/export` | +| `webhooks` | أحداث صادرة للمستأجرين المتقدمين | إدارة الاشتراكات | +| `admin` | تجميعات My Hub، التقارير، الحملات | `GET /admin/reports` | +| `realtime` | WebSocket Gateway — [09](09-realtime.md) | WS namespaces | + +## آلة حالة الرحلة (Trip State Machine) +مصدر الحقيقة على الباك إند، ويطابقها الـ Bloc على الموبايل: +``` +searching → assigned → driver_arriving → driver_arrived + → in_progress → completed → paid + (فروع: cancelled_by_rider | cancelled_by_driver | no_drivers | expired) +``` +- كل انتقال يُسجّل في `trip_events` (سجل تشخيصي + مطلب المنظّم). +- الانتقالات تأتي من: المستخدم (API)، الـ socket (السائق)، المؤقتات (BullMQ). + +## قواعد الهندسة +- **لا استعلام بلا `tenant_id`:** يُفرض بـ Repository scoped أو subscriber على مستوى TypeORM. راجع [06](06-tenant-model.md). +- **DTO لكل مدخل/مخرج** — لا كائنات خام تعبر الحدود. +- **الخدمات نقية قابلة للاختبار:** المنطق في Services، الـ Controllers رفيعة. +- **الهجرات (migrations) فقط** — لا `synchronize: true` في الإنتاج. +- **الأسرار من متغيرات البيئة/Vault** — لا مفاتيح في الكود. + +## هيكل الكود (backend/) +``` +src/ +├── main.ts +├── app.module.ts +├── common/ # guards, interceptors, decorators, filters, pipes +│ ├── tenant/ # TenantGuard, tenant-scoped repository, decorator +│ └── usage/ # UsageInterceptor (مستعاد من انطلق) +├── config/ # ConfigModule, country-packs loader +├── modules/ +│ ├── auth/ tenants/ users/ drivers/ trips/ matching/ +│ ├── tariff/ dispatch/ maps/ payments/ billing/ +│ ├── pricing-zones/ notifications/ regulator/ webhooks/ admin/ +├── realtime/ # WebSocket gateway + adapters +├── jobs/ # BullMQ processors +├── database/ # entities, migrations, seeds +└── integrations/ # payment adapters, sms adapters, regulator adapters +``` + +← السابق: [01-architecture](01-architecture.md) · التالي: [03-mobile-plan](03-mobile-plan.md) diff --git a/docs/03-mobile-plan.md b/docs/03-mobile-plan.md new file mode 100644 index 0000000..bba3431 --- /dev/null +++ b/docs/03-mobile-plan.md @@ -0,0 +1,64 @@ +# 03 — خطة تطبيقات الموبايل (Flutter) + +## الحزمة التقنية +- **Flutter** (أحدث مستقر) + **Dart 3**. +- إدارة الحالة: **flutter_bloc** — **Cubit** افتراضياً، **Bloc كامل** لآلتَي حالة فقط. +- التوجيه: **go_router** · الحقن: **get_it** (+ injectable اختياري). +- الشبكة: **dio** + interceptors (tenant header, auth, retry). +- الخرائط: عميل انطلق (بلاطات vector عبر `maplibre_gl` أو ما يعادله فوق Martin). +- الترجمة: **flutter_localizations** + ARB، RTL أول درجة. + +## لماذا Cubit افتراضياً؟ +الانتقال من GetX Controller شبه مباشر: `Controller` → `Cubit`، `update()` → `emit(state)`. نكسب قابلية الاختبار (`bloc_test`) والقبول الصناعي لمنتج يُرخّص ويُدقّق، دون كلفة أحداث Bloc الكاملة في الشاشات الاعتيادية. + +## أين نستخدم Bloc الكامل (حصراً) +| آلة الحالة | لماذا Bloc | +|-----------|-----------| +| **دورة حياة الرحلة (راكب)** | سلسلة انتقالات مسماة تأتيها أحداث من 3 جهات (المستخدم، socket، مؤقتات) — الأحداث الصريحة تمنع أخطاء التزامن وتعطي سجل انتقالات للتشخيص | +| **تدفق العروض/الطلبات (سائق)** | نفس السبب: عروض تصل وتنتهي صلاحيتها بتزامن حسّاس | + +## بنية الحزمة (packages / flavors) +كود موحّد + نكهة لكل مستأجر: +``` +mobile/ +├── packages/ +│ └── tripz_core/ # مشترك: نماذج، شبكة، تعرفة، خرائط، ثيم، l10n +├── apps/ +│ ├── rider/ # تطبيق الراكب +│ └── driver/ # تطبيق السائق +└── flavors/ # إعداد لكل مستأجر (bundle id, ألوان, أيقونة, مفاتيح) +``` +- **ما يتغير بالبناء فقط:** bundle id، الأيقونة، اسم التطبيق، مفاتيح FCM → في الـ flavor. +- **ما يمكن جعله ديناميكياً:** النصوص، الميزات المفعّلة، طرق الدفع، التعرفة، الألوان → يُجلب من `GET /tenant/config` عند الإقلاع. +- **درء رفض آبل (بند 4.3):** كل نكهة بأصول ومحتوى متجر مميّز؛ خيار «التطبيق الجامع» (راكب واحد يضم مشغّلين) للباقة المجانية. + +## طبقات كل تطبيق +``` +lib/ +├── main_.dart # نقطة دخول لكل نكهة +├── app.dart # MaterialApp.router + ثيم + l10n +├── core/ # DI, router, dio client, config bootstrap +├── data/ # repositories, data sources, models (DTO↔domain) +├── domain/ # entities, use cases (نقية) +└── features/ + ├── auth/ # cubit + screens + ├── home/ # الخريطة + طلب رحلة + ├── trip/ # ★ TripBloc (Bloc كامل) + شاشات دورة الرحلة + ├── offers/ (سائق) # ★ OffersBloc (Bloc كامل) + ├── earnings/ (سائق) # cubit + ├── wallet/ # cubit (P3) + ├── chat/ rating/ profile/ settings/ # cubits +``` + +## الشاشات الأساسية (P1) +**الراكب:** onboarding/OTP → الخريطة والطلب → اختيار الوجهة والسعر المقفول → انتظار الإسناد → تتبع حي → دردشة → إنهاء ودفع كاش → تقييم · العناوين المفضلة · السجل. +**السائق:** OTP → تسجيل بالوثائق → الحالة (متصل/مشغول) → استقبال العرض → التوجّه/الوصول → بدء/إنهاء الرحلة → الأرباح. + +## قواعد الهندسة +- **لا منطق عمل في الـ Widgets** — في الـ Cubit/Bloc والـ use cases. +- **الثيم والنصوص من config** حيث أمكن — لا قيم مبعثرة. +- **RTL افتراضي**، اختبار كل شاشة بالعربية أولاً. +- **اختبار:** `bloc_test` لكل Cubit/Bloc حرج + golden tests للشاشات الرئيسية. +- تجريد الخرائط في `tripz_core/maps` لتبديل المزوّد لأي مستأجر خارج تغطية انطلق. + +← السابق: [02-backend-plan](02-backend-plan.md) · التالي: [04-tariff-engine](04-tariff-engine.md) diff --git a/docs/04-tariff-engine.md b/docs/04-tariff-engine.md new file mode 100644 index 0000000..43eaf02 --- /dev/null +++ b/docs/04-tariff-engine.md @@ -0,0 +1,69 @@ +# 04 — محرك التعرفة + +## المبدأ +محرك قواعد يقرأ تعريف التعرفة كـ **وثيقة JSON** لكل مجموعة `(مستأجر × مدينة × فئة خدمة)`. يغطي كل قدرات Onde الموثقة ويضيف ما يحتاجه سوقنا (العداد المنظّم الأردني، تقريب العملة). + +## الأوضاع المدعومة +| الوضع | الوصف | مثال سوق | +|-------|-------|---------| +| `time_and_distance` | زمن + مسافة معاً | عام | +| `time_or_distance` | يبدّل حسب عتبة سرعة (تحت العتبة = دقيقة/زحمة، فوقها = كم) | العداد المنظّم — عمّان | +| `fixed_quote` | سعر مقفول لحظة تحديد الوجهة | يلا غو / كريم | +| `zone_matrix` | مصفوفة منطقة ← منطقة (PostGIS) | مطار ← وسط البلد | + +## المكوّنات +- **فتحة عداد** (flag down) + **سعر/كم** + **سعر/دقيقة** + **رسوم خدمة/حجز**. +- **نوافذ زمنية:** نهار/ليل/جمعة/أعياد بجدولة صريحة. +- **Surge** بسقف معلن (مضاعف حسب العرض/الطلب في خلايا H3، قابل للتعطيل حيث يمنعه المنظّم). +- **رسوم إضافية مسماة:** بدل تطبيق، مطار، أمتعة. +- **بدل انتظار:** بالدقيقة أو بزيادات ثوانٍ. +- **رسوم إلغاء متدرجة** بمرحلة الرحلة + **حد أدنى للأجرة**. +- **قواعد تقريب لكل عملة** (أقرب 0.05 دينار؛ أقرب 500 ل.س...). +- **سياسة إعادة الاحتساب:** متى يُكسر السعر المقفول (انحراف مسار > نسبة محددة). + +## واجهة المحرك (الباك إند) +``` +tariff.quote(input) → يحسب سعراً تقديرياً/مقفولاً قبل الطلب +tariff.finalize(trip) → يحسب السعر النهائي عند الإنهاء +tariff.cancelFee(trip) → رسم الإلغاء حسب المرحلة +``` +كلها نقية، مختبَرة بوحدات (نفس المدخل = نفس المخرج). + +## مثال — عمّان على العداد المنظّم (أرقام رسمية) +```json +{ + "tenant": "amman-operator-x", + "service_class": "taxi-yellow", + "currency": "JOD", + "rounding": { "increment": 0.05, "mode": "nearest" }, + "mode": "time_or_distance", + "speed_threshold_kmh": 18, + "windows": [ + { "name": "day", "from": "06:00", "to": "22:00", + "flag": 0.39, "per_km": 0.28, "per_min_waiting": 0.48 }, + { "name": "night", "from": "22:00", "to": "06:00", + "flag": 0.40, "per_km": 0.33, "per_min_waiting": 0.55 } + ], + "booking_fee": 0.25, + "min_fare": 1.00, + "surge": { "enabled": false }, + "cancellation": [ + { "stage": "after_assign", "after_sec": 120, "fee": 0.50 }, + { "stage": "driver_arrived", "fee": 1.00 } + ], + "recalc_policy": { "fixed_quote": false } +} +``` +> قيم الانتظار بالدقيقة تقديرية للتوضيح وتُضبط من الزيادات الرسمية (كل 35 ثانية) عند التفعيل؛ فتحة العداد وسعر الكيلومتر هما الرقمان الرسميان المعتمدان. + +## مخطط التخزين +- الجدول `tariffs`: `id, tenant_id, city, service_class, definition (jsonb), version, active_from`. +- إصدارات (versioning) — التعرفة النافذة وقت الرحلة تُثبَّت في `trip.tariff_version` للمراجعة. +- مناطق `zone_matrix` تُخزَّن كـ PostGIS polygons في [pricing-zones](02-backend-plan.md). + +## قواعد +- **لا سعر بلا تعرفة نافذة مطابقة** — خطأ صريح لا افتراض صامت. +- **كل رحلة تحفظ نسخة من التعرفة المستخدمة** (audit trail + مطلب المنظّم). +- **التقريب آخر خطوة دائماً** بعد جمع كل المكوّنات. + +← السابق: [03-mobile-plan](03-mobile-plan.md) · التالي: [05-pricing-billing](05-pricing-billing.md) diff --git a/docs/05-pricing-billing.md b/docs/05-pricing-billing.md new file mode 100644 index 0000000..55561e8 --- /dev/null +++ b/docs/05-pricing-billing.md @@ -0,0 +1,50 @@ +# 05 — تسعير المنتج وفوترة المستأجر + +> فرّق بين شيئين: **[محرك التعرفة](04-tariff-engine.md)** يسعّر رحلة الراكب. **هذا الملف** يسعّر اشتراك المستأجر لدينا ويحسب فاتورته الشهرية. + +## الموقع التسعيري مقابل السوق +| المنصة | الإعداد | شهرياً | لكل رحلة | الثغرة | +|--------|---------|--------|----------|--------| +| Onde | $4,500–23,000 | $99–259 | نسبة سرية، حد أدنى $0.10 | غموض + قفل + لا استضافة محلية | +| TaxiCaller | $0 | $20–28/مركبة | — | لا يكبر مع GMV، لا تعريب | +| **Tripz** | **$0–12,000** | **$0–599** | **نسبة معلنة، حد أدنى $0.02** | **الشفافية نفسها هي التمايز** | + +## الباقات الأربع +| الباقة | الإعداد | شهرياً | GMV% | الجوهر | +|--------|---------|--------|------|--------| +| **انطلاقة** | $0 | $0 | 5% | مدينة المستأجر داخل تطبيقنا الجامع، لوحة + dispatch أساسي، حد أدنى $0.03/رحلة، سقف 90 يوماً أو 10k رحلة ثم ترقية | +| **علامة** ★ | $2,500 | $99 | 2.5% | تطبيقا راكب+سائق بعلامته، نشر المتاجر علينا، محرك تعرفة كامل، country pack، حد أدنى $0.02، دعم عربي | +| **أسطول+** | $5,000 | $199 | 2% | dispatch متقدم، بوابة شركات، حجز ويب، محافظ سائقين، API+Webhooks، تقارير متقدمة | +| **سيادة** | من $12,000 | $599 | 1% | نسخة معزولة داخل الدولة، بديل رسم/مركبة، واجهة المنظّم، SLA 99.9%، ضمان تصدير + خيار Escrow | + +## خوارزمية الفوترة الشهرية +``` +invoice = base_fee + + max( min_monthly, + Σ rate(tier_i) × GMV(tier_i) ) // نسب هامشية تنازلية + +شرائح GMV الشهرية (باقة «علامة»): + حتى $50,000 → 2.5% + $50k – $200k → 2.0% + فوق $200,000 → 1.5% + +min_monthly = عدد الرحلات المكتملة × $0.02 +``` + +## مثال محسوب — مشغّل عمّان +- 1,500 رحلة/يوم × 30 = **45,000 رحلة**، متوسط 2.2 دينار (≈$3.10) → **GMV ≈ $139,500**. +- فاتورة «علامة»: `$99 + (2.5%×50k) + (2.0%×89.5k)` ≈ **$3,139/شهر** (~2.3% فعلي). +- **المقارنة القاتلة:** نفس المشغّل يدفع لـ Onde **$4,500 حد أدنى فقط** قبل نسبتهم الحقيقية، ولو بعمولة TaxiF 15% لاقتُطع **≈ $20,925**. + +## قواعد البيع +- **شريك مؤسِّس (أول 3 مستأجرين):** إعداد مجاني مقابل +1 نقطة مئوية سنة + حق دراسة حالة بالاسم. يحل «لا قصص نجاح بعد». +- **دفع سنوي مقدّم:** خصم 15% على الاشتراك. +- **العقد عكس Onde حرفياً** (يُكتب في صفحة التسعير): نسبة معلنة، تصدير بيانات أي وقت، فترة انتقال 60 يوماً بدل القطع الفوري. +- **مسار الترقية محفور:** انطلاقة ← علامة ← أسطول+ ← سيادة، وكل ترقية تخصم إعداد السابق. + +## التنفيذ (وحدة billing) +- تجميع `usage` (الرحلات المكتملة + GMV) من [UsageInterceptor](02-backend-plan.md) شهرياً عبر BullMQ. +- توليد فاتورة PDF + سجل `invoices`. +- لوحة My Hub تعرض الفاتورة الجارية والتاريخية بشفافية كاملة. + +← السابق: [04-tariff-engine](04-tariff-engine.md) · التالي: [06-tenant-model](06-tenant-model.md) diff --git a/docs/06-tenant-model.md b/docs/06-tenant-model.md new file mode 100644 index 0000000..3d26b0e --- /dev/null +++ b/docs/06-tenant-model.md @@ -0,0 +1,59 @@ +# 06 — نموذج الاستئجار متعدد المستأجرين + +## وضعان +### 1. القياسي (Shared) — الافتراضي +- نشرة واحدة مشتركة، قاعدة واحدة، عمود **`tenant_id`** في كل جدول. +- نطاق إلزامي على مستوى الـ Repository (نفس نمط انطلق الحالي). +- الأرخص تشغيلاً والأسرع تحديثاً. يخدم باقات انطلاقة/علامة/أسطول+. + +### 2. السيادة (Sovereign / Enterprise) +- نسخة معزولة كاملة (Docker Compose / K8s namespace) داخل بلد المستأجر أو على خوادمه. +- **نفس الكود، متغيرات بيئة مختلفة** — لا فرع كود منفصل. +- المنتج المضاد لثغرة Onde السيادية. يخدم باقة سيادة. + +## فرض العزل (الأهم أمنياً) +- **كل استعلام يُنطَّق بـ `tenant_id`** — لا استثناء. +- يُفرض عبر أحد نمطين (يُحسم في P0): + - **Repository scoped:** مستودع مخصّص يحقن الشرط تلقائياً. + - **TypeORM subscriber/query filter:** فلتر عام يُضاف لكل استعلام. +- `TenantGuard` يستخرج `tenant_id` من الـ subdomain/header/JWT ويحقنه في سياق الطلب (AsyncLocalStorage). +- **اختبار اختراق العزل** ضمن CI: طلب من مستأجر A لا يرى بيانات B أبداً. + +## حزمة الدولة (Country Pack) +ملف إعداد لكل دولة يجمع كل ما يختلف بين الأسواق: +```yaml +country: JO +currency: JOD +rounding: { increment: 0.05 } +locale: ar +direction: rtl +phone: { length: 9, prefix: "+962" } +otp: { length: 4, ttl_sec: 300 } +payments: [cash, cliq, zaincash] +sms_provider: local_jo +maps_scope: "amman,zarqa,irbid" +default_tariff_mode: time_or_distance # العداد المنظّم +regulator: ltrc # هيئة تنظيم النقل البري +legal_docs: { terms: jo/terms.md, privacy: jo/privacy.md } +``` +**المستأجر الجديد = country pack + هوية بصرية. لا كود جديد.** + +## التطبيقات: كود واحد، نكهة لكل مستأجر +``` +كود موحّد (راكب+سائق) + → flavor لكل مستأجر (bundle id, أيقونة, ألوان, مفاتيح FCM) + → إعداد تشغيلي من GET /tenant/config عند الإقلاع + → fastlane + CI ينشران كل النكهات بأمر واحد +``` +- ديناميكي (من API، بلا إصدار متجر): النصوص، الميزات، طرق الدفع، التعرفة، الألوان. +- بالبناء فقط (flavor): اسم الحزمة، الأيقونة، مفاتيح FCM. +- **الهدف الصلب: مستأجر جديد في يوم عمل واحد.** إن احتاج أكثر — النموذج لا يتوسع. + +## جدول tenants (مبسّط) +``` +tenants: id, name, slug, country_pack, plan, mode (shared|sovereign), + branding (jsonb), features (jsonb), status, created_at +``` +راجع [08-data-model](08-data-model.md) للتفصيل. + +← السابق: [05-pricing-billing](05-pricing-billing.md) · التالي: [07-integrations](07-integrations.md) diff --git a/docs/07-integrations.md b/docs/07-integrations.md new file mode 100644 index 0000000..3839177 --- /dev/null +++ b/docs/07-integrations.md @@ -0,0 +1,59 @@ +# 07 — طبقة التكاملات + +> المبدأ: كل تكامل خارجي = **محوّل (Adapter)** خلف واجهة موحّدة، يُفعَّل من [country pack](06-tenant-model.md). إضافة مزوّد جديد = ملف adapter واحد، لا مساس بالمنطق. + +## 1. الدفع (Payment Adapters) +``` +interface PaymentAdapter { + charge(ctx, amount, currency): PaymentResult + refund(ctx, txId): RefundResult + status(txId): PaymentStatus +} +``` +| المزوّد | الأولوية | السوق | +|---------|---------|-------| +| Cash | اليوم الأول | الكل | +| CliQ | P2 | الأردن | +| ZainCash | P2 | الأردن | +| Syriatel Cash / MTN Cash | P2 | سوريا | +| Binance Pay | P2 (عمل جاهز في مستودعك) | عابر | +- التسويات (settlements) للسائقين والمستأجر عبر BullMQ + سجل `payouts`. + +## 2. OTP و SMS +- **ذاتي أولاً** (أصل موجود من مشروعك) + **مزوّد احتياطي لكل دولة**. +``` +interface SmsAdapter { send(phone, message): SmsResult } +``` +- طول الـ OTP و TTL من country pack. + +## 3. واجهة المنظّم الحكومي (ميزة تُباع لا عبء) +- نقطة **تصدير/بث موحّدة**: رحلات، سائقون، مركبات — قابلة للتشكيل لكل هيئة. +- مصمّمة على شاكلة متطلبات **هيئة تنظيم النقل البري (تعليمات النقل الذكي)**. +``` +interface RegulatorAdapter { + exportTrips(range): RegulatorPayload + streamTrip(trip): void // بث لحظي حيث يُطلب +} +``` +- **فصل الأدوار قانونياً:** نحن مزوّد تقنية، المستأجر هو المشغّل المرخّص (يُوثَّق في العقد). + +## 4. الإشعارات +- **FCM** للراكب والسائق (قوالب من `notifications`). +- **Webhooks عامة** للمستأجرين المتقدمين (أسطول+/سيادة): نظام محاسبة، ERP أسطول... — توقيع HMAC + إعادة محاولة. + +## 5. الخرائط (انطلق) — الخندق التنافسي +- البلاطات عبر **Martin** (HTTPS)، الترميز/التوجيه/snapping من واجهات انطلق. +- **صفر اعتماد على Google** في القلب. +- **طبقة تجريد** في الباك إند والموبايل تسمح بتبديل مزوّد البلاطات لأي مستأجر خارج تغطية انطلق. +- سطر البيع: «خرائط غير محدودة مشمولة — بلا فاتورة للأبد». + +## هيكل الكود +``` +backend/src/integrations/ +├── payments/ { cash, cliq, zaincash, syriatel, mtn, binance }.adapter.ts +├── sms/ { self, twilio, local-jo, local-sy }.adapter.ts +├── regulator/ { ltrc, generic }.adapter.ts +└── registry.ts # يربط اسم المزوّد (من country pack) بالمحوّل +``` + +← السابق: [06-tenant-model](06-tenant-model.md) · التالي: [08-data-model](08-data-model.md) diff --git a/docs/08-data-model.md b/docs/08-data-model.md new file mode 100644 index 0000000..01fe4dd --- /dev/null +++ b/docs/08-data-model.md @@ -0,0 +1,75 @@ +# 08 — نموذج البيانات + +> PostgreSQL 16 + PostGIS. **كل جدول عملياتي فيه `tenant_id`** (عدا الجداول العالمية المعلّمة 🌐). فهارس مكانية للسائقين القريبين ومناطق التعرفة. + +## الجداول الأساسية + +### الاستئجار +``` +🌐 tenants id, name, slug, country_pack, plan, mode(shared|sovereign), + branding jsonb, features jsonb, status, created_at +🌐 country_packs code, config jsonb (أو ملفات في المستودع) + tenant_config tenant_id, key, value (تجاوزات ديناميكية) +``` + +### المستخدمون والأدوار +``` +users id, tenant_id, phone, name, role(rider|driver|dispatcher|admin), + status, created_at +drivers id, tenant_id, user_id, vehicle_id, docs jsonb, verification_status, + rating, is_online, last_location geography(Point) ← فهرس GIST +vehicles id, tenant_id, driver_id, plate, model, service_class, color, docs +``` + +### الرحلات +``` +trips id, tenant_id, rider_id, driver_id, service_class, + origin geography(Point), destination geography(Point), + status, tariff_version, quoted_fare, final_fare, currency, + payment_method, requested_at, assigned_at, completed_at +trip_events id, trip_id, tenant_id, from_status, to_status, source, + payload jsonb, created_at ← سجل الانتقالات (تشخيص + منظّم) +ratings id, tenant_id, trip_id, by_role, stars, comment +chat_messages id, tenant_id, trip_id, sender_id, body, created_at +``` + +### التعرفة والمناطق +``` +tariffs id, tenant_id, city, service_class, definition jsonb, + version, active_from, active +pricing_zones id, tenant_id, name, area geography(Polygon) ← فهرس GIST +zone_matrix id, tenant_id, from_zone, to_zone, price +``` + +### الدفع والفوترة +``` +payments id, tenant_id, trip_id, provider, amount, currency, status, tx_ref +payouts id, tenant_id, driver_id, amount, period, status +usage id, tenant_id, period, completed_trips, gmv, currency ← يغذّي الفوترة +invoices id, tenant_id, period, base_fee, gmv_fee, min_applied, + total, status, pdf_url ← فاتورتنا للمستأجر +``` + +### الإشعارات والتكامل +``` +device_tokens id, tenant_id, user_id, fcm_token, platform +webhooks id, tenant_id, url, events[], secret, active +``` + +## العلاقات (مبسّطة) +``` +tenant 1─* users 1─1 drivers 1─* vehicles +tenant 1─* trips *─1 rider(users) trips *─1 driver(drivers) +trip 1─* trip_events trip 1─1 payment trip 1─* chat_messages +tenant 1─* tariffs tenant 1─* pricing_zones +tenant 1─* usage 1─1 invoice(period) +``` + +## قواعد +- **الهجرات فقط** (migrations)؛ لا `synchronize` في الإنتاج. +- **فهارس GIST** على كل عمود `geography`. +- **فهرس مركّب** يبدأ بـ `tenant_id` على الجداول عالية الاستعلام (trips, users, drivers). +- **soft-delete** (`deleted_at`) لا حذف صلب — بيانات المنظّم والتدقيق. +- الحضور اللحظي وموقع السائق الجاري في **Redis** (لا Postgres) — راجع [09-realtime](09-realtime.md). + +← السابق: [07-integrations](07-integrations.md) · التالي: [09-realtime](09-realtime.md) diff --git a/docs/09-realtime.md b/docs/09-realtime.md new file mode 100644 index 0000000..20e05be --- /dev/null +++ b/docs/09-realtime.md @@ -0,0 +1,51 @@ +# 09 — الطبقة الحية (Realtime) + +## التقنية +- **Socket.IO** عبر `@nestjs/websockets` Gateway. +- **`socket.io-redis-adapter`** للتوسع الأفقي (عدة نسخ من الباك إند تتشارك الحالة). +- **Redis** للحضور (presence) والمطابقة الجغرافية (GEO commands). + +## المصادقة على الاتصال +- كل socket يحمل JWT + `tenant_id` → يُتحقق في `handleConnection`. +- ينضم تلقائياً لغرف مُنطَّقة بالمستأجر: `tenant:{id}:...` — **لا تسريب بين المستأجرين**. + +## القنوات (Namespaces / Rooms) +| القناة | من ← إلى | المحتوى | التردد | +|--------|---------|---------|--------| +| `driver:location` | السائق → الخادم | إحداثيات السائق | مجمّع كل 3–5 ثوانٍ | +| `trip:offers` | الخادم → السائقين المرشحين | عرض طلب جديد + مؤقت | عند الطلب | +| `trip:track` | الخادم → الراكب | موقع السائق + حالة الرحلة | عند التحديث | +| `dispatch:live` | الخادم → لوحة المشغّل | كل الطلبات/السائقين النشطين | مستمر | +| `trip:chat` | ثنائي | رسائل الدردشة | فوري | + +## المطابقة (Matching) — أقرب سائق مؤهل +``` +1. موقع كل سائق متصل → Redis: GEOADD tenant:{id}:drivers +2. عند طلب: GEOSEARCH ضمن نصف قطر متزايد +3. فلترة: service_class مطابق + متصل + غير مشغول + مؤهل +4. ترتيب: الأقرب زمنياً (ETA من routing انطلق) لا مسافة خطية +5. بث العرض تسلسلياً/بالدفعات عبر trip:offers مع مؤقت قبول +6. أول قبول يفوز → trip.assign() → إلغاء بقية العروض +``` + +## الحضور (Presence) +- `is_online` وموقع السائق الجاري في **Redis فقط** (TTL) — لا Postgres. +- عند الفصل: تنظيف من فهرس GEO ومن غرف الرحلة النشطة. +- إعادة الاتصال: يستعيد الحالة من مصدر الحقيقة (Postgres) + يعيد الانضمام للغرف. + +## التكامل مع آلة حالة الرحلة +- أحداث الـ socket تُترجم لأحداث في **TripBloc** (راكب) و**OffersBloc** (سائق) على الموبايل — [03](03-mobile-plan.md). +- كل انتقال حالة يُكتب في `trip_events` [08](08-data-model.md) قبل البث — مصدر الحقيقة أولاً، ثم الإشعار. + +## المهام غير المتزامنة (BullMQ) +تُشغّل خارج مسار الـ realtime لتخفيف الضغط: +- انتهاء صلاحية العروض غير المقبولة (timeout). +- الإشعارات (FCM/SMS). +- التسويات، تجميع usage للفوترة، التقارير. + +## قواعد +- **الخادم مصدر الحقيقة للحالة** — العميل يعرض ما يصله، لا يقرر. +- **تجميع مواقع السائقين** (batch) لتقليل الضغط — لا بث لكل إحداثية. +- **إعادة المحاولة والانقطاع الآمن** — الشبكات الإقليمية متقلبة؛ تصميم offline-tolerant. + +← السابق: [08-data-model](08-data-model.md) · التالي: [10-roadmap](10-roadmap.md) diff --git a/docs/10-roadmap.md b/docs/10-roadmap.md new file mode 100644 index 0000000..88d3d7d --- /dev/null +++ b/docs/10-roadmap.md @@ -0,0 +1,58 @@ +# 10 — خطة التنفيذ المرحلية + +> الهدف: **MVP قابل للبيع خلال ~12 أسبوعاً**، تكافؤ تجاري مع جوهر Onde خلال ~20 أسبوعاً، أول مستأجر فعلي بالشهر الخامس. + +## P0 — التأسيس · أسبوعان +**التسليمات:** +- Monorepo (backend NestJS + mobile: tripz_core + rider + driver). +- استخراج نواة **Tenant/Usage** من انطلق كمكتبة مشتركة. +- CI هيكلي مع **flavors + fastlane** — [11](11-devops-cicd.md). +- نظام تصميم **RTL** للتطبيقين. +- حسم الاسم التجاري وبدء تسجيل العلامة. +- حسم نمط فرض العزل (Repository scoped vs subscriber) — [06](06-tenant-model.md). + +**شرط الخروج:** بناء نكهة تجريبية ثانية بأمر CI واحد. + +## P1 — MVP قابل للبيع · 10 أسابيع +**التسليمات:** +- دورة الرحلة كاملة (طلب فوري ومسبق، إسناد أقرب سائق، تتبع حي، كاش، تقييم). +- تطبيق السائق (تسجيل بالوثائق، عروض، أرباح). +- **محرك تعرفة v1** (الأوضاع الأربعة + النوافذ الزمنية) — [04](04-tariff-engine.md). +- لوحة إدارة أساسية. +- OTP ذاتي، خرائط انطلق كاملة، دردشة رحلة. + +**شرط الخروج:** **100 رحلة حقيقية متتالية بلا تدخل يدوي** على مستأجر تجريبي. + +## P2 — تكافؤ تجاري · 8 أسابيع +**التسليمات:** +- Dispatch هاتفي للمشغّلين. +- مناطق سعر ثابت + Surge بسقف. +- خطط فوترة السائقين. +- محفظة راكب أساسية، برومو وإحالات، حسابات شركات، تقارير. +- **حاسبة التوفير + اللاندنج + صفحة تسعير علنية** — [12](12-landing-gtm.md). +- واجهة المنظّم v1 — [07](07-integrations.md). + +**شرط الخروج:** توقيع أول «شريك مؤسِّس» خارجي. + +## P3 — التوسع · مستمر +**التسليمات:** +- وحدة التوصيل (Super App)، حجز ويب للفنادق، gamification للسائقين، BI متقدم. +- country packs جديدة حسب الطلب، برنامج موزّعين (Reseller). + +**شرط الخروج:** 3 مستأجرين يدفعون في دولتين+. + +## مسار قانوني/ترخيص — **بالتوازي لا لاحقاً** +- تسجيل العلامة (وزارة الصناعة والتجارة الأردنية أولاً) مع شعار مميّز؛ حجز النطاقات وحسابات المتاجر باكراً. +- عقد SaaS (عربي/إنجليزي) ببنود مضادة لعقد Onde: نسبة مكتوبة، تصدير بيانات، فترة انتقال، Escrow للسيادة. +- فصل الأدوار: نحن مزوّد تقنية، المستأجر هو المشغّل المرخّص. +- **سيرو = المستأجر المرجعي رقم صفر**؛ منطقه (OTP/VoIP/التدفقات) يُنقل كمعرفة، والكود يُعاد بناؤه على المعمارية الجديدة. + +## جدول مختصر +| المرحلة | المدة التراكمية | المحصلة | +|---------|----------------|---------| +| P0 | أسبوعان | أساس يبني نكهتين بأمر واحد | +| P1 | ~12 أسبوعاً | MVP قابل للبيع | +| P2 | ~20 أسبوعاً | تكافؤ تجاري + أول شريك مؤسِّس | +| P3 | مستمر | 3 مستأجرين دافعين في دولتين+ | + +← السابق: [09-realtime](09-realtime.md) · التالي: [11-devops-cicd](11-devops-cicd.md) diff --git a/docs/11-devops-cicd.md b/docs/11-devops-cicd.md new file mode 100644 index 0000000..f335c7c --- /dev/null +++ b/docs/11-devops-cicd.md @@ -0,0 +1,57 @@ +# 11 — النشر والأتمتة (DevOps / CI-CD) + +> القاعدة الحاكمة: **مستأجر جديد = ملف إعداد + أصول + أمر CI واحد.** إن احتاج نشر مستأجر أكثر من يوم عمل، النموذج لا يتوسع. + +## البيئات +| البيئة | الغرض | القاعدة | +|--------|-------|---------| +| `local` | تطوير | Docker Compose | +| `staging` | اختبار قبل النشر | مشترك | +| `production-shared` | باقات انطلاقة/علامة/أسطول+ | نشرة مشتركة | +| `production-sovereign-{tenant}` | باقة سيادة | نسخة معزولة داخل الدولة | + +## الباك إند +- **Docker** لكل خدمة + **docker-compose** (Postgres/PostGIS، Redis، Martin، API، worker). +- الهجرات تُشغَّل آلياً في خط النشر (لا `synchronize`). +- الأسرار من متغيرات بيئة / Vault — لا في الكود. +- **وضع السيادة:** نفس صور Docker، `.env` مختلف، تُنشر على خوادم المستأجر أو سحابة داخل الدولة. + +## الموبايل — flavors + fastlane +``` +mobile/ +├── flavors// +│ ├── config.json # ألوان، اسم، bundle id، مفاتيح +│ ├── assets/ # أيقونة، سبلاش، لوجو +│ └── store/ # لقطات، وصف المتجر (تمايز ضد بند آبل 4.3) +├── fastlane/ +│ ├── Fastfile # lanes: build_flavor, deploy_ios, deploy_android +│ └── Appfile +``` +- **أمر واحد:** `fastlane deploy tenant:` يبني الراكب والسائق وينشرهما. +- ما يتغير بالبناء فقط في الـ flavor؛ الباقي ديناميكي من `GET /tenant/config`. +- **درء رفض آبل 4.3:** كل نكهة بأصول ومحتوى متجر مميّز؛ خيار «التطبيق الجامع» للباقة المجانية. + +## خط CI (GitHub Actions أو ما يعادله) +``` +on PR: + - lint + typecheck (backend + flutter analyze) + - unit tests (jest + bloc_test) + - اختبار عزل المستأجر (A لا يرى بيانات B) + - build نكهة تجريبية (تحقق أن flavors يعمل) +on main: + - migrations → staging → smoke tests + - build جميع النكهات النشطة +on tag: + - نشر production (shared) + fastlane للنكهات +``` + +## المراقبة +- تسجيل مركزي (structured logs) مع `tenant_id` و`trip_id` في كل سطر. +- صفحة **حالة (status page)** لكل مستأجر — جزء من عرض الدعم/SLA. +- تنبيهات على: فشل الإسناد، تأخر الـ socket، طوابير BullMQ المتضخمة. + +## قواعد الإصدار +- تحديث كل النكهات عند إصدار فلاتر → **مؤتمت عبر fastlane** (لا يدوي). +- إصدار الباك إند semver؛ الهجرات متوافقة رجعياً حين أمكن (نشر بلا توقف). + +← السابق: [10-roadmap](10-roadmap.md) · التالي: [12-landing-gtm](12-landing-gtm.md) diff --git a/docs/12-landing-gtm.md b/docs/12-landing-gtm.md new file mode 100644 index 0000000..4f7a442 --- /dev/null +++ b/docs/12-landing-gtm.md @@ -0,0 +1,39 @@ +# 12 — اللاندنج بيج وخطة الوصول للسوق (بلا إعلانات مدفوعة) + +## بنية الصفحة — نقتبس هيكل Onde ونقلب رسالته +1. **Hero ثنائي اللغة:** «أطلق تطبيق النقل بعلامتك خلال 30 يوماً — ببيانات تبقى في بلدك». + - CTA أول: **«جرّب الديمو الآن»** — بيئة حية فورية بلا مكالمة (عكس بوابة Onde المقفلة). + - CTA ثانٍ: **زر واتساب مباشر** (سوقنا يشتري بالواتساب لا بـ Calendly). +2. **ثلاث ركائز مقلوبة من ثغرات المنافس:** سيادة بياناتك · تكلفة شفافة (نسبة معلنة + بلا فاتورة خرائط) · إطلاق أسرع (30 يوماً بعلامتك). +3. **حاسبة التوفير التفاعلية:** أدخل رحلاتك اليومية ومتوسط الأجرة ← قارن فاتورتنا بحد Onde الأدنى ($0.10/رحلة) وبعمولة 15%. **أقوى أداة بيع صامتة، تعمل 24/7.** +4. **مكونات المنصة الأربعة** بلقطات RTL حقيقية (راكب، سائق، dispatch، إدارة). +5. **صفحة تسعير علنية بالكامل** — وجودها وحده تمايز في سوق يعتمد الغموض. +6. **دراسة حالة سيرو بالأرقام** ثم **FAQ عربي صريح** (من يملك البيانات؟ ماذا لو انسحبت؟ — نجيب بعكس أجوبة Onde). + +## قنوات البيع بلا ميزانية إعلانات +- **بيع مباشر مستهدف:** قوائم مكاتب التكسي والأساطيل المرخّصة (سجلات هيئات النقل علنية غالباً) — عرض PDF + ديمو واتساب. **هدف: 20 محادثة مؤهلة/شهر.** +- **برنامج الشريك المؤسِّس** — [05](05-pricing-billing.md) — كخبر قابل للنشر في مجموعات ومنتديات النقل. +- **محتوى عربي في فراغ Onde:** «كم يكلف تطبيق تكسي بعلامتك؟» · «شرح تعليمات النقل الذكي للمشغّلين» · «عمولة 15% أم اشتراك؟» — ثلاث مقالات تلتقط بحث Google العربي كله تقريباً لانعدام المنافسة. +- **شراكات إحالة:** شركات تأجير وتمويل المركبات، مكاتب تخليص تراخيص النقل — عمولة % من الإعداد. +- **لاحقاً فقط (بعد أول مرجعين):** معارض النقل الإقليمية وحملات مدفوعة مركّزة. + +## الأصلان الرخيصان اللذان يعوّضان غياب الإعلانات +1. **حاسبة التوفير** (أعلاه) — تبيع وأنت نائم. +2. **ديمو حي** يُجرّب فوراً بلا مكالمة — يزيل احتكاك أكبر من قمع Onde. + +## القمع (عكس قمع Onde) +``` +ديمو حي فوري / محتوى عربي + → محادثة واتساب مؤهلة + → عرض شفاف (النسبة مكتوبة) + حاسبة توفير مخصّصة + → عقد مضاد لـ Onde (تصدير بيانات، فترة انتقال 60 يوماً) + → إطلاق خلال 30 يوماً + → ترقية داخل المنتج (انطلاقة ← علامة ← أسطول+ ← سيادة) +``` + +## التنفيذ التقني للاندنج +- موقع ثابت (Next.js/Astro) أو صفحة واحدة — منفصل عن تطبيق المنصة. +- الديمو الحي = مستأجر تجريبي جاهز (seed data) على staging. +- الحاسبة = JS بحت في المتصفح (لا خادم) — سريعة ومجانية التشغيل. + +← السابق: [11-devops-cicd](11-devops-cicd.md) · التالي: [13-risks-decisions](13-risks-decisions.md) diff --git a/docs/13-risks-decisions.md b/docs/13-risks-decisions.md new file mode 100644 index 0000000..782bb1e --- /dev/null +++ b/docs/13-risks-decisions.md @@ -0,0 +1,38 @@ +# 13 — المخاطر الكبرى والقرارات المفتوحة + +## المخاطر والتخفيف +| الخطر | أثره | التخفيف | +|-------|------|---------| +| استنزاف الدعم التشغيلي 24/7 | توقف التطوير كلياً | توثيق عربي ذاتي الخدمة + مستويات دعم مسعّرة + صفحة حالة + حدود SLA مكتوبة منذ أول عقد | +| رفض آبل للتطبيقات المستنسخة (4.3) | تعطّل نشر النكهات | تمايز أصول/محتوى لكل نكهة، حسابات مطوّر باسم المستأجر، التطبيق الجامع كخطة بديلة — [11](11-devops-cicd.md) | +| مطاردة تكافؤ Onde الكامل | سنة بلا إيراد | الالتزام الصارم بنطاق P1/P2؛ أي ميزة خارجهما تُباع كإضافة لاحقاً — [10](10-roadmap.md) | +| مستأجر ضعيف تسويقياً = GMV صفر | نسبة من لا شيء | الحد الأدنى الشهري لكل رحلة + رسم اشتراك ثابت يضمنان أرضية — [05](05-pricing-billing.md) | +| هبوط سرعة الفريق مع Cubit الجديدة | تأخر MVP شهراً | أسبوع تدريب + قالب Cubit يطابق بنية GetX Controller + مراجعات كود مبكرة | +| تسريب بيانات بين المستأجرين | كارثي (ثقة + قانوني) | فرض `tenant_id` على مستوى Repository + اختبار عزل في CI — [06](06-tenant-model.md) | + +## القرارات التي تحتاج حسم المالك (قبل P0) + +### 1. الاسم التجاري +«نقل ذكي» وصفي وواضح لكنه **ضعيف الحماية** كعلامة، و«Tripz» هو الاسم المؤقت الحالي. +- **الخيار أ:** اعتماد اسم مبتكر (Tripz أو غيره) كعلامة تجارية + «نقل ذكي» وصفاً تسويقياً. +- **الخيار ب:** اعتماد «نقل ذكي» تجارياً مع شعار مميّز يقوّي الحماية. +- **التوصية:** الخيار أ — اسم مبتكر قابل للحماية + الوصف العربي للتسويق. + +### 2. سيرو والمنصة — ✅ محسوم (2026-07-16): مؤجَّل +- **القرار:** نبني الجديد من الصفر (كيوبت + فلاتر + نِست)، **ننسى سيرو عملياً** ونأخذ منه **الخرائط (انطلق) فقط**. +- بعد رؤية نظافة/تنظيم البناء الجديد نقرر: إمّا الاستغناء عن سيرو واعتماد الجديد كأول تطبيق للاستئجار، أو إعادة النظر. القرار النهائي لاحقاً لا الآن. +- التفاصيل في [14-server-conventions](14-server-conventions.md). + +### 3. سوق الانطلاق للمستأجر الخارجي الأول +- **الأردن:** تنظيم واضح، دفع أسهل، لكن TaxiF قائم. +- **سوريا:** فراغ تنافسي أكبر (يلا غو وحيدة تقريباً)، لكن دفع/عملة أصعب. +- **التوصية:** حسم المالك — يعتمد على شبكة علاقاته وأيّ سوق يملك فيه أول عميل جاهز. + +### 4. وحدة التوصيل (Super App) +- P3 افتراضياً، لكن **إن طلبها أول شريك مؤسِّس كشرط توقيع** → تُقدَّم إلى P2؟ +- **التوصية:** لا تُقدَّم إلا مقابل عقد موقّع يبررها؛ لا تُبنى استباقياً. + +## كيف نتتبع هذه القرارات +تُحسم في اجتماع بدء P0 وتُوثَّق هنا بالنتيجة والتاريخ. لا يبدأ الكود قبل حسم 1 و2 و3 على الأقل. + +← السابق: [12-landing-gtm](12-landing-gtm.md) · التالي: [project-tree](project-tree.md) diff --git a/docs/14-server-conventions.md b/docs/14-server-conventions.md new file mode 100644 index 0000000..3cd0ab0 --- /dev/null +++ b/docs/14-server-conventions.md @@ -0,0 +1,50 @@ +# 14 — اصطلاحات السيرفر والعزل (قرارات تشغيلية محسومة) + +> هذه قرارات نهائية اتخذها المالك — تُطبَّق حرفياً في الكود والـ config من اليوم الأول. + +## 1. بيئة العمل مقابل النشر +- **كتابة الكود والتطوير:** على الماك (محلياً). +- **النشر (Deployment):** على السيرفر. +- **الباك إند يعمل داخل Docker** في الحالتين (نفس الصور محلياً وعلى السيرفر). + +## 2. السيرفر مشترك — العزل إلزامي +السيرفر الحالي **تجريبي ومشترك**: عليه برامج وملفات كثيرة (WordPress وغيره) وقد يعمل عليه نفس التطبيق أكثر من مرة. لذلك كل موارد Tripz تُعزل بوضوح: + +### أ. بادئة (Prefix) لكل شيء +- **جداول قاعدة البيانات:** بادئة `tripz_` لكل جدول (مثال: `tripz_trips`, `tripz_users`). +- **مفاتيح Redis:** بادئة `tripz:` قبل كل مفتاح (فوق بادئة `tenant:` الداخلية). +- **قوائم/طوابير BullMQ:** بادئة `tripz_`. +- **أسماء حاويات/شبكات Docker:** بادئة `tripz-`. + +### ب. قاعدة بيانات منفصلة عن الأصلية +- **PostgreSQL:** قاعدة بيانات مستقلة خاصة بـ Tripz (اسمها `tripz`)، لا نشارك قاعدة أي برنامج آخر. +- **Redis:** نختار **رقم قاعدة بيانات (DB index) غير الافتراضي 0** لتفادي التصادم — نعتمد **DB رقم 3** (من أصل 0–15). قابل للتعديل عبر `REDIS_DB` في البيئة، لكن الافتراضي المعتمد ≠ 0. + +### ج. متغيرات البيئة الحاكمة +```env +# PostgreSQL +DB_NAME=tripz +DB_TABLE_PREFIX=tripz_ + +# Redis — رقم غير افتراضي للعزل عن باقي البرامج على السيرفر +REDIS_DB=3 +REDIS_KEY_PREFIX=tripz: + +# BullMQ +QUEUE_PREFIX=tripz_ +``` +- في TypeORM: يُضبط `entityPrefix: process.env.DB_TABLE_PREFIX`. +- في Redis client / BullMQ: تُمرَّر `db` و`keyPrefix` من البيئة. + +## 3. الخرائط — انطلق فقط (المأخوذ الوحيد من سيرو) +- نأخذ من سيرو **شيئاً واحداً فقط: تكامل خرائط انطلق** — الـ API والباكج والتنظيم الكامل الموجود في خدمة الخرائط (MapService) بتطبيق سيرو. +- **الخطوة العملية:** نفحص باكج/تنظيم انطلق في سيرو، ننقله/نكيّفه إلى تطبيق فلاتر الجديد (داخل `mobile/packages/tripz_core/maps`) وإلى وكيل الخرائط في الباك إند. +- كل ما عدا الخرائط من سيرو: **لا يُنقل كوداً** — يُعاد البناء من جديد بـ Cubit + Bloc + NestJS. + +## 4. مصير سيرو — مؤجَّل (نعمل ونقرر لاحقاً) +- **لا نفصل سيرو الآن ولا نربطه الآن.** نبني منصة جديدة من الصفر (كيوبت + فلاتر + نِست) ونرى النتيجة والتنظيم. +- إذا خرج البناء الجديد نظيفاً ومرتباً → **قد نستغني عن سيرو** ونعتمد الجديد كأول تطبيق حقيقي/مبدئي للاستئجار. +- إذا لا → نعيد النظر. القرار النهائي **بعد رؤية النتيجة**، لا الآن. +- الثابت الوحيد الآن: **ننسى سيرو عملياً ونأخذ منه الخرائط (انطلق) فقط**. + +← يُقرأ مع: [06-tenant-model](06-tenant-model.md) · [11-devops-cicd](11-devops-cicd.md) · [07-integrations](07-integrations.md) diff --git a/docs/15-deploy-flow.md b/docs/15-deploy-flow.md new file mode 100644 index 0000000..832c669 --- /dev/null +++ b/docs/15-deploy-flow.md @@ -0,0 +1,49 @@ +# 15 — تدفق النشر (Mac ⟶ Server) + +> القاعدة: **لا بناء ولا تشغيل على الماك.** الماك للكتابة فقط. البناء والتشغيل والهجرات كلها على السيرفر عبر Docker. + +## السيرفر +- `root@194.163.173.157` (CloudPanel — يستضيف مواقع كثيرة). +- مجلد Tripz المخصّص: **`/home/tripz-llc`** (منفصل عن مواقع CloudPanel لتفادي التصادم). +- Git الخاص: `https://git.intaleqapp.com/Hamza/tripz-llc.git`. + +## الطريقة المعتمدة (الأنظف): Git +``` +# على الماك (مرة واحدة) — إعداد الريبو والدفع +git add . && git commit -m "..." && git push + +# على السيرفر — أول مرة +cd /home && git clone https://git.intaleqapp.com/Hamza/tripz-llc.git +cd tripz-llc/backend && cp .env.example .env # عدّل الأسرار +docker compose up -d --build +docker compose exec api npm run migration:run + +# على السيرفر — كل تحديث +cd /home/tripz-llc && git pull +cd backend && docker compose up -d --build +``` +`.gitignore` يمنع رفع node_modules/dist/.env/الملفات الكبيرة — **نصوص فقط**. + +## الطريقة البديلة (سريعة بلا git): rsync +``` +./sync-to-server.sh # مزامنة الكود فقط +./sync-to-server.sh --deploy # مزامنة + docker compose up + migrations +./sync-to-server.sh --logs # متابعة اللوغ +``` +- يرفع النصوص فقط، يستثني المخرجات والأسرار، ويُبقي `.env` على السيرفر. +- عدّل `REMOTE_DIR` في أعلى السكربت لو أردت مساراً آخر. + +## الفصل والعزل على السيرفر المشترك (راجع docs/14) +- مجلد مستقل `/home/tripz-llc`، قاعدة `tripz`، بادئة جداول `tripz_`، Redis DB **3**. +- منافذ مضيف غير قياسية: API `4010`، Postgres `55432`. +- حاويات/شبكة/فوليوم ببادئة `tripz-` — لا تصادم مع WordPress أو باقي المشاريع. + +## أول رفع (الأوامر التي طُلبت) +``` +git init +git checkout -b main +git add . +git commit -m "first commit: منصة Tripz — خطط + سكافولد باك إند NestJS/Docker" +git remote add origin https://git.intaleqapp.com/Hamza/tripz-llc.git +git push -u origin main +``` diff --git a/docs/project-tree.md b/docs/project-tree.md new file mode 100644 index 0000000..2409b5a --- /dev/null +++ b/docs/project-tree.md @@ -0,0 +1,101 @@ +# 🌳 شجرة المستودع الكاملة (المرجع البنائي) + +> هذا هو الشكل المستهدف للمونوريبو بعد P0. ابنِ نحوه تدريجياً. الرموز: ★ = آلة حالة Bloc كاملة، 🌐 = عالمي بلا tenant_id. + +``` +Tripz/ +├── README.md +├── docs/ # ← أنت هنا (الخطط) +│ +├── backend/ # NestJS — [02] [08] [09] +│ ├── src/ +│ │ ├── main.ts +│ │ ├── app.module.ts +│ │ ├── common/ +│ │ │ ├── tenant/ # TenantGuard, scoped repo, @Tenant() [06] +│ │ │ ├── usage/ # UsageInterceptor (من انطلق) [05] +│ │ │ ├── guards/ interceptors/ filters/ pipes/ decorators/ +│ │ ├── config/ # ConfigModule + country-packs loader [06] +│ │ ├── modules/ +│ │ │ ├── auth/ tenants/ users/ drivers/ +│ │ │ ├── trips/ # ★ آلة حالة الرحلة (مصدر الحقيقة) [02] +│ │ │ ├── matching/ # Redis GEO [09] +│ │ │ ├── tariff/ # محرك التعرفة [04] +│ │ │ ├── dispatch/ maps/ payments/ billing/ +│ │ │ ├── pricing-zones/ notifications/ regulator/ webhooks/ admin/ +│ │ ├── realtime/ # Socket.IO Gateway + redis adapter [09] +│ │ ├── jobs/ # BullMQ processors [09] +│ │ ├── database/ +│ │ │ ├── entities/ # [08] +│ │ │ ├── migrations/ +│ │ │ └── seeds/ # مستأجر تجريبي للديمو [12] +│ │ └── integrations/ # [07] +│ │ ├── payments/ sms/ regulator/ registry.ts +│ ├── test/ # e2e + اختبار عزل المستأجر [11] +│ ├── docker-compose.yml # postgres/postgis, redis, martin, api, worker +│ ├── Dockerfile +│ └── package.json +│ +├── mobile/ # Flutter — [03] +│ ├── packages/ +│ │ └── tripz_core/ # مشترك +│ │ └── lib/ +│ │ ├── models/ network/ maps/ tariff/ theme/ l10n/ config/ +│ ├── apps/ +│ │ ├── rider/ +│ │ │ └── lib/ +│ │ │ ├── main_.dart app.dart +│ │ │ ├── core/ # DI (get_it), router (go_router), dio +│ │ │ ├── data/ domain/ +│ │ │ └── features/ +│ │ │ ├── auth/ home/ +│ │ │ ├── trip/ # ★ TripBloc + شاشات دورة الرحلة [03] +│ │ │ ├── chat/ rating/ profile/ settings/ wallet/(P3) +│ │ └── driver/ +│ │ └── lib/ +│ │ └── features/ +│ │ ├── auth/ registration/ +│ │ ├── offers/ # ★ OffersBloc [03] +│ │ ├── active-trip/ earnings/ profile/ +│ ├── flavors// # config.json + assets/ + store/ [11] +│ ├── fastlane/ # Fastfile (build/deploy lanes) [11] +│ └── melos.yaml # إدارة المونوريبو (اختياري) +│ +├── admin-web/ # لوحة My Hub (Web) — [02] admin module +│ └── src/ # React/Next أو Flutter Web +│ +├── landing/ # اللاندنج + الحاسبة + صفحة التسعير — [12] +│ └── src/ # Next.js/Astro (منفصل عن المنصة) +│ +├── country-packs/ # حزم الدول — [06] +│ ├── jo.yaml sy.yaml +│ └── legal/{jo,sy}/{terms,privacy}.md +│ +├── infra/ # [11] +│ ├── docker/ k8s/(للسيادة) terraform/(اختياري) +│ └── github-actions/ # خطوط CI +│ +└── contracts/ # عقود SaaS مضادة لـ Onde (ar/en) — [10] +``` + +## خريطة «ملف الخطة ← مجلد الكود» +| الخطة | يتحقق في | +|-------|----------| +| [02 backend](02-backend-plan.md) | `backend/src/modules`, `common` | +| [03 mobile](03-mobile-plan.md) | `mobile/apps`, `packages/tripz_core` | +| [04 tariff](04-tariff-engine.md) | `backend/src/modules/tariff` | +| [05 billing](05-pricing-billing.md) | `backend/src/modules/billing`, `common/usage` | +| [06 tenant](06-tenant-model.md) | `backend/src/common/tenant`, `country-packs` | +| [07 integrations](07-integrations.md) | `backend/src/integrations` | +| [08 data](08-data-model.md) | `backend/src/database` | +| [09 realtime](09-realtime.md) | `backend/src/realtime`, `matching`, `jobs` | +| [11 devops](11-devops-cicd.md) | `infra`, `mobile/fastlane`, `flavors` | +| [12 landing](12-landing-gtm.md) | `landing`, `admin-web` | + +## ترتيب البناء المقترح (يتبع [10-roadmap](10-roadmap.md)) +1. **P0:** `backend` (nest new + tenant/usage) → `mobile` (core + rider/driver skeleton) → `infra/CI` → flavor تجريبي ثانٍ. +2. **P1:** `trips` + `matching` + `tariff` + `realtime` + شاشات الرحلة + `admin-web` أساسي. +3. **P2:** `dispatch` + `pricing-zones` + `billing` + `regulator` + `landing` + الحاسبة. +4. **P3:** التوصيل + حجز الويب + BI + country packs جديدة. + +← العودة إلى [README](../README.md) diff --git a/sync-to-server.sh b/sync-to-server.sh new file mode 100755 index 0000000..e9eb0a6 --- /dev/null +++ b/sync-to-server.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# ============================================================ +# Tripz — مزامنة الكود من الماك إلى السيرفر (نصوص فقط) +# الاستخدام من تيرمينال الماك: +# ./sync-to-server.sh # مزامنة فقط +# ./sync-to-server.sh --deploy # مزامنة ثم docker compose up على السيرفر +# ./sync-to-server.sh --logs # عرض لوغ الحاويات على السيرفر +# ملاحظة: لا نبني ولا نشغّل شيئاً على الماك — كل التشغيل على السيرفر عبر Docker. +# ============================================================ +set -euo pipefail + +# ---- إعدادات السيرفر (عدّل عند اللزوم) ---- +SERVER_USER="root" +SERVER_IP="194.163.173.157" +REMOTE_DIR="/home/tripz-llc" # مجلد مخصّص جديد (منفصل عن مواقع CloudPanel) +LOCAL_DIR="$(cd "$(dirname "$0")" && pwd)/" + +SSH="ssh ${SERVER_USER}@${SERVER_IP}" + +echo "==> مزامنة ${LOCAL_DIR} ⟶ ${SERVER_USER}@${SERVER_IP}:${REMOTE_DIR}" + +# تأكد من وجود المجلد على السيرفر +$SSH "mkdir -p ${REMOTE_DIR}" + +# rsync: يرفع النصوص فقط، يستثني المخرجات والأسرار، ويحذف المحذوف محلياً +# لكن يُبقي .env على السيرفر (يُنشأ هناك ولا يُرفع من الماك) +rsync -avz --delete \ + --exclude='.git/' \ + --exclude='node_modules/' \ + --exclude='dist/' \ + --exclude='build/' \ + --exclude='.dart_tool/' \ + --exclude='**/pgdata/' \ + --exclude='**/redisdata/' \ + --exclude='.env' \ + --exclude='.DS_Store' \ + --exclude='*.log' \ + "${LOCAL_DIR}" "${SERVER_USER}@${SERVER_IP}:${REMOTE_DIR}/" + +echo "==> تمت المزامنة." + +# ---- خيارات إضافية ---- +case "${1:-}" in + --deploy) + echo "==> تشغيل docker compose على السيرفر..." + $SSH "cd ${REMOTE_DIR}/backend && \ + [ -f .env ] || cp .env.example .env && \ + docker compose up -d --build && \ + docker compose exec -T api npm run migration:run || true" + echo "==> التطبيق: http://${SERVER_IP}:4010/api/health" + ;; + --logs) + $SSH "cd ${REMOTE_DIR}/backend && docker compose logs --tail=100 -f" + ;; +esac