mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
Generate ticket, dashboard, excess luggage rate updates
This commit is contained in:
@@ -9,6 +9,19 @@ import { PassengerAdmin } from '../../common/passenger-guards';
|
||||
export class TicketsController {
|
||||
constructor(private service: TicketsService) {}
|
||||
|
||||
@Post('smart-assign/:bookingId')
|
||||
@PassengerAdmin()
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
@ApiOperation({
|
||||
summary: 'Smart seat assignment + ticket generation',
|
||||
description:
|
||||
'Keeps original seats if still free, auto-reassigns to an available seat of the same coach type if taken, ' +
|
||||
'or throws 409 if the schedule is fully booked in that class.',
|
||||
})
|
||||
smartAssignAndGenerate(@Param('bookingId') bookingId: string) {
|
||||
return this.service.smartAssignAndGenerate(bookingId);
|
||||
}
|
||||
|
||||
@Post('generate/:bookingId')
|
||||
@PassengerAdmin()
|
||||
@ApiBearerAuth('IAM-auth')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, NotFoundException, BadRequestException, HttpException, HttpStatus, Logger } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException, BadRequestException, ConflictException, HttpException, HttpStatus, Logger } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { PrismaService } from '../../common/prisma.service';
|
||||
@@ -174,6 +174,109 @@ export class TicketsService {
|
||||
};
|
||||
}
|
||||
|
||||
// Smart seat assignment for conflict resolution:
|
||||
// 1. If the original seat is still free → keep it and generate
|
||||
// 2. If the original seat is taken → find a truly available seat in the same coach type
|
||||
// (excludes: confirmed/boarded bookings, active holds, seat blocks, BOOKED/HELD/REMOVED status)
|
||||
// 3. If no seats of that class remain → throw so the agent is notified
|
||||
async smartAssignAndGenerate(bookingId: string) {
|
||||
const booking = await this.prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: {
|
||||
seats: {
|
||||
include: {
|
||||
seat: { include: { coach: { include: { coachType: true } } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!booking) throw new NotFoundException(`Booking ${bookingId} not found`);
|
||||
|
||||
// Seats taken by other confirmed/boarded bookings on this schedule
|
||||
const takenByOthers = await this.prisma.bookingSeat.findMany({
|
||||
where: {
|
||||
booking: { id: { not: bookingId }, status: { in: ['CONFIRMED', 'BOARDED'] } },
|
||||
seat: { coach: { assignments: { some: { scheduleId: booking.scheduleId } } } },
|
||||
},
|
||||
select: { seatId: true },
|
||||
}).then(rows => new Set(rows.map(r => r.seatId)));
|
||||
|
||||
// Seats held by any active SeatHold (not yet expired)
|
||||
const heldSeatIds = await this.prisma.seatHold.findMany({
|
||||
where: { expiresAt: { gt: new Date() } },
|
||||
select: { seatIds: true },
|
||||
}).then(rows => new Set(rows.flatMap(r => r.seatIds)));
|
||||
|
||||
// Seats with an active SeatBlock
|
||||
const blockedSeatIds = await this.prisma.seatBlock.findMany({
|
||||
select: { seatId: true },
|
||||
}).then(rows => new Set(rows.map(r => r.seatId)));
|
||||
|
||||
// Union of all unavailable seat IDs (excluding the booking's own seats)
|
||||
const ownSeatIds = new Set((booking as any).seats.map((bs: any) => bs.seatId as string));
|
||||
const unavailableIds = new Set([
|
||||
...[...takenByOthers].filter(id => !ownSeatIds.has(id)),
|
||||
...[...heldSeatIds],
|
||||
...[...blockedSeatIds],
|
||||
]);
|
||||
|
||||
const reassigned: { seatNumber: string; newSeatNumber: string }[] = [];
|
||||
|
||||
for (const bs of (booking as any).seats) {
|
||||
const originalSeatId: string = bs.seatId;
|
||||
|
||||
// Case 1: original seat is still free — nothing to do
|
||||
if (!takenByOthers.has(originalSeatId) && !heldSeatIds.has(originalSeatId) && !blockedSeatIds.has(originalSeatId)) continue;
|
||||
|
||||
// Case 2: original seat is unavailable — find a truly available seat in the same coach type
|
||||
const coachTypeId: string | undefined = bs.seat?.coach?.coachTypeId;
|
||||
|
||||
const candidate = await this.prisma.seat.findFirst({
|
||||
where: {
|
||||
status: 'AVAILABLE',
|
||||
seatNumber: { not: '' },
|
||||
NOT: [
|
||||
{ seatNumber: { startsWith: '-' } },
|
||||
{ id: { in: [...unavailableIds] } },
|
||||
],
|
||||
coach: {
|
||||
assignments: { some: { scheduleId: booking.scheduleId } },
|
||||
...(coachTypeId ? { coachTypeId } : {}),
|
||||
},
|
||||
},
|
||||
orderBy: [{ coach: { number: 'asc' } }, { row: 'asc' }, { col: 'asc' }],
|
||||
});
|
||||
|
||||
// Case 3: no seats left in that class
|
||||
if (!candidate) {
|
||||
const className = bs.seat?.coach?.coachType?.name ?? 'the same class';
|
||||
throw new ConflictException(
|
||||
`No available seats remaining in ${className} on this schedule. Please contact the passenger to arrange an alternative.`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.prisma.bookingSeat.update({
|
||||
where: { id: bs.id },
|
||||
data: { seatId: candidate.id },
|
||||
});
|
||||
|
||||
// Mark the newly assigned seat as taken so subsequent passengers in the
|
||||
// same booking don't get assigned the same seat.
|
||||
unavailableIds.add(candidate.id);
|
||||
|
||||
reassigned.push({ seatNumber: bs.seat.seatNumber, newSeatNumber: candidate.seatNumber });
|
||||
}
|
||||
|
||||
await this.auditService.log({
|
||||
action: 'UPDATE',
|
||||
entityType: 'Booking',
|
||||
entityId: bookingId,
|
||||
newData: { smartReassigned: true, changes: reassigned },
|
||||
});
|
||||
|
||||
return this.generate(bookingId);
|
||||
}
|
||||
|
||||
async generate(bookingId: string) {
|
||||
if (!bookingId) throw new BadRequestException('Booking ID is required');
|
||||
|
||||
@@ -231,11 +334,29 @@ export class TicketsService {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for seat conflicts before deleting existing tickets or issuing new ones
|
||||
const seatIds = (booking as any).seats.map((bs: any) => bs.seatId);
|
||||
const conflictingSeats = await this.prisma.bookingSeat.findMany({
|
||||
where: {
|
||||
seatId: { in: seatIds },
|
||||
booking: {
|
||||
id: { not: bookingId },
|
||||
status: { in: ['CONFIRMED', 'BOARDED'] },
|
||||
},
|
||||
},
|
||||
include: { seat: true },
|
||||
});
|
||||
if (conflictingSeats.length > 0) {
|
||||
const labels = [...new Set(conflictingSeats.map((s: any) => s.seat.seatNumber))].join(', ');
|
||||
throw new ConflictException(
|
||||
`Seat(s) ${labels} are already confirmed for another booking.`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.prisma.ticket.deleteMany({ where: { bookingId } });
|
||||
|
||||
// Generate one ticket per unique passenger (grouped by passengerName)
|
||||
const tickets = [];
|
||||
const seatIds = (booking as any).seats.map((bs: any) => bs.seatId);
|
||||
|
||||
// Group seats by passenger
|
||||
const passengerSeatsMap = new Map<string, any[]>();
|
||||
|
||||
Reference in New Issue
Block a user