P2: ride types + class-aware matching + round-trip + dispatch + wallet + FCM + WS live location/WebRTC

- ride-types: per-tenant catalog (economy/comfort/electric/family_van/women/scooter), seeded
- matching now class-scoped (geo:drivers:{tenant}:{class}); trips match by requested type
- trips: is_round_trip (doubles distance for fare) + payment_method + wallet settlement on paid
- dispatch: operator creates trip for phone customer (role-guarded)
- wallet: balance + credit/debit + txns + self topup; wallet-paid trips settle rider→driver
- notifications: FCM device tokens + best-effort push (assign event)
- realtime: trip:join, live driver:location broadcast, WebRTC call signaling (offer/answer/ice/end)
- migration InitP2 (ride_types, wallets, wallet_txns, device_tokens, trips.is_round_trip)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 17:39:56 +03:00
co-authored by Claude Opus 4.8
parent ccb80d9e53
commit 16df6258c1
30 changed files with 737 additions and 27 deletions
@@ -0,0 +1,21 @@
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { DispatchService, DispatchOrderDto } from './dispatch.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { CurrentUser, AuthUser } from '../auth/decorators/current-user.decorator';
@ApiTags('dispatch')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('dispatcher', 'admin')
@Controller('dispatch')
export class DispatchController {
constructor(private readonly dispatch: DispatchService) {}
@Post('orders')
create(@CurrentUser() user: AuthUser, @Body() body: DispatchOrderDto) {
return this.dispatch.createOrder(user.tenantId, body);
}
}
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { DispatchService } from './dispatch.service';
import { DispatchController } from './dispatch.controller';
import { UsersModule } from '../users/users.module';
import { TripsModule } from '../trips/trips.module';
@Module({
imports: [UsersModule, TripsModule],
controllers: [DispatchController],
providers: [DispatchService],
})
export class DispatchModule {}
@@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { TripsService, RequestTripDto } from '../trips/trips.service';
export interface DispatchOrderDto extends RequestTripDto {
phone: string; // هاتف الراكب الذي اتصل
name?: string;
}
/**
* لوحة المشغّل: إنشاء رحلة نيابةً عن راكب اتصل هاتفياً (docs/02).
* يجد/ينشئ مستخدم الراكب بالهاتف ثم يطلب الرحلة باسمه.
*/
@Injectable()
export class DispatchService {
constructor(
private readonly users: UsersService,
private readonly trips: TripsService,
) {}
async createOrder(tenantId: string, dto: DispatchOrderDto) {
const rider = await this.users.findOrCreate(tenantId, dto.phone, 'rider');
return this.trips.request(tenantId, rider.id, dto);
}
}