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:
2026-07-16 15:22:06 +03:00
co-authored by Claude Opus 4.8
commit 95fea546f5
49 changed files with 1881 additions and 0 deletions
+64
View File
@@ -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
+59
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
# Tripz — تطبيق السائق (Flutter)
كود موحّد + flavor لكل مستأجر. **OffersBloc** لتدفق العروض. راجع docs/03.
الحالة: هيكل placeholder — يُنشأ فعلياً في P0/P1.
+3
View File
@@ -0,0 +1,3 @@
# Tripz — تطبيق الراكب (Flutter)
كود موحّد + flavor لكل مستأجر. الحالة: Cubit افتراضياً، **TripBloc** لدورة الرحلة. راجع docs/03.
الحالة: هيكل placeholder — يُنشأ فعلياً في P0/P1.
+8
View File
@@ -0,0 +1,8 @@
node_modules
dist
npm-debug.log
.env
.git
.gitignore
test
**/*.spec.ts
+32
View File
@@ -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
+21
View File
@@ -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"]
+48
View File
@@ -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(وكيل انطلق).
+74
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
+53
View File
@@ -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"
}
}
+40
View File
@@ -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}`);
}),
);
}
}
+41
View File
@@ -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',
},
});
+22
View File
@@ -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`);
}
}
+30
View File
@@ -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,
};
}
}
+17
View File
@@ -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();
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
+23
View File
@@ -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
}
}
+3
View File
@@ -0,0 +1,3 @@
# Tripz — لوحة المستأجر (My Hub)
لوحة إدارة **لكل مستأجر**: سائقوه، مشغّلوه، تعرفته، تقاريره، فاتورته. راجع docs/02 (admin) و docs/05.
الحالة: placeholder — تُبنى في P1/P2.
+4
View File
@@ -0,0 +1,4 @@
# Tripz — لوحة السوبر-آدمن (مالك المنصة)
لوحة **مالك المنصة** فوق كل المستأجرين: إنشاء/تعطيل مستأجر، متابعة كل التطبيقات،
GMV والفواتير عبر المنصة، حالة السيرفرات، إطلاق نكهات جديدة. مختلفة عن admin-web (لكل مستأجر).
الحالة: placeholder — تُبنى بعد استقرار الباك إند.
+48
View File
@@ -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)
+61
View File
@@ -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)
+81
View File
@@ -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)
+64
View File
@@ -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)
+69
View File
@@ -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)
+50
View File
@@ -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)
+59
View File
@@ -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)
+59
View File
@@ -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)
+75
View File
@@ -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)
+51
View File
@@ -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)
+58
View File
@@ -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)
+57
View File
@@ -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)
+39
View File
@@ -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)
+38
View File
@@ -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)
+50
View File
@@ -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)
+49
View File
@@ -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
```
+101
View File
@@ -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)
+55
View File
@@ -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