mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
209 lines
8.5 KiB
TypeScript
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 };
|
|
}
|
|
}
|