first commit: منصة Tripz — خطط كاملة + سكافولد باك إند NestJS/Docker
- 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 <noreply@anthropic.com>
This commit is contained in:
+64
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
# Tripz — تطبيق السائق (Flutter)
|
||||
كود موحّد + flavor لكل مستأجر. **OffersBloc** لتدفق العروض. راجع docs/03.
|
||||
الحالة: هيكل placeholder — يُنشأ فعلياً في P0/P1.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Tripz — تطبيق الراكب (Flutter)
|
||||
كود موحّد + flavor لكل مستأجر. الحالة: Cubit افتراضياً، **TripBloc** لدورة الرحلة. راجع docs/03.
|
||||
الحالة: هيكل placeholder — يُنشأ فعلياً في P0/P1.
|
||||
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
dist
|
||||
npm-debug.log
|
||||
.env
|
||||
.git
|
||||
.gitignore
|
||||
test
|
||||
**/*.spec.ts
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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://<server>:4010/api/health # {"status":"ok",...}
|
||||
# توثيق Swagger: http://<server>: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(وكيل انطلق).
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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<string>('db.host'),
|
||||
port: cfg.get<number>('db.port'),
|
||||
database: cfg.get<string>('db.name'),
|
||||
username: cfg.get<string>('db.user'),
|
||||
password: cfg.get<string>('db.password'),
|
||||
// بادئة الجداول (tripz_) لعزل السيرفر المشترك — راجع docs/14
|
||||
entityPrefix: cfg.get<string>('db.tablePrefix'),
|
||||
synchronize: cfg.get<boolean>('db.synchronize'),
|
||||
autoLoadEntities: true,
|
||||
}),
|
||||
}),
|
||||
|
||||
ThrottlerModule.forRoot([{ ttl: 60000, limit: 120 }]),
|
||||
|
||||
HealthModule,
|
||||
TenantsModule,
|
||||
],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(TenantMiddleware).forRoutes('*');
|
||||
}
|
||||
}
|
||||
@@ -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<TenantStore>();
|
||||
|
||||
export function currentTenantId(): string | undefined {
|
||||
return tenantContext.getStore()?.tenantId;
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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<any> {
|
||||
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}`);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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}'],
|
||||
});
|
||||
@@ -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<string, any>;
|
||||
|
||||
@Column({ type: 'jsonb', default: {} })
|
||||
features: Record<string, any>;
|
||||
|
||||
@Column({ default: 'active' })
|
||||
status: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
await q.query(`DROP TABLE IF EXISTS tripz_tenants`);
|
||||
}
|
||||
}
|
||||
@@ -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<number>('apiPort') ?? 4010;
|
||||
await app.listen(port, '0.0.0.0');
|
||||
Logger.log(`Tripz API on :${port} (docs at /api/docs)`, 'Bootstrap');
|
||||
}
|
||||
bootstrap();
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HealthController } from './health.controller';
|
||||
|
||||
@Module({ controllers: [HealthController] })
|
||||
export class HealthModule {}
|
||||
@@ -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<Tenant>) {
|
||||
return this.tenants.create(body);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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<Tenant>,
|
||||
) {}
|
||||
|
||||
findAll(): Promise<Tenant[]> {
|
||||
return this.repo.find();
|
||||
}
|
||||
|
||||
findBySlug(slug: string): Promise<Tenant | null> {
|
||||
return this.repo.findOne({ where: { slug } });
|
||||
}
|
||||
|
||||
create(data: Partial<Tenant>): Promise<Tenant> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Tripz — لوحة المستأجر (My Hub)
|
||||
لوحة إدارة **لكل مستأجر**: سائقوه، مشغّلوه، تعرفته، تقاريره، فاتورته. راجع docs/02 (admin) و docs/05.
|
||||
الحالة: placeholder — تُبنى في P1/P2.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Tripz — لوحة السوبر-آدمن (مالك المنصة)
|
||||
لوحة **مالك المنصة** فوق كل المستأجرين: إنشاء/تعطيل مستأجر، متابعة كل التطبيقات،
|
||||
GMV والفواتير عبر المنصة، حالة السيرفرات، إطلاق نكهات جديدة. مختلفة عن admin-web (لكل مستأجر).
|
||||
الحالة: placeholder — تُبنى بعد استقرار الباك إند.
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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_<flavor>.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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,58 @@
|
||||
# 10 — خطة التنفيذ المرحلية
|
||||
|
||||
> الهدف: **MVP قابل للبيع خلال ~12 أسبوعاً**، تكافؤ تجاري مع جوهر Onde خلال ~20 أسبوعاً، أول مستأجر فعلي بالشهر الخامس.
|
||||
|
||||
## <a id="p0"></a>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)
|
||||
@@ -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/<tenant>/
|
||||
│ ├── config.json # ألوان، اسم، bundle id، مفاتيح
|
||||
│ ├── assets/ # أيقونة، سبلاش، لوجو
|
||||
│ └── store/ # لقطات، وصف المتجر (تمايز ضد بند آبل 4.3)
|
||||
├── fastlane/
|
||||
│ ├── Fastfile # lanes: build_flavor, deploy_ios, deploy_android
|
||||
│ └── Appfile
|
||||
```
|
||||
- **أمر واحد:** `fastlane deploy tenant:<slug>` يبني الراكب والسائق وينشرهما.
|
||||
- ما يتغير بالبناء فقط في الـ 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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
```
|
||||
@@ -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_<flavor>.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/<tenant>/ # 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)
|
||||
Executable
+55
@@ -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
|
||||
Reference in New Issue
Block a user