Refactor business logic for train,schedule,coach,seat and search modules

This commit is contained in:
Roba Boru
2026-05-22 14:47:38 +03:00
parent 9151110fd8
commit 096c717bfa
48 changed files with 2254 additions and 2865 deletions

View File

@@ -10,12 +10,12 @@ export class SeatsController {
constructor(private service: SeatsService) {}
// ── Seat Map ──────────────────────────────────────────────────────────────
@Get('seatmap/:tripId')
@ApiOperation({ summary: 'Get seat map for a trip' })
@ApiParam({ name: 'tripId', description: 'Trip UUID' })
@Get('seatmap/:scheduleId')
@ApiOperation({ summary: 'Get seat map for a schedule' })
@ApiParam({ name: 'scheduleId', description: 'TrainSchedule UUID' })
@ApiQuery({ name: 'coachId', required: false, description: 'Filter by coach UUID' })
@ApiResponse({ status: 200, description: 'Returns coaches with seats and seat class info' })
getSeatMap(@Param('tripId') tripId: string, @Query('coachId') coachId?: string) { return this.service.getSeatMap(tripId, coachId); }
getSeatMap(@Param('scheduleId') scheduleId: string, @Query('coachId') coachId?: string) { return this.service.getSeatMap(scheduleId, coachId); }
// ── Hold / Release ────────────────────────────────────────────────────────
@Post('hold')
@@ -33,10 +33,10 @@ export class SeatsController {
@ApiResponse({ status: 404, description: 'Hold not found' })
releaseHold(@Param('holdId') holdId: string) { return this.service.releaseHold(holdId); }
@Get('export/csv/:tripId') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Export seats as CSV' })
async exportCSV(@Param('tripId') tripId: string) {
const csv = await this.service.exportSeatsCSV(tripId);
return { csv, filename: `seats-${tripId}.csv` };
@Get('export/csv/:scheduleId') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Export seats as CSV' })
async exportCSV(@Param('scheduleId') scheduleId: string) {
const csv = await this.service.exportSeatsCSV(scheduleId);
return { csv, filename: `seats-${scheduleId}.csv` };
}
@Post('import/preview') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Preview CSV import' })
@@ -45,7 +45,7 @@ export class SeatsController {
}
@Post('import/commit') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Commit CSV import' })
importCSV(@Body() body: { tripId: string; csv: string; commit: boolean }) {
return this.service.importSeatsCSV(body.tripId, body.csv, body.commit);
importCSV(@Body() body: { scheduleId: string; csv: string; commit: boolean }) {
return this.service.importSeatsCSV(body.scheduleId, body.csv, body.commit);
}
}

View File

@@ -2,7 +2,7 @@ import { IsString, IsArray, IsOptional } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class HoldSeatsDto {
@ApiProperty({ example: 'trip-uuid' }) @IsString() tripId: string;
@ApiProperty({ example: 'schedule-uuid' }) @IsString() scheduleId: string;
@ApiProperty({ example: 'passenger-uuid' }) @IsString() passengerId: string;
@ApiProperty({ type: [String], example: ['seat-uuid-1', 'seat-uuid-2'] }) @IsArray() seatIds: string[];
@ApiPropertyOptional({ example: 'fare-quote-uuid' }) @IsOptional() @IsString() fareQuoteId?: string;

View File

@@ -8,14 +8,20 @@ export class SeatsService {
constructor(private prisma: PrismaService) {}
// ── Seat Map ──────────────────────────────────────────────────────────────
async getSeatMap(tripId: string, coachId?: string) {
const coaches = await this.prisma.coach.findMany({ where: { tripId, ...(coachId ? { id: coachId } : {}) }, include: { seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] } } });
async getSeatMap(scheduleId: string, coachId?: string) {
const assignments = await this.prisma.coachAssignment.findMany({
where: { scheduleId, ...(coachId ? { coachId } : {}) },
include: { coach: { include: { seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] }, seatClass: true } } },
orderBy: { positionNumber: 'asc' },
});
return {
coaches: coaches.map((coach) => ({
id: coach.id,
name: `Coach ${coach.label}`,
serviceClass: coach.serviceClass,
seats: coach.seats.map((s) => ({ id: s.id, number: s.label, status: s.status, kind: s.kind })),
coaches: assignments.map((a) => ({
id: a.coach.id,
assignmentId: a.id,
name: `Coach ${a.coach.label}`,
seatClass: a.coach.seatClass.name,
positionNumber: a.positionNumber,
seats: a.coach.seats.map((s) => ({ id: s.id, number: s.label, status: s.status, kind: s.kind })),
})),
};
}
@@ -28,9 +34,9 @@ export class SeatsService {
const unavailable = seats.filter((s) => s.status === 'BOOKED' || s.status === 'BLOCKED' || (s.status === 'HELD' && s.heldUntil && s.heldUntil > new Date()));
if (unavailable.length > 0) throw new ConflictException('One or more seats unavailable');
await tx.seat.updateMany({ where: { id: { in: dto.seatIds } }, data: { status: 'HELD', heldUntil: expiresAt } });
return tx.seatHold.create({ data: { tripId: dto.tripId, passengerId: dto.passengerId, seatIds: dto.seatIds, fareQuoteId: dto.fareQuoteId, expiresAt } });
return tx.seatHold.create({ data: { scheduleId: dto.scheduleId, passengerId: dto.passengerId, seatIds: dto.seatIds, fareQuoteId: dto.fareQuoteId, expiresAt } });
});
return { id: hold.id, tripId: dto.tripId, seatIds: dto.seatIds, expiresAt };
return { id: hold.id, scheduleId: dto.scheduleId, seatIds: dto.seatIds, expiresAt };
}
async releaseHold(holdId: string) {
@@ -44,10 +50,10 @@ export class SeatsService {
async confirmSeats(seatIds: string[]) { await this.prisma.seat.updateMany({ where: { id: { in: seatIds } }, data: { status: 'BOOKED', heldUntil: null } }); }
async releaseSeats(seatIds: string[]) { await this.prisma.seat.updateMany({ where: { id: { in: seatIds } }, data: { status: 'AVAILABLE', heldUntil: null } }); }
async autoAssignSeats(tripId: string, count: number, serviceClass: string, eligibility?: string): Promise<string[]> {
async autoAssignSeats(scheduleId: string, count: number, seatClassName: string, eligibility?: string): Promise<string[]> {
const seats = await this.prisma.seat.findMany({
where: {
coach: { tripId, serviceClass: serviceClass as any },
coach: { seatClass: { name: seatClassName }, assignments: { some: { scheduleId } } },
status: 'AVAILABLE',
...(eligibility ? { eligibility } : {}),
},
@@ -81,18 +87,15 @@ export class SeatsService {
return seats.slice(0, count);
}
async exportSeatsCSV(tripId: string): Promise<string> {
const coaches = await this.prisma.coach.findMany({
where: { tripId },
include: { seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] } },
async exportSeatsCSV(scheduleId: string): Promise<string> {
const assignments = await this.prisma.coachAssignment.findMany({
where: { scheduleId },
include: { coach: { include: { seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] } } } },
});
const rows = ['coachId,coachLabel,row,col,label,kind,status,premiumFeeMinor,eligibility'];
for (const coach of coaches) {
for (const seat of coach.seats) {
rows.push(
`${coach.id},${coach.label},${seat.row},${seat.col},${seat.label},${seat.kind},${seat.status},${seat.premiumFeeMinor},${seat.eligibility || ''}`,
);
for (const a of assignments) {
for (const seat of a.coach.seats) {
rows.push(`${a.coach.id},${a.coach.label},${seat.row},${seat.col},${seat.label},${seat.kind},${seat.status},${seat.premiumFeeMinor},${seat.eligibility || ''}`);
}
}
return rows.join('\n');
@@ -123,7 +126,7 @@ export class SeatsService {
return { valid, invalid, errors: errors.slice(0, 10) };
}
async importSeatsCSV(tripId: string, csvContent: string, commit: boolean): Promise<{ imported: number; errors: string[] }> {
async importSeatsCSV(scheduleId: string, csvContent: string, commit: boolean): Promise<{ imported: number; errors: string[] }> {
const lines = csvContent.trim().split('\n').slice(1);
const errors: string[] = [];
let imported = 0;