Initial commit of edr-passenger-api alpha version

This commit is contained in:
Stephanos A
2026-05-13 16:58:49 +03:00
parent 199a3eba11
commit 39ba561d8f
113 changed files with 3602 additions and 1035 deletions

View File

@@ -1,35 +0,0 @@
import { Passenger } from "@edr/types";
import {
IsDateString,
IsEnum,
IsNumber,
IsOptional,
IsString,
IsUUID,
Min,
} from "class-validator";
export class CreateScheduleDto {
@IsString()
trainCode!: string;
@IsUUID()
originStationId!: string;
@IsUUID()
destinationStationId!: string;
@IsDateString()
departureTime!: string;
@IsDateString()
arrivalTime!: string;
@IsNumber()
@Min(0)
basePrice!: number;
@IsOptional()
@IsEnum(Passenger.ScheduleStatus)
status?: Passenger.ScheduleStatus;
}

View File

@@ -1,32 +0,0 @@
import { BaseEntity } from "@edr/api-common";
import { Passenger } from "@edr/types";
import { Column, Entity } from "typeorm";
@Entity({ name: "schedules" })
export class Schedule extends BaseEntity {
@Column({ name: "train_code", type: "varchar", length: 32 })
trainCode!: string;
@Column({ name: "origin_station_id", type: "uuid" })
originStationId!: string;
@Column({ name: "destination_station_id", type: "uuid" })
destinationStationId!: string;
@Column({ name: "departure_time", type: "timestamptz" })
departureTime!: Date;
@Column({ name: "arrival_time", type: "timestamptz" })
arrivalTime!: Date;
@Column({
name: "status",
type: "enum",
enum: Passenger.ScheduleStatus,
default: Passenger.ScheduleStatus.Scheduled,
})
status!: Passenger.ScheduleStatus;
@Column({ name: "base_price", type: "numeric", precision: 10, scale: 2 })
basePrice!: number;
}

View File

@@ -1,37 +1,21 @@
import {
Body,
Controller,
Get,
Param,
ParseUUIDPipe,
Post,
} from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { SchedulesService } from './schedules.service';
import { CreateTripDto, CreateFareRuleDto, UpdateTripStatusDto } from './schedules.dto';
import { JwtGuard } from '../../common/jwt.guard';
import { CreateScheduleDto } from "./dto/create-schedule.dto";
import { SchedulesService } from "./schedules.service";
@ApiTags("schedules")
// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth
@Controller("schedules")
@ApiTags('Schedule')
@Controller('schedule')
export class SchedulesController {
constructor(private readonly schedulesService: SchedulesService) {}
@Post()
@ApiOperation({ summary: "Publish a new train schedule" })
create(@Body() dto: CreateScheduleDto) {
return this.schedulesService.create(dto);
}
@Get()
@ApiOperation({ summary: "List all schedules" })
findAll() {
return this.schedulesService.findAll();
}
@Get(":id")
@ApiOperation({ summary: "Get a schedule by ID" })
findOne(@Param("id", ParseUUIDPipe) id: string) {
return this.schedulesService.findById(id);
}
constructor(private service: SchedulesService) {}
@Post('trips') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Create trip' })
createTrip(@Body() dto: CreateTripDto) { return this.service.createTrip(dto); }
@Get('trips/:id') @ApiOperation({ summary: 'Get trip details' })
getTrip(@Param('id') id: string) { return this.service.getTrip(id); }
@Patch('trips/:id/status') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Update trip status' })
updateStatus(@Param('id') id: string, @Body() dto: UpdateTripStatusDto) { return this.service.updateTripStatus(id, dto); }
@Post('fares') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Create fare rule' })
createFareRule(@Body() dto: CreateFareRuleDto) { return this.service.createFareRule(dto); }
@Get('fares/:tripId') @ApiOperation({ summary: 'Get fare for trip and class' })
getFare(@Param('tripId') tripId: string, @Query('class') cls: string) { return this.service.getFare(tripId, cls ?? 'ECONOMY'); }
}

View File

@@ -0,0 +1,25 @@
import { IsString, IsDateString, IsInt, IsOptional, IsEnum } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ServiceClass } from '@prisma/client';
export class CreateTripDto {
@ApiProperty() @IsString() serviceId: string;
@ApiProperty() @IsString() originStationId: string;
@ApiProperty() @IsString() destinationStationId: string;
@ApiProperty({ example: '2026-05-11T08:30:00Z' }) @IsDateString() departureAt: string;
@ApiProperty({ example: '2026-05-11T20:00:00Z' }) @IsDateString() arrivalAt: string;
@ApiPropertyOptional() @IsOptional() @IsInt() stopsCount?: number;
}
export class CreateFareRuleDto {
@ApiPropertyOptional() @IsOptional() @IsString() tripId?: string;
@ApiPropertyOptional() @IsOptional() @IsString() route?: string;
@ApiProperty({ enum: ServiceClass }) @IsEnum(ServiceClass) serviceClass: ServiceClass;
@ApiProperty({ example: 45000 }) @IsInt() baseFareMinor: number;
@ApiProperty({ example: '2026-01-01T00:00:00Z' }) @IsDateString() validFrom: string;
@ApiPropertyOptional() @IsOptional() @IsDateString() validUntil?: string;
}
export class UpdateTripStatusDto {
@ApiProperty({ example: 'EN_ROUTE' }) @IsString() status: string;
}

