feat: add transit microservice and supervisor dashboard

This commit is contained in:
Hamza-Ayed
2026-07-18 18:18:40 +03:00
parent 4aeab5275a
commit f30209b1d7
43 changed files with 3642 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# -----------------------------------------------------------------------------
# Tripz Transit Microservice Configuration
# -----------------------------------------------------------------------------
# --- Server Port ---
PORT=4020
# --- Database Credentials (Transit) ---
# These must match what's in docker-compose.yml under transit-db
DB_HOST=transit-db
DB_PORT=5432
DB_USER=transit_user
DB_PASSWORD=26ab87247a55f86032c943b7e361c475cd3ca8b3b772d82e2fe6a9b5ee0839a3
DB_NAME=tripz_transit
+39
View File
@@ -0,0 +1,39 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and Editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# Environment
.env
.env.test
.env.production
+27
View File
@@ -0,0 +1,27 @@
version: '3.8'
services:
transit-db:
image: postgres:15-alpine
container_name: tripz-transit-db
env_file:
- .env
ports:
- "5433:5432" # Exposed on 5433 to avoid conflict with main DB
volumes:
- transit_pgdata:/var/lib/postgresql/data
transit-api:
build:
context: .
dockerfile: Dockerfile
container_name: tripz-transit-api
ports:
- "4020:4020"
env_file:
- .env
depends_on:
- transit-db
volumes:
transit_pgdata:
+27
View File
@@ -0,0 +1,27 @@
{
"name": "backend-transit",
"version": "1.0.0",
"description": "Tripz Transit Microservice",
"scripts": {
"build": "nest build",
"start": "nest start",
"start:dev": "nest start --watch",
"start:prod": "node dist/main"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/typeorm": "^10.0.0",
"pg": "^8.11.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"typeorm": "^0.3.17"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@types/node": "^20.3.1",
"typescript": "^5.1.3"
}
}
+25
View File
@@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TransitModule } from './transit/transit.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
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'),
synchronize: true, // Should be false in production
autoLoadEntities: true,
}),
}),
TransitModule,
],
})
export class AppModule {}
+18
View File
@@ -0,0 +1,18 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const cfg = app.get(ConfigService);
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
app.enableCors();
// Start the standalone transit API
const port = cfg.get<number>('PORT') || 4020;
await app.listen(port);
console.log(`Transit Microservice running on port ${port}`);
}
bootstrap();
@@ -0,0 +1,32 @@
export class CreateRouteDto {
name: string;
tenant_id: string;
polyline?: Record<string, any>;
is_active?: boolean;
}
export class CreateStationDto {
route_id: string;
name: string;
latitude: number;
longitude: number;
order_index: number;
}
export class CreateSupervisorDto {
tenant_id: string;
user_id: string;
route_id?: string;
institution_name?: string;
}
export class StartTripDto {
route_id: string;
driver_id: string;
}
export class BuyTicketDto {
passenger_id: string;
bus_trip_id: string;
price: number;
}
@@ -0,0 +1,33 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { TransitRoute } from './transit-route.entity';
export type BusTripStatus = 'active' | 'completed' | 'cancelled';
@Entity('transit_bus_trips')
export class TransitBusTrip {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
route_id: string;
// External reference to the main DB user
@Column()
driver_id: string;
@Column({ type: 'varchar', length: 50, default: 'active' })
status: BusTripStatus;
@Column({ nullable: true })
current_station_id: string;
@CreateDateColumn()
created_at: Date;
@UpdateDateColumn()
updated_at: Date;
@ManyToOne(() => TransitRoute)
@JoinColumn({ name: 'route_id' })
route: TransitRoute;
}
@@ -0,0 +1,25 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('transit_routes')
export class TransitRoute {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
tenant_id: string;
@Column({ length: 150 })
name: string;
@Column({ type: 'jsonb', nullable: true })
polyline: Record<string, any>;
@Column({ default: true })
is_active: boolean;
@CreateDateColumn()
created_at: Date;
@UpdateDateColumn()
updated_at: Date;
}
@@ -0,0 +1,30 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { TransitRoute } from './transit-route.entity';
@Entity('transit_stations')
export class TransitStation {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
route_id: string;
@Column({ length: 150 })
name: string;
@Column('decimal', { precision: 10, scale: 7 })
latitude: number;
@Column('decimal', { precision: 10, scale: 7 })
longitude: number;
@Column('int')
order_index: number;
@CreateDateColumn()
created_at: Date;
@ManyToOne(() => TransitRoute)
@JoinColumn({ name: 'route_id' })
route: TransitRoute;
}
@@ -0,0 +1,28 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { TransitRoute } from './transit-route.entity';
@Entity('transit_supervisors')
export class TransitSupervisor {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
tenant_id: string;
// External reference to the main DB user
@Column()
user_id: string;
@Column({ nullable: true })
route_id: string;
@Column({ length: 150, nullable: true })
institution_name: string;
@CreateDateColumn()
created_at: Date;
@ManyToOne(() => TransitRoute)
@JoinColumn({ name: 'route_id' })
route: TransitRoute;
}
@@ -0,0 +1,33 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { TransitBusTrip } from './transit-bus-trip.entity';
export type TicketStatus = 'active' | 'used' | 'refunded';
@Entity('transit_tickets')
export class TransitTicket {
@PrimaryGeneratedColumn('uuid')
id: string;
// External reference to the main DB user
@Column()
passenger_id: string;
@Column()
bus_trip_id: string;
@Column('decimal', { precision: 10, scale: 3 })
price: number;
@Column({ type: 'varchar', length: 50, default: 'active' })
status: TicketStatus;
@Column({ nullable: true })
qr_code: string;
@CreateDateColumn()
created_at: Date;
@ManyToOne(() => TransitBusTrip)
@JoinColumn({ name: 'bus_trip_id' })
bus_trip: TransitBusTrip;
}
@@ -0,0 +1,53 @@
import { Controller, Post, Get, Body, Param, Query } from '@nestjs/common';
import { TransitService } from './transit.service';
import { CreateRouteDto, CreateStationDto, CreateSupervisorDto, StartTripDto, BuyTicketDto } from './dto/transit.dto';
@Controller('transit')
export class TransitController {
constructor(private readonly transitService: TransitService) {}
@Post('routes')
createRoute(@Body() dto: CreateRouteDto) {
return this.transitService.createRoute(dto);
}
@Get('routes')
getRoutes(@Query('tenant_id') tenant_id: string) {
return this.transitService.getRoutes(tenant_id);
}
@Post('stations')
createStation(@Body() dto: CreateStationDto) {
return this.transitService.createStation(dto);
}
@Get('routes/:routeId/stations')
getStations(@Param('routeId') routeId: string) {
return this.transitService.getStations(routeId);
}
@Post('supervisors')
assignSupervisor(@Body() dto: CreateSupervisorDto) {
return this.transitService.assignSupervisor(dto);
}
@Get('reports/supervisor/:userId')
getSupervisorReports(@Param('userId') userId: string) {
return this.transitService.getSupervisorReports(userId);
}
@Post('trips/start')
startTrip(@Body() dto: StartTripDto) {
return this.transitService.startTrip(dto);
}
@Post('trips/:id/end')
endTrip(@Param('id') id: string) {
return this.transitService.endTrip(id);
}
@Post('tickets/buy')
buyTicket(@Body() dto: BuyTicketDto) {
return this.transitService.buyTicket(dto);
}
}
@@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TransitRoute } from './entities/transit-route.entity';
import { TransitStation } from './entities/transit-station.entity';
import { TransitSupervisor } from './entities/transit-supervisor.entity';
import { TransitBusTrip } from './entities/transit-bus-trip.entity';
import { TransitTicket } from './entities/transit-ticket.entity';
import { TransitController } from './transit.controller';
import { TransitService } from './transit.service';
@Module({
imports: [
TypeOrmModule.forFeature([
TransitRoute,
TransitStation,
TransitSupervisor,
TransitBusTrip,
TransitTicket,
]),
],
controllers: [TransitController],
providers: [TransitService],
exports: [TypeOrmModule, TransitService],
})
export class TransitModule {}
@@ -0,0 +1,74 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TransitRoute } from './entities/transit-route.entity';
import { TransitStation } from './entities/transit-station.entity';
import { TransitSupervisor } from './entities/transit-supervisor.entity';
import { TransitBusTrip } from './entities/transit-bus-trip.entity';
import { TransitTicket } from './entities/transit-ticket.entity';
import { CreateRouteDto, CreateStationDto, CreateSupervisorDto, StartTripDto, BuyTicketDto } from './dto/transit.dto';
@Injectable()
export class TransitService {
constructor(
@InjectRepository(TransitRoute) private routesRepo: Repository<TransitRoute>,
@InjectRepository(TransitStation) private stationsRepo: Repository<TransitStation>,
@InjectRepository(TransitSupervisor) private supervisorsRepo: Repository<TransitSupervisor>,
@InjectRepository(TransitBusTrip) private tripsRepo: Repository<TransitBusTrip>,
@InjectRepository(TransitTicket) private ticketsRepo: Repository<TransitTicket>,
) {}
// Routes
async createRoute(dto: CreateRouteDto) {
const route = this.routesRepo.create(dto);
return this.routesRepo.save(route);
}
async getRoutes(tenant_id: string) {
return this.routesRepo.find({ where: { tenant_id, is_active: true } });
}
// Stations
async createStation(dto: CreateStationDto) {
const station = this.stationsRepo.create(dto);
return this.stationsRepo.save(station);
}
async getStations(route_id: string) {
return this.stationsRepo.find({ where: { route_id }, order: { order_index: 'ASC' } });
}
// Supervisors
async assignSupervisor(dto: CreateSupervisorDto) {
const supervisor = this.supervisorsRepo.create(dto);
return this.supervisorsRepo.save(supervisor);
}
async getSupervisorReports(user_id: string) {
// Basic implementation: find trips and tickets for the supervisor's route
const supervisor = await this.supervisorsRepo.findOne({ where: { user_id } });
if (!supervisor || !supervisor.route_id) throw new NotFoundException('Supervisor or Route not found');
const trips = await this.tripsRepo.count({ where: { route_id: supervisor.route_id } });
// This requires a more complex query in production, but simplified for scaffolding
return { supervisor_id: supervisor.id, route_id: supervisor.route_id, total_trips: trips };
}
// Trips
async startTrip(dto: StartTripDto) {
const trip = this.tripsRepo.create({ ...dto, status: 'active' });
return this.tripsRepo.save(trip);
}
async endTrip(id: string) {
await this.tripsRepo.update(id, { status: 'completed' });
return this.tripsRepo.findOne({ where: { id } });
}
// Tickets
async buyTicket(dto: BuyTicketDto) {
// Future: Call main API to deduct from wallet
const ticket = this.ticketsRepo.create({ ...dto, status: 'active' });
return this.ticketsRepo.save(ticket);
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"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": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}