mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Refactor seat class handling
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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<number> {
|
||||
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<number> {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 } })),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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) {}
|
||||
@@ -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 {}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 })),
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user