View File

@@ -1,15 +1,6 @@
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Module } from '@nestjs/common';
import { SchedulesController } from './schedules.controller';
import { SchedulesService } from './schedules.service';
import { Schedule } from "./entities/schedule.entity";
import { SchedulesController } from "./schedules.controller";
import { SchedulesRepository } from "./schedules.repository";
import { SchedulesService } from "./schedules.service";
@Module({
imports: [TypeOrmModule.forFeature([Schedule])],
controllers: [SchedulesController],
providers: [SchedulesService, SchedulesRepository],
exports: [SchedulesService],
})
@Module({ controllers: [SchedulesController], providers: [SchedulesService] })
export class SchedulesModule {}

View File

@@ -1,16 +0,0 @@
import { BaseRepository } from "@edr/api-common";
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Schedule } from "./entities/schedule.entity";
@Injectable()
export class SchedulesRepository extends BaseRepository<Schedule> {
constructor(
@InjectRepository(Schedule)
repository: Repository<Schedule>,
) {
super(repository);
}
}

View File

@@ -1,35 +1,39 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { CreateScheduleDto } from "./dto/create-schedule.dto";
import { Schedule } from "./entities/schedule.entity";
import { SchedulesRepository } from "./schedules.repository";
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { CreateTripDto, CreateFareRuleDto, UpdateTripStatusDto } from './schedules.dto';
@Injectable()
export class SchedulesService {
constructor(private readonly schedulesRepository: SchedulesRepository) {}
constructor(private prisma: PrismaService) {}
/** Publish a new train schedule. */
create(dto: CreateScheduleDto): Promise<Schedule> {
return this.schedulesRepository.create({
...dto,
departureTime: new Date(dto.departureTime),
arrivalTime: new Date(dto.arrivalTime),
async createTrip(dto: CreateTripDto) {
const dep = new Date(dto.departureAt), arr = new Date(dto.arrivalAt);
return this.prisma.trip.create({
data: { serviceId: dto.serviceId, originStationId: dto.originStationId, destinationStationId: dto.destinationStationId, departureAt: dep, arrivalAt: arr, durationMinutes: Math.round((arr.getTime() - dep.getTime()) / 60000), stopsCount: dto.stopsCount ?? 0 },
include: { service: true, originStation: true, destinationStation: true },
});
}
/** List every published schedule. */
findAll(): Promise<Schedule[]> {
return this.schedulesRepository.findAll({
order: { departureTime: "ASC" },
});
async getTrip(id: string) {
const trip = await this.prisma.trip.findUnique({ where: { id }, include: { service: true, originStation: true, destinationStation: true, coaches: { include: { seats: true } }, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } });
if (!trip) throw new NotFoundException('Trip not found');
return trip;
}
/** Get a single schedule by ID. */
async findById(id: string): Promise<Schedule> {
const schedule = await this.schedulesRepository.findById(id);
if (!schedule) {
throw new NotFoundException(`Schedule ${id} not found`);
}
return schedule;
updateTripStatus(id: string, dto: UpdateTripStatusDto) { return this.prisma.trip.update({ where: { id }, data: { status: dto.status as any } }); }
createFareRule(dto: CreateFareRuleDto) {
return this.prisma.fareRule.create({ data: { ...dto, validFrom: new Date(dto.validFrom), validUntil: dto.validUntil ? new Date(dto.validUntil) : null } });
}
async getFare(tripId: string, serviceClass: string) {
const trip = await this.prisma.trip.findUnique({ where: { id: tripId }, include: { originStation: true, destinationStation: true } });
if (!trip) throw new NotFoundException('Trip not found');
const route = `${trip.originStation.code}-${trip.destinationStation.code}`;
const rule = await this.prisma.fareRule.findFirst({
where: { serviceClass: serviceClass as any, validFrom: { lte: new Date() }, OR: [{ tripId }, { route }, { tripId: null, route: null }], AND: [{ OR: [{ validUntil: null }, { validUntil: { gte: new Date() } }] }] },
orderBy: { validFrom: 'desc' },
});
return rule ?? { baseFareMinor: 45000, currency: 'ETB', serviceClass };
}
}