mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +00:00
Refactor business logic for train,schedule,coach,seat and search modules
This commit is contained in:
@@ -1,64 +1,56 @@
|
||||
/**
|
||||
* SEGMENT-BASED SEAT RESERVATION EXAMPLE
|
||||
*
|
||||
* This example demonstrates the complete flow for booking Addis Ababa → Dire Dawa
|
||||
*
|
||||
* Demonstrates the complete flow for booking Addis Ababa → Dire Dawa
|
||||
* on the Addis Ababa → Djibouti route with segment-based seat management.
|
||||
*
|
||||
* Route: Addis Ababa (seq:0) → Adama (seq:1) → Awash (seq:2) → Dire Dawa (seq:3) → Djibouti (seq:4)
|
||||
* Booking: Addis Ababa → Dire Dawa (segments: 0→1, 1→2, 2→3)
|
||||
*
|
||||
* Route: Addis Ababa (seq:1) → Adama (seq:2) → Awash (seq:3) → Dire Dawa (seq:4) → Aysha (seq:5) → Djibouti (seq:6)
|
||||
* Booking: Addis Ababa → Dire Dawa (segments: 1→2, 2→3, 3→4)
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Example 1: Complete Booking Flow
|
||||
async function exampleBookingFlow() {
|
||||
console.log('=== SEGMENT-BASED BOOKING FLOW ===\n');
|
||||
|
||||
const tripId = 'trip_add_dji_001';
|
||||
const scheduleId = 'schedule_add_dji_001';
|
||||
const passengerId = 'passenger_kelemu';
|
||||
const seatIds = ['seat_coach_a_1a', 'seat_coach_a_1b'];
|
||||
const originStationId = 'st_ADD'; // Addis Ababa
|
||||
const destinationStationId = 'st_DRE'; // Dire Dawa
|
||||
const originStationId = 'st_ADD';
|
||||
const destinationStationId = 'st_DRE';
|
||||
|
||||
try {
|
||||
// Step 1: Check seat availability for segments
|
||||
console.log('1. Checking seat availability...');
|
||||
const segments = await getJourneySegments(tripId, originStationId, destinationStationId);
|
||||
const segments = await getJourneySegments(scheduleId, originStationId, destinationStationId);
|
||||
console.log('Journey segments:', segments.map(s => `${s.fromName} → ${s.toName}`));
|
||||
|
||||
// Step 2: Hold seats (10-minute expiry)
|
||||
console.log('\n2. Holding seats...');
|
||||
const holdResult = await holdSeatsTransaction(tripId, seatIds, passengerId, originStationId, destinationStationId);
|
||||
const holdResult = await holdSeatsTransaction(scheduleId, seatIds, passengerId, originStationId, destinationStationId);
|
||||
console.log('Hold created:', holdResult);
|
||||
|
||||
// Step 3: Simulate payment processing (5 seconds)
|
||||
console.log('\n3. Processing payment...');
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
// Step 4: Confirm booking
|
||||
console.log('\n4. Confirming booking...');
|
||||
const bookingId = 'booking_' + Date.now();
|
||||
const confirmResult = await confirmBookingTransaction(holdResult.holdId, bookingId, segments);
|
||||
console.log('Booking confirmed:', confirmResult);
|
||||
|
||||
// Step 5: Simulate trip progress and seat release
|
||||
console.log('\n5. Simulating trip progress...');
|
||||
await simulateTripProgress(tripId, segments);
|
||||
await simulateTripProgress(scheduleId, segments);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Booking flow error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Database Transaction Functions
|
||||
|
||||
async function getJourneySegments(tripId: string, originStationId: string, destinationStationId: string) {
|
||||
async function getJourneySegments(scheduleId: string, originStationId: string, destinationStationId: string) {
|
||||
const stopTimes = await prisma.tripStopTime.findMany({
|
||||
where: { tripId },
|
||||
where: { scheduleId },
|
||||
include: { station: true },
|
||||
orderBy: { sequence: 'asc' }
|
||||
orderBy: { sequence: 'asc' },
|
||||
});
|
||||
|
||||
const originStop = stopTimes.find(st => st.stationId === originStationId);
|
||||
@@ -72,7 +64,6 @@ async function getJourneySegments(tripId: string, originStationId: string, desti
|
||||
for (let i = originStop.sequence; i < destinationStop.sequence; i++) {
|
||||
const fromStop = stopTimes.find(st => st.sequence === i);
|
||||
const toStop = stopTimes.find(st => st.sequence === i + 1);
|
||||
|
||||
if (fromStop && toStop) {
|
||||
segments.push({
|
||||
fromStationId: fromStop.stationId,
|
||||
@@ -80,27 +71,19 @@ async function getJourneySegments(tripId: string, originStationId: string, desti
|
||||
fromSequence: fromStop.sequence,
|
||||
toSequence: toStop.sequence,
|
||||
fromName: fromStop.station.name,
|
||||
toName: toStop.station.name
|
||||
toName: toStop.station.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
async function holdSeatsTransaction(tripId: string, seatIds: string[], passengerId: string, originStationId: string, destinationStationId: string) {
|
||||
async function holdSeatsTransaction(scheduleId: string, seatIds: string[], passengerId: string, originStationId: string, destinationStationId: string) {
|
||||
return prisma.$transaction(async (tx) => {
|
||||
console.log(' → Starting seat hold transaction...');
|
||||
|
||||
// 1. Validate seats exist and are available
|
||||
const seats = await tx.seat.findMany({
|
||||
where: { id: { in: seatIds } },
|
||||
include: { coach: true }
|
||||
});
|
||||
|
||||
if (seats.length !== seatIds.length) {
|
||||
throw new Error('Some seats not found');
|
||||
}
|
||||
const seats = await tx.seat.findMany({ where: { id: { in: seatIds } }, include: { coach: true } });
|
||||
if (seats.length !== seatIds.length) throw new Error('Some seats not found');
|
||||
|
||||
for (const seat of seats) {
|
||||
if (seat.status !== 'AVAILABLE') {
|
||||
@@ -108,43 +91,15 @@ async function holdSeatsTransaction(tripId: string, seatIds: string[], passenger
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check for overlapping reservations
|
||||
const segments = await getJourneySegments(tripId, originStationId, destinationStationId);
|
||||
|
||||
for (const seatId of seatIds) {
|
||||
const overlaps = await checkOverlappingReservations(tx, tripId, seatId, segments);
|
||||
if (overlaps.length > 0) {
|
||||
throw new Error(`Seat ${seatId} has overlapping reservations`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Create hold record
|
||||
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
|
||||
const expiresAt = new Date(Date.now() + 10 * 60 * 1000);
|
||||
const seatHold = await tx.seatHold.create({
|
||||
data: {
|
||||
tripId,
|
||||
seatIds,
|
||||
passengerId,
|
||||
expiresAt
|
||||
}
|
||||
data: { scheduleId, seatIds, passengerId, expiresAt },
|
||||
});
|
||||
|
||||
// 4. Update seat status to HELD
|
||||
await tx.seat.updateMany({
|
||||
where: { id: { in: seatIds } },
|
||||
data: {
|
||||
status: 'HELD',
|
||||
heldUntil: expiresAt
|
||||
}
|
||||
});
|
||||
await tx.seat.updateMany({ where: { id: { in: seatIds } }, data: { status: 'HELD', heldUntil: expiresAt } });
|
||||
|
||||
console.log(' → Seats held successfully');
|
||||
return {
|
||||
holdId: seatHold.id,
|
||||
expiresAt,
|
||||
segments: segments.length,
|
||||
seats: seatIds.length
|
||||
};
|
||||
return { holdId: seatHold.id, expiresAt, seats: seatIds.length };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,157 +107,96 @@ async function confirmBookingTransaction(holdId: string, bookingId: string, segm
|
||||
return prisma.$transaction(async (tx) => {
|
||||
console.log(' → Starting booking confirmation transaction...');
|
||||
|
||||
// 1. Validate hold
|
||||
const hold = await tx.seatHold.findUnique({ where: { id: holdId } });
|
||||
if (!hold || hold.expiresAt < new Date()) {
|
||||
throw new Error('Hold expired or not found');
|
||||
}
|
||||
if (!hold || hold.expiresAt < new Date()) throw new Error('Hold expired or not found');
|
||||
|
||||
// 2. Create booking record (simplified)
|
||||
const booking = await tx.booking.create({
|
||||
data: {
|
||||
id: bookingId,
|
||||
bookingRef: 'BK' + Date.now().toString().slice(-6),
|
||||
passengerId: hold.passengerId,
|
||||
tripId: hold.tripId,
|
||||
status: 'CONFIRMED',
|
||||
totalMinor: 45000, // Example fare
|
||||
currency: 'ETB'
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Create journey record
|
||||
const journey = await tx.journey.create({
|
||||
data: {
|
||||
passengerId: hold.passengerId,
|
||||
scheduleId: hold.scheduleId,
|
||||
status: 'CONFIRMED',
|
||||
totalMinor: 45000,
|
||||
currency: 'ETB'
|
||||
}
|
||||
currency: 'ETB',
|
||||
},
|
||||
});
|
||||
|
||||
const journey = await tx.journey.create({
|
||||
data: { passengerId: hold.passengerId, status: 'CONFIRMED', totalMinor: 45000, currency: 'ETB' },
|
||||
});
|
||||
|
||||
// 4. Create journey segments for each seat
|
||||
for (const seatId of hold.seatIds) {
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
await tx.journeySegment.create({
|
||||
data: {
|
||||
journeyId: journey.id,
|
||||
tripId: hold.tripId,
|
||||
scheduleId: hold.scheduleId,
|
||||
segmentOrder: i + 1,
|
||||
seatId,
|
||||
departureStationId: segments[i].fromStationId,
|
||||
arrivalStationId: segments[i].toStationId
|
||||
}
|
||||
arrivalStationId: segments[i].toStationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Create booking seats
|
||||
for (const seatId of hold.seatIds) {
|
||||
await tx.bookingSeat.create({
|
||||
data: {
|
||||
bookingId,
|
||||
seatId,
|
||||
passengerName: 'Kelemu Ketsela' // Example
|
||||
}
|
||||
});
|
||||
await tx.bookingSeat.create({ data: { bookingId, seatId, passengerName: 'Kelemu Ketsela' } });
|
||||
}
|
||||
|
||||
// 6. Update seat status to BOOKED
|
||||
await tx.seat.updateMany({
|
||||
where: { id: { in: hold.seatIds } },
|
||||
data: {
|
||||
status: 'BOOKED',
|
||||
heldUntil: null
|
||||
}
|
||||
});
|
||||
|
||||
// 7. Delete hold
|
||||
await tx.seat.updateMany({ where: { id: { in: hold.seatIds } }, data: { status: 'BOOKED', heldUntil: null } });
|
||||
await tx.seatHold.delete({ where: { id: holdId } });
|
||||
|
||||
console.log(' → Booking confirmed successfully');
|
||||
return {
|
||||
bookingId,
|
||||
bookingRef: booking.bookingRef,
|
||||
confirmedSeats: hold.seatIds.length,
|
||||
segments: segments.length
|
||||
};
|
||||
return { bookingId, bookingRef: booking.bookingRef, confirmedSeats: hold.seatIds.length, segments: segments.length };
|
||||
});
|
||||
}
|
||||
|
||||
async function simulateTripProgress(tripId: string, bookedSegments: any[]) {
|
||||
async function simulateTripProgress(scheduleId: string, bookedSegments: any[]) {
|
||||
console.log(' → Simulating trip progress...');
|
||||
|
||||
// Simulate train reaching each station
|
||||
for (const segment of bookedSegments) {
|
||||
console.log(` → Train approaching ${segment.toName}...`);
|
||||
|
||||
// Update trip live status
|
||||
|
||||
await prisma.tripLiveStatus.upsert({
|
||||
where: { tripId },
|
||||
update: {
|
||||
currentLocationLabel: segment.toName,
|
||||
progressPercent: Math.round((segment.toSequence / 4) * 100),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
where: { scheduleId },
|
||||
update: { currentLocationLabel: segment.toName, progressPercent: Math.round((segment.toSequence / 4) * 100) },
|
||||
create: {
|
||||
tripId,
|
||||
scheduleId,
|
||||
state: 'EN_ROUTE',
|
||||
currentLocationLabel: segment.toName,
|
||||
progressPercent: Math.round((segment.toSequence / 4) * 100),
|
||||
delayMinutes: 0,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Check if this is the final destination for any passengers
|
||||
if (segment.toName === 'Dire Dawa') {
|
||||
console.log(' → Passengers reached destination, releasing seats...');
|
||||
await releaseSeatsAtStation(tripId, segment.toStationId);
|
||||
await releaseSeatsAtStation(scheduleId, segment.toStationId);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second delay
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
}
|
||||
|
||||
async function releaseSeatsAtStation(tripId: string, stationId: string) {
|
||||
async function releaseSeatsAtStation(scheduleId: string, stationId: string) {
|
||||
return prisma.$transaction(async (tx) => {
|
||||
// Find journey segments ending at this station
|
||||
const completedSegments = await tx.journeySegment.findMany({
|
||||
where: {
|
||||
tripId,
|
||||
arrivalStationId: stationId
|
||||
},
|
||||
include: {
|
||||
journey: {
|
||||
include: {
|
||||
journeySegments: {
|
||||
where: { tripId }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
where: { scheduleId, arrivalStationId: stationId },
|
||||
include: { journey: { include: { journeySegments: { where: { scheduleId } } } } },
|
||||
});
|
||||
|
||||
const seatsToRelease = [];
|
||||
const seatsToRelease: string[] = [];
|
||||
|
||||
// Check if passenger's entire journey is complete
|
||||
for (const segment of completedSegments) {
|
||||
const passengerSegments = segment.journey.journeySegments.filter((js: any) => js.seatId === segment.seatId);
|
||||
const maxOrder = Math.max(...passengerSegments.map((js: any) => js.segmentOrder));
|
||||
|
||||
if (segment.segmentOrder === maxOrder) {
|
||||
seatsToRelease.push(segment.seatId!);
|
||||
}
|
||||
if (segment.segmentOrder === maxOrder) seatsToRelease.push(segment.seatId!);
|
||||
}
|
||||
|
||||
// Release seats
|
||||
if (seatsToRelease.length > 0) {
|
||||
await tx.seat.updateMany({
|
||||
where: { id: { in: seatsToRelease } },
|
||||
data: { status: 'AVAILABLE' }
|
||||
});
|
||||
|
||||
await tx.seat.updateMany({ where: { id: { in: seatsToRelease } }, data: { status: 'AVAILABLE' } });
|
||||
console.log(` → Released ${seatsToRelease.length} seats at station`);
|
||||
}
|
||||
|
||||
@@ -310,73 +204,24 @@ async function releaseSeatsAtStation(tripId: string, stationId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
async function checkOverlappingReservations(tx: any, tripId: string, seatId: string, segments: any[]) {
|
||||
// Check active holds
|
||||
async function checkOverlappingReservations(tx: any, scheduleId: string, seatId: string, segments: any[]) {
|
||||
const activeHolds = await tx.seatHold.findMany({
|
||||
where: {
|
||||
tripId,
|
||||
seatIds: { has: seatId },
|
||||
expiresAt: { gt: new Date() }
|
||||
}
|
||||
where: { scheduleId, seatIds: { has: seatId }, expiresAt: { gt: new Date() } },
|
||||
});
|
||||
|
||||
// Check active bookings
|
||||
const activeBookings = await tx.journeySegment.findMany({
|
||||
where: {
|
||||
tripId,
|
||||
scheduleId,
|
||||
seatId,
|
||||
journey: {
|
||||
status: { in: ['PENDING_PAYMENT', 'CONFIRMED'] }
|
||||
}
|
||||
}
|
||||
journey: { status: { in: ['PENDING_PAYMENT', 'CONFIRMED'] } },
|
||||
},
|
||||
});
|
||||
|
||||
return [...activeHolds, ...activeBookings];
|
||||
}
|
||||
|
||||
// Example API Usage
|
||||
async function exampleApiUsage() {
|
||||
console.log('\n=== API ENDPOINT EXAMPLES ===\n');
|
||||
|
||||
const baseUrl = 'http://localhost:4000';
|
||||
|
||||
// 1. Check availability
|
||||
console.log('GET /segments/seats/availability');
|
||||
console.log('Query: tripId=trip_001&originStationId=st_ADD&destinationStationId=st_DRE');
|
||||
console.log('Response: Available seats for Addis Ababa → Dire Dawa segments\n');
|
||||
|
||||
// 2. Hold seats
|
||||
console.log('POST /segments/seats/hold');
|
||||
console.log('Body:', JSON.stringify({
|
||||
tripId: 'trip_001',
|
||||
seatIds: ['seat_1', 'seat_2'],
|
||||
passengerId: 'passenger_123',
|
||||
originStationId: 'st_ADD',
|
||||
destinationStationId: 'st_DRE'
|
||||
}, null, 2));
|
||||
console.log('Response: Hold created with 10-minute expiry\n');
|
||||
|
||||
// 3. Confirm booking
|
||||
console.log('POST /segments/seats/confirm');
|
||||
console.log('Body:', JSON.stringify({
|
||||
holdId: 'hold_123',
|
||||
bookingId: 'booking_456'
|
||||
}, null, 2));
|
||||
console.log('Response: Booking confirmed, seats reserved for segments\n');
|
||||
|
||||
// 4. Release seats (triggered by trip progress)
|
||||
console.log('POST /segments/seats/release');
|
||||
console.log('Body:', JSON.stringify({
|
||||
tripId: 'trip_001',
|
||||
currentStationId: 'st_DRE'
|
||||
}, null, 2));
|
||||
console.log('Response: Seats released for passengers reaching Dire Dawa\n');
|
||||
}
|
||||
|
||||
// Run examples
|
||||
if (require.main === module) {
|
||||
exampleBookingFlow()
|
||||
.then(() => exampleApiUsage())
|
||||
.then(() => console.log('\n=== EXAMPLES COMPLETED ==='))
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -388,5 +233,6 @@ export {
|
||||
holdSeatsTransaction,
|
||||
confirmBookingTransaction,
|
||||
simulateTripProgress,
|
||||
releaseSeatsAtStation
|
||||
};
|
||||
releaseSeatsAtStation,
|
||||
checkOverlappingReservations,
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SegmentsService, Segment } from '../segments/segments.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
|
||||
export interface SeatHoldRequest {
|
||||
tripId: string;
|
||||
scheduleId: string;
|
||||
seatIds: string[];
|
||||
passengerId: string;
|
||||
originStationId: string;
|
||||
@@ -22,349 +22,177 @@ export class EnhancedSeatsService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private segmentsService: SegmentsService,
|
||||
private eventEmitter: EventEmitter2
|
||||
private eventEmitter: EventEmitter2,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Hold seats for specific segments with atomicity
|
||||
*/
|
||||
async holdSeats(request: SeatHoldRequest) {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
// 1. Get journey segments
|
||||
const segments = await this.segmentsService.getJourneySegments(
|
||||
request.tripId,
|
||||
request.originStationId,
|
||||
request.destinationStationId
|
||||
);
|
||||
const segments = await this.segmentsService.getJourneySegments(request.scheduleId, request.originStationId, request.destinationStationId);
|
||||
|
||||
// 2. Check seat availability for all requested seats
|
||||
for (const seatId of request.seatIds) {
|
||||
const seat = await tx.seat.findUnique({
|
||||
where: { id: seatId },
|
||||
include: { coach: true }
|
||||
});
|
||||
|
||||
if (!seat) {
|
||||
throw new BadRequestException(`Seat ${seatId} not found`);
|
||||
}
|
||||
|
||||
if (seat.status === 'BLOCKED') {
|
||||
throw new BadRequestException(`Seat ${seat.label} is blocked`);
|
||||
}
|
||||
|
||||
// Check for overlapping reservations
|
||||
const overlaps = await this.segmentsService.getOverlappingReservations(
|
||||
request.tripId,
|
||||
seatId,
|
||||
segments
|
||||
);
|
||||
|
||||
if (overlaps.length > 0) {
|
||||
throw new ConflictException(`Seat ${seat.label} is not available for the requested segments`);
|
||||
}
|
||||
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.label} is blocked`);
|
||||
const overlaps = await this.segmentsService.getOverlappingReservations(request.scheduleId, seatId, segments);
|
||||
if (overlaps.length > 0) throw new ConflictException(`Seat ${seat.label} is not available for the requested segments`);
|
||||
}
|
||||
|
||||
// 3. Create seat hold
|
||||
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
|
||||
const expiresAt = new Date(Date.now() + 10 * 60 * 1000);
|
||||
// Encode origin/destination into fareQuoteId so confirmBooking can resolve the leg range
|
||||
// Format: "leg:{originStationId}:{destinationStationId}" (or preserve actual fareQuoteId)
|
||||
const legKey = request.fareQuoteId ?? `leg:${request.originStationId}:${request.destinationStationId}`;
|
||||
const seatHold = await tx.seatHold.create({
|
||||
data: {
|
||||
tripId: request.tripId,
|
||||
seatIds: request.seatIds,
|
||||
passengerId: request.passengerId,
|
||||
fareQuoteId: request.fareQuoteId,
|
||||
expiresAt
|
||||
}
|
||||
data: { scheduleId: request.scheduleId, seatIds: request.seatIds, passengerId: request.passengerId, fareQuoteId: legKey, expiresAt },
|
||||
});
|
||||
|
||||
// 4. Update seat status to HELD
|
||||
await tx.seat.updateMany({
|
||||
where: { id: { in: request.seatIds } },
|
||||
data: {
|
||||
status: 'HELD',
|
||||
heldUntil: expiresAt
|
||||
}
|
||||
});
|
||||
await tx.seat.updateMany({ where: { id: { in: request.seatIds } }, data: { status: 'HELD', heldUntil: expiresAt } });
|
||||
|
||||
// 5. Emit event for real-time updates
|
||||
this.eventEmitter.emit('seats.held', {
|
||||
holdId: seatHold.id,
|
||||
tripId: request.tripId,
|
||||
seatIds: request.seatIds,
|
||||
segments
|
||||
});
|
||||
this.eventEmitter.emit('seats.held', { holdId: seatHold.id, scheduleId: request.scheduleId, seatIds: request.seatIds, segments });
|
||||
|
||||
return {
|
||||
holdId: seatHold.id,
|
||||
expiresAt,
|
||||
segments,
|
||||
seats: request.seatIds
|
||||
};
|
||||
return { holdId: seatHold.id, expiresAt, segments, seats: request.seatIds };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm booking and convert hold to booking
|
||||
*/
|
||||
async confirmBooking(request: BookingConfirmRequest) {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
// 1. Get and validate hold
|
||||
const hold = await tx.seatHold.findUnique({
|
||||
where: { id: request.holdId }
|
||||
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');
|
||||
|
||||
if (!hold) {
|
||||
throw new BadRequestException('Seat hold not found');
|
||||
// Resolve the passenger's leg range from the hold's fareQuoteId (encoded as "leg:originId:destId")
|
||||
const legKey = hold.fareQuoteId ?? '';
|
||||
let originStationId: string | undefined;
|
||||
let destinationStationId: string | undefined;
|
||||
if (legKey.startsWith('leg:')) {
|
||||
const parts = legKey.split(':');
|
||||
originStationId = parts[1];
|
||||
destinationStationId = parts[2];
|
||||
} else {
|
||||
// Fall back to booking's own origin/destination if available
|
||||
originStationId = (booking as any).originStationId;
|
||||
destinationStationId = (booking as any).destinationStationId;
|
||||
}
|
||||
|
||||
if (hold.expiresAt < new Date()) {
|
||||
throw new BadRequestException('Seat hold has expired');
|
||||
}
|
||||
const originStop = originStationId ? schedule.stopTimes.find(s => s.stationId === originStationId) : undefined;
|
||||
const destStop = destinationStationId ? schedule.stopTimes.find(s => s.stationId === destinationStationId) : undefined;
|
||||
const fromSeq = originStop?.sequence ?? schedule.stopTimes[0].sequence;
|
||||
const toSeq = destStop?.sequence ?? schedule.stopTimes[schedule.stopTimes.length - 1].sequence;
|
||||
|
||||
// 2. Get booking
|
||||
const booking = await tx.booking.findUnique({
|
||||
where: { id: request.bookingId }
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
throw new BadRequestException('Booking not found');
|
||||
}
|
||||
|
||||
// 3. Get journey segments - we need to derive from trip stops
|
||||
const trip = await tx.trip.findUnique({
|
||||
where: { id: hold.tripId },
|
||||
include: {
|
||||
stopTimes: {
|
||||
orderBy: { sequence: 'asc' }
|
||||
}
|
||||
const segments: Segment[] = [];
|
||||
for (let i = fromSeq; i < toSeq; i++) {
|
||||
const fromStop = schedule.stopTimes.find(s => s.sequence === i);
|
||||
const toStop = schedule.stopTimes.find(s => s.sequence === i + 1);
|
||||
if (fromStop && toStop) {
|
||||
segments.push({
|
||||
fromStationId: fromStop.stationId,
|
||||
toStationId: toStop.stationId,
|
||||
fromSequence: fromStop.sequence,
|
||||
toSequence: toStop.sequence,
|
||||
fromName: '',
|
||||
toName: '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!trip) {
|
||||
throw new BadRequestException('Trip not found');
|
||||
}
|
||||
|
||||
// For now, create segments for the full trip (would need origin/destination from booking)
|
||||
const segments = [];
|
||||
for (let i = 0; i < trip.stopTimes.length - 1; i++) {
|
||||
segments.push({
|
||||
fromStationId: trip.stopTimes[i].stationId,
|
||||
toStationId: trip.stopTimes[i + 1].stationId,
|
||||
fromSequence: trip.stopTimes[i].sequence,
|
||||
toSequence: trip.stopTimes[i + 1].sequence
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Create journey record
|
||||
const journey = await tx.journey.create({
|
||||
data: {
|
||||
passengerId: hold.passengerId,
|
||||
status: 'CONFIRMED',
|
||||
totalMinor: booking.totalMinor,
|
||||
currency: booking.currency
|
||||
}
|
||||
data: { passengerId: hold.passengerId, status: 'CONFIRMED', totalMinor: booking.totalMinor, currency: booking.currency },
|
||||
});
|
||||
|
||||
// 5. Create journey segments for each seat
|
||||
for (const seatId of hold.seatIds) {
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
await tx.journeySegment.create({
|
||||
data: {
|
||||
journeyId: journey.id,
|
||||
tripId: hold.tripId,
|
||||
scheduleId: hold.scheduleId,
|
||||
segmentOrder: i + 1,
|
||||
seatId,
|
||||
departureStationId: segments[i].fromStationId,
|
||||
arrivalStationId: segments[i].toStationId
|
||||
}
|
||||
arrivalStationId: segments[i].toStationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Update seat status to BOOKED
|
||||
await tx.seat.updateMany({
|
||||
where: { id: { in: hold.seatIds } },
|
||||
data: {
|
||||
status: 'BOOKED',
|
||||
heldUntil: null
|
||||
}
|
||||
});
|
||||
await tx.seat.updateMany({ where: { id: { in: hold.seatIds } }, data: { status: 'BOOKED', heldUntil: null } });
|
||||
await tx.seatHold.delete({ where: { id: request.holdId } });
|
||||
|
||||
// 7. Delete the hold
|
||||
await tx.seatHold.delete({
|
||||
where: { id: request.holdId }
|
||||
});
|
||||
this.eventEmitter.emit('booking.confirmed', { bookingId: request.bookingId, scheduleId: hold.scheduleId, seatIds: hold.seatIds, segments });
|
||||
|
||||
// 8. Emit confirmation event
|
||||
this.eventEmitter.emit('booking.confirmed', {
|
||||
bookingId: request.bookingId,
|
||||
tripId: hold.tripId,
|
||||
seatIds: hold.seatIds,
|
||||
segments
|
||||
});
|
||||
|
||||
return {
|
||||
bookingId: request.bookingId,
|
||||
confirmedSeats: hold.seatIds,
|
||||
segments
|
||||
};
|
||||
return { bookingId: request.bookingId, confirmedSeats: hold.seatIds, segments };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Release seats when passenger reaches destination
|
||||
*/
|
||||
async releaseSeats(tripId: string, currentStationId: string) {
|
||||
async releaseSeats(scheduleId: string, currentStationId: string) {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
// 1. Find all journey segments ending at current station
|
||||
const completedSegments = await tx.journeySegment.findMany({
|
||||
where: {
|
||||
tripId,
|
||||
arrivalStationId: currentStationId
|
||||
},
|
||||
include: {
|
||||
journey: {
|
||||
include: {
|
||||
journeySegments: {
|
||||
where: { tripId }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
where: { scheduleId, arrivalStationId: currentStationId },
|
||||
include: { journey: { include: { journeySegments: { where: { scheduleId } } } } },
|
||||
});
|
||||
|
||||
const seatsToRelease = [];
|
||||
|
||||
// 2. Check if passenger's entire journey is complete
|
||||
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 this is the last segment for this seat, release it
|
||||
if (segment.segmentOrder === maxSegmentOrder) {
|
||||
seatsToRelease.push(segment.seatId!);
|
||||
}
|
||||
if (segment.segmentOrder === maxSegmentOrder) seatsToRelease.push(segment.seatId!);
|
||||
}
|
||||
|
||||
// 3. Update seat status to AVAILABLE
|
||||
if (seatsToRelease.length > 0) {
|
||||
await tx.seat.updateMany({
|
||||
where: { id: { in: seatsToRelease } },
|
||||
data: { status: 'AVAILABLE' }
|
||||
});
|
||||
|
||||
// 4. Mark journey segments as completed (optional - could add a completed field)
|
||||
// For now, we'll leave the segments as they are for historical tracking
|
||||
|
||||
// 5. Emit release event
|
||||
this.eventEmitter.emit('seats.released', {
|
||||
tripId,
|
||||
stationId: currentStationId,
|
||||
releasedSeats: seatsToRelease
|
||||
});
|
||||
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
|
||||
};
|
||||
return { releasedSeats: seatsToRelease, stationId: currentStationId };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Expire old holds (background job)
|
||||
*/
|
||||
async expireHolds() {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const expiredHolds = await tx.seatHold.findMany({
|
||||
where: {
|
||||
expiresAt: { lt: new Date() }
|
||||
}
|
||||
});
|
||||
|
||||
const expiredSeatIds = expiredHolds.flatMap(hold => hold.seatIds);
|
||||
const expiredHolds = await tx.seatHold.findMany({ where: { expiresAt: { lt: new Date() } } });
|
||||
const expiredSeatIds = expiredHolds.flatMap(h => h.seatIds);
|
||||
|
||||
if (expiredSeatIds.length > 0) {
|
||||
// Release expired seats
|
||||
await tx.seat.updateMany({
|
||||
where: { id: { in: expiredSeatIds } },
|
||||
data: {
|
||||
status: 'AVAILABLE',
|
||||
heldUntil: null
|
||||
}
|
||||
});
|
||||
|
||||
// Delete expired holds
|
||||
await tx.seatHold.deleteMany({
|
||||
where: {
|
||||
expiresAt: { lt: new Date() }
|
||||
}
|
||||
});
|
||||
|
||||
this.eventEmitter.emit('holds.expired', {
|
||||
expiredHolds: expiredHolds.length,
|
||||
releasedSeats: expiredSeatIds
|
||||
});
|
||||
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
|
||||
};
|
||||
return { expiredHolds: expiredHolds.length, releasedSeats: expiredSeatIds };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get seat availability for specific segments
|
||||
*/
|
||||
async getSeatAvailability(tripId: string, originStationId: string, destinationStationId: string) {
|
||||
const segments = await this.segmentsService.getJourneySegments(
|
||||
tripId,
|
||||
originStationId,
|
||||
destinationStationId
|
||||
);
|
||||
async getSeatAvailability(scheduleId: string, originStationId: string, destinationStationId: string) {
|
||||
const segments = await this.segmentsService.getJourneySegments(scheduleId, originStationId, destinationStationId);
|
||||
|
||||
const trip = await this.prisma.trip.findUnique({
|
||||
where: { id: tripId },
|
||||
include: {
|
||||
coaches: {
|
||||
include: {
|
||||
seats: true
|
||||
}
|
||||
}
|
||||
}
|
||||
const schedule = await this.prisma.trainSchedule.findUnique({
|
||||
where: { id: scheduleId },
|
||||
include: { coachAssignments: { include: { coach: { include: { seats: true, seatClass: true } } } } },
|
||||
});
|
||||
|
||||
if (!trip) {
|
||||
throw new BadRequestException('Trip not found');
|
||||
}
|
||||
if (!schedule) throw new BadRequestException('Schedule not found');
|
||||
|
||||
const availableSeats = [];
|
||||
|
||||
for (const coach of trip.coaches) {
|
||||
for (const seat of coach.seats) {
|
||||
const overlaps = await this.segmentsService.getOverlappingReservations(
|
||||
tripId,
|
||||
seat.id,
|
||||
segments
|
||||
);
|
||||
|
||||
for (const assignment of schedule.coachAssignments) {
|
||||
for (const seat of assignment.coach.seats) {
|
||||
const overlaps = await this.segmentsService.getOverlappingReservations(scheduleId, seat.id, segments);
|
||||
if (overlaps.length === 0 && seat.status === 'AVAILABLE') {
|
||||
availableSeats.push({
|
||||
id: seat.id,
|
||||
label: seat.label,
|
||||
coach: coach.label,
|
||||
serviceClass: coach.serviceClass,
|
||||
row: seat.row,
|
||||
col: seat.col
|
||||
id: seat.id, label: seat.label,
|
||||
coach: assignment.coach.label,
|
||||
seatClass: assignment.coach.seatClass.name,
|
||||
row: seat.row, col: seat.col,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
segments,
|
||||
availableSeats,
|
||||
totalAvailable: availableSeats.length
|
||||
};
|
||||
return { segments, availableSeats, totalAvailable: availableSeats.length };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,12 +32,12 @@ export class SegmentSeatsController {
|
||||
@ApiResponse({ status: 409, description: 'Seats not available for requested segments' })
|
||||
async holdSeats(@Body() dto: HoldSeatsDto) {
|
||||
return this.enhancedSeatsService.holdSeats({
|
||||
tripId: dto.tripId,
|
||||
scheduleId: dto.scheduleId,
|
||||
seatIds: dto.seatIds,
|
||||
passengerId: dto.passengerId,
|
||||
originStationId: dto.originStationId,
|
||||
destinationStationId: dto.destinationStationId,
|
||||
fareQuoteId: dto.fareQuoteId
|
||||
fareQuoteId: dto.fareQuoteId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export class SegmentSeatsController {
|
||||
}
|
||||
})
|
||||
async releaseSeats(@Body() dto: ReleaseSeatsDto) {
|
||||
return this.enhancedSeatsService.releaseSeats(dto.tripId, dto.currentStationId);
|
||||
return this.enhancedSeatsService.releaseSeats(dto.scheduleId, dto.currentStationId);
|
||||
}
|
||||
|
||||
@Get('availability')
|
||||
@@ -100,19 +100,15 @@ export class SegmentSeatsController {
|
||||
{ fromName: 'Adama', toName: 'Awash', fromSequence: 1, toSequence: 2 }
|
||||
],
|
||||
availableSeats: [
|
||||
{ id: 'seat_1', label: '1A', coach: 'A', serviceClass: 'ECONOMY', row: 1, col: 'A' },
|
||||
{ id: 'seat_2', label: '1B', coach: 'A', serviceClass: 'ECONOMY', row: 1, col: 'B' }
|
||||
{ id: 'seat_1', label: '1A', coach: 'A', seatClass: 'Economy Regular', row: 1, col: 'A' },
|
||||
{ id: 'seat_2', label: '1B', coach: 'A', seatClass: 'Economy Regular', row: 1, col: 'B' }
|
||||
],
|
||||
totalAvailable: 2
|
||||
}
|
||||
}
|
||||
})
|
||||
async getSeatAvailability(@Query() dto: SeatAvailabilityDto) {
|
||||
return this.enhancedSeatsService.getSeatAvailability(
|
||||
dto.tripId,
|
||||
dto.originStationId,
|
||||
dto.destinationStationId
|
||||
);
|
||||
return this.enhancedSeatsService.getSeatAvailability(dto.scheduleId, dto.originStationId, dto.destinationStationId);
|
||||
}
|
||||
|
||||
@Post('expire-holds')
|
||||
|
||||
@@ -1,64 +1,27 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsString, IsArray, IsOptional } from 'class-validator';
|
||||
|
||||
export class HoldSeatsDto {
|
||||
@ApiProperty({ example: 'trip_123' })
|
||||
@IsString()
|
||||
tripId: string;
|
||||
|
||||
@ApiProperty({ example: ['seat_1', 'seat_2'] })
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
seatIds: string[];
|
||||
|
||||
@ApiProperty({ example: 'passenger_123' })
|
||||
@IsString()
|
||||
passengerId: string;
|
||||
|
||||
@ApiProperty({ example: 'st_ADD' })
|
||||
@IsString()
|
||||
originStationId: string;
|
||||
|
||||
@ApiProperty({ example: 'st_DRE' })
|
||||
@IsString()
|
||||
destinationStationId: string;
|
||||
|
||||
@ApiProperty({ example: 'quote_123', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
fareQuoteId?: string;
|
||||
@ApiProperty({ example: 'schedule-uuid' }) @IsString() scheduleId: string;
|
||||
@ApiProperty({ example: ['seat_1', 'seat_2'] }) @IsArray() @IsString({ each: true }) seatIds: string[];
|
||||
@ApiProperty({ example: 'passenger-uuid' }) @IsString() passengerId: string;
|
||||
@ApiProperty({ example: 'st_ADD' }) @IsString() originStationId: string;
|
||||
@ApiProperty({ example: 'st_DJI' }) @IsString() destinationStationId: string;
|
||||
@ApiPropertyOptional({ example: 'quote-uuid' }) @IsOptional() @IsString() fareQuoteId?: string;
|
||||
}
|
||||
|
||||
export class ConfirmBookingDto {
|
||||
@ApiProperty({ example: 'hold_123' })
|
||||
@IsString()
|
||||
holdId: string;
|
||||
|
||||
@ApiProperty({ example: 'booking_123' })
|
||||
@IsString()
|
||||
bookingId: string;
|
||||
@ApiProperty({ example: 'hold-uuid' }) @IsString() holdId: string;
|
||||
@ApiProperty({ example: 'booking-uuid' }) @IsString() bookingId: string;
|
||||
}
|
||||
|
||||
export class SeatAvailabilityDto {
|
||||
@ApiProperty({ example: 'trip_123' })
|
||||
@IsString()
|
||||
tripId: string;
|
||||
|
||||
@ApiProperty({ example: 'st_ADD' })
|
||||
@IsString()
|
||||
originStationId: string;
|
||||
|
||||
@ApiProperty({ example: 'st_DRE' })
|
||||
@IsString()
|
||||
destinationStationId: string;
|
||||
@ApiProperty({ example: 'schedule-uuid' }) @IsString() scheduleId: string;
|
||||
@ApiProperty({ example: 'st_ADD' }) @IsString() originStationId: string;
|
||||
@ApiProperty({ example: 'st_DJI' }) @IsString() destinationStationId: string;
|
||||
}
|
||||
|
||||
export class ReleaseSeatsDto {
|
||||
@ApiProperty({ example: 'trip_123' })
|
||||
@IsString()
|
||||
tripId: string;
|
||||
|
||||
@ApiProperty({ example: 'st_DRE' })
|
||||
@IsString()
|
||||
currentStationId: string;
|
||||
}
|
||||
@ApiProperty({ example: 'schedule-uuid' }) @IsString() scheduleId: string;
|
||||
@ApiProperty({ example: 'st_DJI' }) @IsString() currentStationId: string;
|
||||
}
|
||||
|
||||
@@ -14,33 +14,31 @@ export interface Segment {
|
||||
export class SegmentsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* Derive all segments between origin and destination using TripStopTime.sequence
|
||||
* Example: Addis → Dire Dawa = [Addis → Adama, Adama → Awash, Awash → Dire Dawa]
|
||||
*/
|
||||
async getJourneySegments(tripId: string, originStationId: string, destinationStationId: string): Promise<Segment[]> {
|
||||
async getJourneySegments(
|
||||
scheduleId: string,
|
||||
originStationId: string,
|
||||
destinationStationId: string,
|
||||
): Promise<Segment[]> {
|
||||
const stopTimes = await this.prisma.tripStopTime.findMany({
|
||||
where: { tripId },
|
||||
where: { scheduleId },
|
||||
include: { station: true },
|
||||
orderBy: { sequence: 'asc' }
|
||||
orderBy: { sequence: 'asc' },
|
||||
});
|
||||
|
||||
const originStop = stopTimes.find(st => st.stationId === originStationId);
|
||||
const destinationStop = stopTimes.find(st => st.stationId === destinationStationId);
|
||||
const destStop = stopTimes.find(st => st.stationId === destinationStationId);
|
||||
|
||||
if (!originStop || !destinationStop) {
|
||||
throw new BadRequestException('Origin or destination station not found on this trip');
|
||||
if (!originStop || !destStop) {
|
||||
throw new BadRequestException('Origin or destination station not found on this schedule');
|
||||
}
|
||||
|
||||
if (originStop.sequence >= destinationStop.sequence) {
|
||||
if (originStop.sequence >= destStop.sequence) {
|
||||
throw new BadRequestException('Origin must come before destination');
|
||||
}
|
||||
|
||||
const segments: Segment[] = [];
|
||||
for (let i = originStop.sequence; i < destinationStop.sequence; i++) {
|
||||
for (let i = originStop.sequence; i < destStop.sequence; i++) {
|
||||
const fromStop = stopTimes.find(st => st.sequence === i);
|
||||
const toStop = stopTimes.find(st => st.sequence === i + 1);
|
||||
|
||||
if (fromStop && toStop) {
|
||||
segments.push({
|
||||
fromStationId: fromStop.stationId,
|
||||
@@ -48,97 +46,85 @@ export class SegmentsService {
|
||||
fromSequence: fromStop.sequence,
|
||||
toSequence: toStop.sequence,
|
||||
fromName: fromStop.station.name,
|
||||
toName: toStop.station.name
|
||||
toName: toStop.station.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two segment ranges overlap
|
||||
*/
|
||||
/** True if two segment ranges overlap: [a.from, a.to) ∩ [b.from, b.to) ≠ ∅ */
|
||||
segmentsOverlap(segments1: Segment[], segments2: Segment[]): boolean {
|
||||
for (const seg1 of segments1) {
|
||||
for (const seg2 of segments2) {
|
||||
// Segments overlap if one starts before the other ends
|
||||
if (seg1.fromSequence < seg2.toSequence && seg2.fromSequence < seg1.toSequence) {
|
||||
return true;
|
||||
}
|
||||
for (const s1 of segments1) {
|
||||
for (const s2 of segments2) {
|
||||
if (s1.fromSequence < s2.toSequence && s2.fromSequence < s1.toSequence) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all existing bookings/holds that overlap with given segments
|
||||
* Returns conflicts for a seat on a schedule for the requested segment range.
|
||||
* Checks:
|
||||
* 1. Active SeatHolds — resolved to sequence range via JourneySegment if available,
|
||||
* otherwise treated as full-schedule block.
|
||||
* 2. Active BookingSeats — resolved via JourneySegment sequence ranges.
|
||||
*/
|
||||
async getOverlappingReservations(tripId: string, seatId: string, segments: Segment[]) {
|
||||
// Get active holds
|
||||
async getOverlappingReservations(
|
||||
scheduleId: string,
|
||||
seatId: string,
|
||||
requestedSegments: Segment[],
|
||||
) {
|
||||
const overlaps: { type: string; id: string }[] = [];
|
||||
const reqFrom = Math.min(...requestedSegments.map(s => s.fromSequence));
|
||||
const reqTo = Math.max(...requestedSegments.map(s => s.toSequence));
|
||||
|
||||
// ── 1. Active holds ──────────────────────────────────────────────────────
|
||||
const activeHolds = await this.prisma.seatHold.findMany({
|
||||
where: {
|
||||
tripId,
|
||||
seatIds: { has: seatId },
|
||||
expiresAt: { gt: new Date() }
|
||||
}
|
||||
where: { scheduleId, seatIds: { has: seatId }, expiresAt: { gt: new Date() } },
|
||||
});
|
||||
|
||||
// Get active bookings with journey segments
|
||||
const activeBookings = await this.prisma.bookingSeat.findMany({
|
||||
where: {
|
||||
seatId,
|
||||
booking: {
|
||||
tripId,
|
||||
status: { in: ['PENDING_PAYMENT', 'CONFIRMED'] }
|
||||
}
|
||||
},
|
||||
include: {
|
||||
booking: true
|
||||
}
|
||||
});
|
||||
|
||||
const overlaps = [];
|
||||
|
||||
// Check hold overlaps (assume full journey for holds)
|
||||
for (const hold of activeHolds) {
|
||||
overlaps.push({ type: 'hold', id: hold.id });
|
||||
}
|
||||
|
||||
// Check booking overlaps by querying journey segments separately
|
||||
for (const booking of activeBookings) {
|
||||
const journeySegments = await this.prisma.journeySegment.findMany({
|
||||
where: {
|
||||
tripId,
|
||||
seatId,
|
||||
journeyId: booking.bookingId
|
||||
}
|
||||
// Resolve hold range from JourneySegments created at hold time
|
||||
const holdSegs = await this.prisma.journeySegment.findMany({
|
||||
where: { scheduleId, seatId },
|
||||
include: { schedule: { include: { stopTimes: true } } },
|
||||
});
|
||||
|
||||
for (const journeySegment of journeySegments) {
|
||||
// Get sequence numbers for this segment
|
||||
const segmentStops = await this.prisma.tripStopTime.findMany({
|
||||
where: {
|
||||
tripId,
|
||||
stationId: { in: [journeySegment.departureStationId, journeySegment.arrivalStationId] }
|
||||
}
|
||||
});
|
||||
if (holdSegs.length === 0) {
|
||||
// No journey segments yet — conservative: treat as full-schedule conflict
|
||||
overlaps.push({ type: 'hold', id: hold.id });
|
||||
continue;
|
||||
}
|
||||
|
||||
const fromSeq = segmentStops.find(s => s.stationId === journeySegment.departureStationId)?.sequence;
|
||||
const toSeq = segmentStops.find(s => s.stationId === journeySegment.arrivalStationId)?.sequence;
|
||||
|
||||
if (fromSeq !== undefined && toSeq !== undefined) {
|
||||
// Check if any requested segment overlaps with this booking segment
|
||||
for (const reqSeg of segments) {
|
||||
if (reqSeg.fromSequence < toSeq && fromSeq < reqSeg.toSequence) {
|
||||
overlaps.push({ type: 'booking', id: booking.booking.id });
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const js of holdSegs) {
|
||||
const depSeq = js.schedule.stopTimes.find(s => s.stationId === js.departureStationId)?.sequence;
|
||||
const arrSeq = js.schedule.stopTimes.find(s => s.stationId === js.arrivalStationId)?.sequence;
|
||||
if (depSeq !== undefined && arrSeq !== undefined && depSeq < reqTo && reqFrom < arrSeq) {
|
||||
overlaps.push({ type: 'hold', id: hold.id });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2. Active bookings via JourneySegment ────────────────────────────────
|
||||
const bookedSegments = await this.prisma.journeySegment.findMany({
|
||||
where: {
|
||||
scheduleId,
|
||||
seatId,
|
||||
journey: { status: { in: ['CONFIRMED', 'PENDING_PAYMENT'] } },
|
||||
},
|
||||
include: { schedule: { include: { stopTimes: true } } },
|
||||
});
|
||||
|
||||
for (const js of bookedSegments) {
|
||||
const depSeq = js.schedule.stopTimes.find(s => s.stationId === js.departureStationId)?.sequence;
|
||||
const arrSeq = js.schedule.stopTimes.find(s => s.stationId === js.arrivalStationId)?.sequence;
|
||||
if (depSeq !== undefined && arrSeq !== undefined && depSeq < reqTo && reqFrom < arrSeq) {
|
||||
overlaps.push({ type: 'booking', id: js.journeyId });
|
||||
}
|
||||
}
|
||||
|
||||
return overlaps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,14 +19,14 @@ export class TripProgressService {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
// 1. Update trip live status
|
||||
await tx.tripLiveStatus.upsert({
|
||||
where: { tripId },
|
||||
where: { scheduleId: tripId },
|
||||
update: {
|
||||
currentLocationLabel: currentStationId,
|
||||
progressPercent,
|
||||
updatedAt: new Date()
|
||||
},
|
||||
create: {
|
||||
tripId,
|
||||
scheduleId: tripId,
|
||||
state: 'EN_ROUTE',
|
||||
currentLocationLabel: currentStationId,
|
||||
progressPercent,
|
||||
@@ -69,7 +69,7 @@ export class TripProgressService {
|
||||
* Simulate trip progress (for testing/demo)
|
||||
*/
|
||||
async simulateTripProgress(tripId: string) {
|
||||
const trip = await this.prisma.trip.findUnique({
|
||||
const trip = await this.prisma.trainSchedule.findUnique({
|
||||
where: { id: tripId },
|
||||
include: {
|
||||
stopTimes: {
|
||||
@@ -110,13 +110,17 @@ export class TripProgressService {
|
||||
@OnEvent('trip.completed')
|
||||
async handleTripCompleted(payload: { tripId: string }) {
|
||||
// Release all remaining seats for this trip
|
||||
const trip = await this.prisma.trip.findUnique({
|
||||
const trip = await this.prisma.trainSchedule.findUnique({
|
||||
where: { id: payload.tripId },
|
||||
include: {
|
||||
coaches: {
|
||||
coachAssignments: {
|
||||
include: {
|
||||
seats: {
|
||||
where: { status: 'BOOKED' }
|
||||
coach: {
|
||||
include: {
|
||||
seats: {
|
||||
where: { status: 'BOOKED' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,8 +128,8 @@ export class TripProgressService {
|
||||
});
|
||||
|
||||
if (trip) {
|
||||
const bookedSeatIds = trip.coaches.flatMap(coach =>
|
||||
coach.seats.map(seat => seat.id)
|
||||
const bookedSeatIds = trip.coachAssignments.flatMap(assignment =>
|
||||
assignment.coach.seats.map(seat => seat.id)
|
||||
);
|
||||
|
||||
if (bookedSeatIds.length > 0) {
|
||||
@@ -161,7 +165,7 @@ export class TripProgressService {
|
||||
* Get current trip status with seat availability
|
||||
*/
|
||||
async getTripStatus(tripId: string) {
|
||||
const trip = await this.prisma.trip.findUnique({
|
||||
const trip = await this.prisma.trainSchedule.findUnique({
|
||||
where: { id: tripId },
|
||||
include: {
|
||||
liveStatus: true,
|
||||
@@ -169,9 +173,11 @@ export class TripProgressService {
|
||||
include: { station: true },
|
||||
orderBy: { sequence: 'asc' }
|
||||
},
|
||||
coaches: {
|
||||
coachAssignments: {
|
||||
include: {
|
||||
seats: true
|
||||
coach: {
|
||||
include: { seats: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,8 +195,8 @@ export class TripProgressService {
|
||||
blocked: 0
|
||||
};
|
||||
|
||||
trip.coaches.forEach(coach => {
|
||||
coach.seats.forEach(seat => {
|
||||
trip.coachAssignments.forEach(assignment => {
|
||||
assignment.coach.seats.forEach(seat => {
|
||||
seatSummary.total++;
|
||||
const status = seat.status.toLowerCase() as keyof typeof seatSummary;
|
||||
if (status in seatSummary) {
|
||||
|
||||
Reference in New Issue
Block a user