From 9151110fd882c345d0e21a74667ed087262cb8f7 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Thu, 21 May 2026 14:31:07 +0300 Subject: [PATCH] Refactor seat class handling --- apps/edr-passenger-api/src/app.module.ts | 2 - .../common/filters/http-exception.filter.ts | 3 +- .../src/modules/bookings/bookings.service.ts | 12 ++---- .../src/modules/fleet/fleet.dto.ts | 10 +++-- .../src/modules/fleet/fleet.service.ts | 36 ++++++++++++----- .../modules/passengers/passengers.service.ts | 2 +- .../src/modules/schedules/schedules.dto.ts | 5 ++- .../modules/schedules/schedules.service.ts | 6 +-- .../seat-classes/seat-classes.controller.ts | 40 ------------------- .../modules/seat-classes/seat-classes.dto.ts | 24 ----------- .../seat-classes/seat-classes.module.ts | 6 --- .../seat-classes/seat-classes.service.ts | 40 ------------------- .../src/modules/seats/seats.service.ts | 4 +- .../segments/enhanced-seats.service.ts | 2 +- 14 files changed, 49 insertions(+), 143 deletions(-) delete mode 100644 apps/edr-passenger-api/src/modules/seat-classes/seat-classes.controller.ts delete mode 100644 apps/edr-passenger-api/src/modules/seat-classes/seat-classes.dto.ts delete mode 100644 apps/edr-passenger-api/src/modules/seat-classes/seat-classes.module.ts delete mode 100644 apps/edr-passenger-api/src/modules/seat-classes/seat-classes.service.ts diff --git a/apps/edr-passenger-api/src/app.module.ts b/apps/edr-passenger-api/src/app.module.ts index 7b2f460d3..e5c62a2bb 100644 --- a/apps/edr-passenger-api/src/app.module.ts +++ b/apps/edr-passenger-api/src/app.module.ts @@ -33,7 +33,6 @@ import { SegmentsModule } from './modules/segments/segments.module'; import { AgentsModule } from './modules/agents/agents.module'; import { ReportsModule } from './modules/reports/reports.module'; import { FraudModule } from './modules/fraud/fraud.module'; -import { SeatClassesModule } from './modules/seat-classes/seat-classes.module'; @Module({ imports: [ @@ -67,7 +66,6 @@ import { SeatClassesModule } from './modules/seat-classes/seat-classes.module'; AgentsModule, ReportsModule, FraudModule, - SeatClassesModule, ], }) export class AppModule implements NestModule { diff --git a/apps/edr-passenger-api/src/common/filters/http-exception.filter.ts b/apps/edr-passenger-api/src/common/filters/http-exception.filter.ts index d1f73bf71..f02ce4abf 100644 --- a/apps/edr-passenger-api/src/common/filters/http-exception.filter.ts +++ b/apps/edr-passenger-api/src/common/filters/http-exception.filter.ts @@ -34,8 +34,9 @@ export class HttpExceptionFilter implements ExceptionFilter { if (status >= 500) { this.logger.error( `${request.method} ${request.url} -> ${status}`, - (exception as Error)?.stack, + exception instanceof Error ? exception.stack : JSON.stringify(exception), ); + console.error('Full error details:', exception); } else { this.logger.warn(`${request.method} ${request.url} -> ${status} ${message}`); } diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts index e44492f58..5d99717d4 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts @@ -92,7 +92,7 @@ export class BookingsService { } // Calculate fare with age-based pricing - const baseFareMinor = await this.getBaseFare(dto.tripId, dto.serviceClass, dto.seatClassId); + const baseFareMinor = await this.getBaseFare(dto.tripId, dto.serviceClass); const adultFareMinor = baseFareMinor * adultCount; const paidChildrenCount = Math.max(0, childCount - 1); const childFareMinor = baseFareMinor * paidChildrenCount; @@ -176,13 +176,7 @@ export class BookingsService { }; } - private async getBaseFare(tripId: string, serviceClass: string, seatClassId?: string): Promise { - if (seatClassId) { - const fareRule = await this.prisma.fareRule.findFirst({ - where: { tripId, seatClassId }, - }); - if (fareRule) return fareRule.baseFareMinor; - } + private async getBaseFare(tripId: string, serviceClass: string): Promise { const fareRule = await this.prisma.fareRule.findFirst({ where: { tripId, serviceClass: serviceClass as any }, }); @@ -214,7 +208,7 @@ export class BookingsService { fullName: bs.passengerName, category: bs.passengerCategory, verifaydaVerified: bs.verifaydaVerified, - seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.serviceClass, seatClassId: bs.seat.coach.seatClassId }, + seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.serviceClass }, })), payment: booking.paymentIntent ? { method: booking.paymentIntent.method, status: booking.paymentIntent.status } : undefined, }; diff --git a/apps/edr-passenger-api/src/modules/fleet/fleet.dto.ts b/apps/edr-passenger-api/src/modules/fleet/fleet.dto.ts index 5aeadeae7..8f3b12d4f 100644 --- a/apps/edr-passenger-api/src/modules/fleet/fleet.dto.ts +++ b/apps/edr-passenger-api/src/modules/fleet/fleet.dto.ts @@ -1,5 +1,6 @@ -import { IsString, IsInt, IsOptional } from 'class-validator'; +import { IsString, IsInt, IsOptional, IsEnum, IsArray } from 'class-validator'; import { ApiProperty, ApiPropertyOptional, PartialType, OmitType } from '@nestjs/swagger'; +import { ServiceClass } from '@prisma/client'; export class CreateTrainServiceDto { @ApiProperty({ example: '301' }) @IsString() number: string; @@ -9,7 +10,10 @@ export class CreateTrainServiceDto { export class CreateCoachDto { @ApiProperty({ example: 'trip-uuid' }) @IsString() tripId: string; @ApiProperty({ example: 'A' }) @IsString() label: string; - @ApiProperty({ example: 'seat-class-uuid' }) @IsString() seatClassId: string; + @ApiProperty({ enum: ServiceClass, example: 'ECONOMY_REGULAR' }) @IsEnum(ServiceClass) serviceClass: ServiceClass; + @ApiPropertyOptional({ example: 'seat-class-uuid' }) @IsOptional() @IsString() seatClassId?: string; + @ApiPropertyOptional({ example: 60 }) @IsOptional() @IsInt() capacity?: number; + @ApiPropertyOptional({ example: 1 }) @IsOptional() @IsInt() sequence?: number; } export class UpdateCoachDto extends PartialType(OmitType(CreateCoachDto, ['tripId'] as const)) {} @@ -17,5 +21,5 @@ export class UpdateCoachDto extends PartialType(OmitType(CreateCoachDto, ['tripI export class CreateSeatBatchDto { @ApiProperty({ example: 'coach-uuid' }) @IsString() coachId: string; @ApiProperty({ example: 10 }) @IsInt() rows: number; - @ApiProperty({ example: ['A', 'B', 'C', 'D'] }) cols: string[]; + @ApiProperty({ example: ['A', 'B', 'C', 'D'], type: [String] }) @IsArray() @IsString({ each: true }) cols: string[]; } diff --git a/apps/edr-passenger-api/src/modules/fleet/fleet.service.ts b/apps/edr-passenger-api/src/modules/fleet/fleet.service.ts index ef6969aaa..71894e807 100644 --- a/apps/edr-passenger-api/src/modules/fleet/fleet.service.ts +++ b/apps/edr-passenger-api/src/modules/fleet/fleet.service.ts @@ -12,26 +12,44 @@ export class FleetService { listCoaches(tripId?: string) { return this.prisma.coach.findMany({ where: tripId ? { tripId } : undefined, - include: { seatClass: { select: { id: true, name: true, basePrice: true } }, _count: { select: { seats: true } } }, + include: { _count: { select: { seats: true } } }, orderBy: { label: 'asc' }, }); } - createCoach(dto: CreateCoachDto) { return this.prisma.coach.create({ data: dto, include: { seatClass: { select: { id: true, name: true, basePrice: true } } } }); } + createCoach(dto: CreateCoachDto) { return this.prisma.coach.create({ data: dto }); } async updateCoach(id: string, dto: UpdateCoachDto) { const coach = await this.prisma.coach.findUnique({ where: { id } }); if (!coach) throw new NotFoundException('Coach not found'); - return this.prisma.coach.update({ where: { id }, data: dto, include: { seatClass: { select: { id: true, name: true, basePrice: true } } } }); + return this.prisma.coach.update({ where: { id }, data: dto }); } async createSeatBatch(dto: CreateSeatBatchDto) { - const coach = await this.prisma.coach.findUnique({ where: { id: dto.coachId } }); - if (!coach) throw new NotFoundException('Coach not found'); - const seats = []; - for (let row = 1; row <= dto.rows; row++) for (const col of dto.cols) seats.push({ coachId: dto.coachId, row, col, label: `${row}${col}` }); - await this.prisma.seat.createMany({ data: seats, skipDuplicates: true }); - return { created: seats.length }; + try { + const coach = await this.prisma.coach.findUnique({ where: { id: dto.coachId } }); + if (!coach) throw new NotFoundException('Coach not found'); + + const seats = []; + for (let row = 1; row <= dto.rows; row++) { + for (const col of dto.cols) { + const seatNumber = `${coach.label}${row}${col}`; + seats.push({ + coachId: dto.coachId, + row, + col, + label: `${row}${col}`, + seatNumber + }); + } + } + + await this.prisma.seat.createMany({ data: seats, skipDuplicates: true }); + return { created: seats.length }; + } catch (error) { + console.error('Error in createSeatBatch:', error); + throw error; + } } async getAnalytics() { diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts index c1121fbed..54222751e 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.service.ts @@ -30,7 +30,7 @@ export class PassengersService { destination: { id: b.trip.destinationStation.id, name: b.trip.destinationStation.name, code: b.trip.destinationStation.code, city: b.trip.destinationStation.city }, departureAt: b.trip.departureAt, }, - passengers: b.seats.map((bs) => ({ fullName: bs.passengerName, seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.seatClassId } })), + passengers: b.seats.map((bs) => ({ fullName: bs.passengerName, seat: { number: bs.seat.label, coach: bs.seat.coach.label, class: bs.seat.coach.serviceClass } })), })), }; } diff --git a/apps/edr-passenger-api/src/modules/schedules/schedules.dto.ts b/apps/edr-passenger-api/src/modules/schedules/schedules.dto.ts index b00aaa906..3d1401cfe 100644 --- a/apps/edr-passenger-api/src/modules/schedules/schedules.dto.ts +++ b/apps/edr-passenger-api/src/modules/schedules/schedules.dto.ts @@ -1,5 +1,6 @@ -import { IsString, IsDateString, IsInt, IsOptional } from 'class-validator'; +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; @@ -13,7 +14,7 @@ export class CreateTripDto { export class CreateFareRuleDto { @ApiPropertyOptional() @IsOptional() @IsString() tripId?: string; @ApiPropertyOptional() @IsOptional() @IsString() route?: string; - @ApiProperty({ example: 'seat-class-uuid' }) @IsString() seatClassId: string; + @ApiProperty({ enum: ServiceClass, example: 'ECONOMY_REGULAR' }) @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; diff --git a/apps/edr-passenger-api/src/modules/schedules/schedules.service.ts b/apps/edr-passenger-api/src/modules/schedules/schedules.service.ts index 062e7f0df..a97b54fd6 100644 --- a/apps/edr-passenger-api/src/modules/schedules/schedules.service.ts +++ b/apps/edr-passenger-api/src/modules/schedules/schedules.service.ts @@ -26,14 +26,14 @@ export class SchedulesService { return this.prisma.fareRule.create({ data: { ...dto, validFrom: new Date(dto.validFrom), validUntil: dto.validUntil ? new Date(dto.validUntil) : null } }); } - async getFare(tripId: string, seatClassId: string) { + 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: { seatClassId, validFrom: { lte: new Date() }, OR: [{ tripId }, { route }, { tripId: null, route: null }], AND: [{ OR: [{ validUntil: null }, { validUntil: { gte: new Date() } }] }] }, + 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', seatClassId }; + return rule ?? { baseFareMinor: 45000, currency: 'ETB', serviceClass }; } } diff --git a/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.controller.ts b/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.controller.ts deleted file mode 100644 index 945834ac0..000000000 --- a/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.controller.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Body, Controller, Get, Param, Patch, Post, UseGuards } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiResponse, ApiBody } from '@nestjs/swagger'; -import { SeatClassesService } from './seat-classes.service'; -import { CreateSeatClassDto, UpdateSeatClassDto } from './seat-classes.dto'; -import { JwtGuard } from '../../common/jwt.guard'; - -@ApiTags('Seat Classes') -@Controller('seat-classes') -export class SeatClassesController { - constructor(private service: SeatClassesService) {} - - @Get() - @ApiOperation({ summary: 'List all seat classes' }) - @ApiResponse({ status: 200, description: 'Returns all seat classes with their coaches' }) - listSeatClasses() { return this.service.listSeatClasses(); } - - @Get(':id') - @ApiOperation({ summary: 'Get a seat class by ID' }) - @ApiParam({ name: 'id', description: 'Seat class UUID' }) - @ApiResponse({ status: 200, description: 'Returns seat class with its coaches' }) - @ApiResponse({ status: 404, description: 'Seat class not found' }) - getSeatClass(@Param('id') id: string) { return this.service.getSeatClass(id); } - - @Post() - @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') - @ApiOperation({ summary: 'Create a seat class' }) - @ApiBody({ type: CreateSeatClassDto }) - @ApiResponse({ status: 201, description: 'Seat class created' }) - @ApiResponse({ status: 409, description: 'Seat class name already exists' }) - createSeatClass(@Body() dto: CreateSeatClassDto) { return this.service.createSeatClass(dto); } - - @Patch(':id') - @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') - @ApiOperation({ summary: 'Update a seat class' }) - @ApiParam({ name: 'id', description: 'Seat class UUID' }) - @ApiBody({ type: UpdateSeatClassDto }) - @ApiResponse({ status: 200, description: 'Seat class updated' }) - @ApiResponse({ status: 404, description: 'Seat class not found' }) - updateSeatClass(@Param('id') id: string, @Body() dto: UpdateSeatClassDto) { return this.service.updateSeatClass(id, dto); } -} diff --git a/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.dto.ts b/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.dto.ts deleted file mode 100644 index d12fe0fb6..000000000 --- a/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IsString, IsInt, IsBoolean, IsOptional } from 'class-validator'; -import { ApiProperty, ApiPropertyOptional, PartialType } from '@nestjs/swagger'; - -export class CreateSeatClassDto { - @ApiProperty({ example: 'Economy Seat' }) - @IsString() - name: string; - - @ApiPropertyOptional({ example: 'Standard economy seating' }) - @IsOptional() - @IsString() - description?: string; - - @ApiProperty({ example: 45000, description: 'Base price in minor currency units' }) - @IsInt() - basePrice: number; - - @ApiPropertyOptional({ example: true }) - @IsOptional() - @IsBoolean() - isActive?: boolean; -} - -export class UpdateSeatClassDto extends PartialType(CreateSeatClassDto) {} diff --git a/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.module.ts b/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.module.ts deleted file mode 100644 index a7e8648e1..000000000 --- a/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.module.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Module } from '@nestjs/common'; -import { SeatClassesController } from './seat-classes.controller'; -import { SeatClassesService } from './seat-classes.service'; - -@Module({ controllers: [SeatClassesController], providers: [SeatClassesService], exports: [SeatClassesService] }) -export class SeatClassesModule {} diff --git a/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.service.ts b/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.service.ts deleted file mode 100644 index 2e3998651..000000000 --- a/apps/edr-passenger-api/src/modules/seat-classes/seat-classes.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; -import { PrismaService } from '../../common/prisma.service'; -import { CreateSeatClassDto, UpdateSeatClassDto } from './seat-classes.dto'; - -@Injectable() -export class SeatClassesService { - constructor(private prisma: PrismaService) {} - - private readonly coachInclude = { - coaches: { - select: { id: true, label: true, tripId: true, _count: { select: { seats: true } } }, - orderBy: { label: 'asc' as const }, - }, - }; - - listSeatClasses() { - return this.prisma.seatClass.findMany({ orderBy: { createdAt: 'asc' }, include: this.coachInclude }); - } - - async getSeatClass(id: string) { - const sc = await this.prisma.seatClass.findUnique({ where: { id }, include: this.coachInclude }); - if (!sc) throw new NotFoundException('SeatClass not found'); - return sc; - } - - async createSeatClass(dto: CreateSeatClassDto) { - try { - return await this.prisma.seatClass.create({ data: dto }); - } catch (e: any) { - if (e.code === 'P2002') throw new ConflictException(`Seat class "${dto.name}" already exists`); - throw e; - } - } - - async updateSeatClass(id: string, dto: UpdateSeatClassDto) { - const sc = await this.prisma.seatClass.findUnique({ where: { id } }); - if (!sc) throw new NotFoundException('SeatClass not found'); - return this.prisma.seatClass.update({ where: { id }, data: dto, include: this.coachInclude }); - } -} diff --git a/apps/edr-passenger-api/src/modules/seats/seats.service.ts b/apps/edr-passenger-api/src/modules/seats/seats.service.ts index 0edef4ddf..3593c38d1 100644 --- a/apps/edr-passenger-api/src/modules/seats/seats.service.ts +++ b/apps/edr-passenger-api/src/modules/seats/seats.service.ts @@ -9,12 +9,12 @@ export class SeatsService { // ── Seat Map ────────────────────────────────────────────────────────────── async getSeatMap(tripId: string, coachId?: string) { - const coaches = await this.prisma.coach.findMany({ where: { tripId, ...(coachId ? { id: coachId } : {}) }, include: { seatClass: true, seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] } } }); + const coaches = await this.prisma.coach.findMany({ where: { tripId, ...(coachId ? { id: coachId } : {}) }, include: { seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] } } }); return { coaches: coaches.map((coach) => ({ id: coach.id, name: `Coach ${coach.label}`, - seatClass: { id: coach.seatClass.id, name: coach.seatClass.name, basePrice: coach.seatClass.basePrice }, + serviceClass: coach.serviceClass, seats: coach.seats.map((s) => ({ id: s.id, number: s.label, status: s.status, kind: s.kind })), })), }; diff --git a/apps/edr-passenger-api/src/modules/segments/enhanced-seats.service.ts b/apps/edr-passenger-api/src/modules/segments/enhanced-seats.service.ts index e7213cf6c..57b6f2d2f 100644 --- a/apps/edr-passenger-api/src/modules/segments/enhanced-seats.service.ts +++ b/apps/edr-passenger-api/src/modules/segments/enhanced-seats.service.ts @@ -353,7 +353,7 @@ export class EnhancedSeatsService { id: seat.id, label: seat.label, coach: coach.label, - serviceClass: coach.seatClassId, + serviceClass: coach.serviceClass, row: seat.row, col: seat.col });