Files
edr-platform/apps/edr-passenger-api/src/modules/segments/enhanced-seats.service.ts

209 lines
8.5 KiB
TypeScript

import { Injectable, BadRequestException, ConflictException } from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { SegmentsService, Segment } from '../segments/segments.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
export interface SeatHoldRequest {
scheduleId: string;
seatIds: string[];
passengerId: string;
originStationId: string;
destinationStationId: string;
}
export interface BookingConfirmRequest {
holdId: string;
bookingId: string;
}
@Injectable()
export class EnhancedSeatsService {
constructor(
private prisma: PrismaService,
private segmentsService: SegmentsService,
private eventEmitter: EventEmitter2,
) {}
async holdSeats(request: SeatHoldRequest) {
return this.prisma.$transaction(async (tx) => {
const segments = await this.segmentsService.getJourneySegments(
request.scheduleId, request.originStationId, request.destinationStationId,
);
const reqFrom = Math.min(...segments.map(s => s.fromSequence));
const reqTo = Math.max(...segments.map(s => s.toSequence));
for (const seatId of request.seatIds) {
const seat = await tx.seat.findUnique({ where: { id: seatId } });
if (!seat) throw new BadRequestException(`Seat ${seatId} not found`);
if (seat.status === 'BLOCKED') throw new BadRequestException(`Seat ${seat.seatNumber} is blocked`);
const free = await this.segmentsService.isSeatFreeForLeg(
request.scheduleId, seatId, reqFrom, reqTo,
);
if (!free) throw new ConflictException(`Seat ${seat.seatNumber} is not available for the requested leg`);
}
const expiresAt = new Date(Date.now() + 10 * 60 * 1000);
const seatHold = await tx.seatHold.create({
data: {
scheduleId: request.scheduleId,
seatIds: request.seatIds,
passengerId: request.passengerId,
createdBy: JSON.stringify({
originStationId: request.originStationId,
destinationStationId: request.destinationStationId,
}),
expiresAt,
},
});
this.eventEmitter.emit('seats.held', { holdId: seatHold.id, scheduleId: request.scheduleId, seatIds: request.seatIds, segments });
return { holdId: seatHold.id, expiresAt, segments, seats: request.seatIds };
});
}
async confirmBooking(request: BookingConfirmRequest) {
return this.prisma.$transaction(async (tx) => {
const hold = await tx.seatHold.findUnique({ where: { id: request.holdId } });
if (!hold) throw new BadRequestException('Seat hold not found');
if (hold.expiresAt < new Date()) throw new BadRequestException('Seat hold has expired');
const booking = await tx.booking.findUnique({ where: { id: request.bookingId } });
if (!booking) throw new BadRequestException('Booking not found');
const schedule = await tx.trainSchedule.findUnique({
where: { id: hold.scheduleId },
include: { stopTimes: { orderBy: { sequence: 'asc' } } },
});
if (!schedule) throw new BadRequestException('Schedule not found');
let originStationId: string | undefined;
let destinationStationId: string | undefined;
try {
if (hold.createdBy) {
const meta = JSON.parse(hold.createdBy);
originStationId = meta.originStationId;
destinationStationId = meta.destinationStationId;
}
} catch { /* ignore */ }
const originStop = originStationId ? schedule.stopTimes.find((s: any) => s.stationId === originStationId) : undefined;
const destStop = destinationStationId ? schedule.stopTimes.find((s: any) => s.stationId === destinationStationId) : undefined;
const fromSeq = originStop?.sequence ?? schedule.stopTimes[0].sequence;
const toSeq = destStop?.sequence ?? schedule.stopTimes[schedule.stopTimes.length - 1].sequence;
const segments: Segment[] = [];
for (let i = fromSeq; i < toSeq; i++) {
const fromStop = schedule.stopTimes.find((s: any) => s.sequence === i);
const toStop = schedule.stopTimes.find((s: any) => s.sequence === i + 1);
if (fromStop && toStop) {
segments.push({
fromStationId: fromStop.stationId,
toStationId: toStop.stationId,
fromSequence: fromStop.sequence,
toSequence: toStop.sequence,
fromName: '',
toName: '',
});
}
}
const journey = await tx.journey.create({
data: { passengerId: hold.passengerId, status: 'CONFIRMED', totalMinor: booking.totalMinor, currency: booking.currency },
});
for (const seatId of hold.seatIds) {
for (let i = 0; i < segments.length; i++) {
await tx.journeySegment.create({
data: {
journeyId: journey.id,
scheduleId: hold.scheduleId,
segmentOrder: i + 1,
seatId,
departureStationId: segments[i].fromStationId,
arrivalStationId: segments[i].toStationId,
},
});
}
}
await tx.seatHold.delete({ where: { id: request.holdId } });
this.eventEmitter.emit('booking.confirmed', { bookingId: request.bookingId, scheduleId: hold.scheduleId, seatIds: hold.seatIds, segments });
return { bookingId: request.bookingId, confirmedSeats: hold.seatIds, segments };
});
}
async releaseSeats(scheduleId: string, currentStationId: string) {
return this.prisma.$transaction(async (tx) => {
const completedSegments = await tx.journeySegment.findMany({
where: { scheduleId, arrivalStationId: currentStationId },
include: { journey: { include: { journeySegments: { where: { scheduleId } } } } },
});
const seatsToRelease: string[] = [];
for (const segment of completedSegments) {
const allSegments = segment.journey.journeySegments.filter((js: any) => js.seatId === segment.seatId);
const maxSegmentOrder = Math.max(...allSegments.map((js: any) => js.segmentOrder));
if (segment.segmentOrder === maxSegmentOrder) seatsToRelease.push(segment.seatId!);
}
if (seatsToRelease.length > 0) {
await tx.seat.updateMany({ where: { id: { in: seatsToRelease } }, data: { status: 'AVAILABLE' } });
this.eventEmitter.emit('seats.released', { scheduleId, stationId: currentStationId, releasedSeats: seatsToRelease });
}
return { releasedSeats: seatsToRelease, stationId: currentStationId };
});
}
async expireHolds() {
return this.prisma.$transaction(async (tx) => {
const expiredHolds = await tx.seatHold.findMany({ where: { expiresAt: { lt: new Date() } } });
const expiredSeatIds = expiredHolds.flatMap(h => h.seatIds);
if (expiredSeatIds.length > 0) {
await tx.seat.updateMany({ where: { id: { in: expiredSeatIds } }, data: { status: 'AVAILABLE', heldUntil: null } });
await tx.seatHold.deleteMany({ where: { expiresAt: { lt: new Date() } } });
this.eventEmitter.emit('holds.expired', { expiredHolds: expiredHolds.length, releasedSeats: expiredSeatIds });
}
return { expiredHolds: expiredHolds.length, releasedSeats: expiredSeatIds };
});
}
async getSeatAvailability(scheduleId: string, originStationId: string, destinationStationId: string) {
const segments = await this.segmentsService.getJourneySegments(scheduleId, originStationId, destinationStationId);
const reqFrom = Math.min(...segments.map(s => s.fromSequence));
const reqTo = Math.max(...segments.map(s => s.toSequence));
const schedule = await this.prisma.trainSchedule.findUnique({
where: { id: scheduleId },
include: { coachAssignments: { include: { coach: { include: { seats: true } } } } },
});
if (!schedule) throw new BadRequestException('Schedule not found');
const availableSeats = [];
for (const assignment of schedule.coachAssignments) {
for (const seat of assignment.coach.seats) {
if (seat.status === 'BLOCKED') continue;
const free = await this.segmentsService.isSeatFreeForLeg(scheduleId, seat.id, reqFrom, reqTo);
if (free) {
availableSeats.push({
id: seat.id, label: seat.seatNumber,
coach: assignment.coach.number,
seatClass: 'Standard',
row: seat.row, col: seat.col,
kind: seat.kind,
isWindow: seat.isWindow,
isAisle: seat.isAisle,
bedPosition: seat.bedPosition,
});
}
}
}
return { segments, availableSeats, totalAvailable: availableSeats.length };
}
}