mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
9.3 KiB
9.3 KiB
Segment-Based Seat Reservation
Overview
This implementation introduces segment-based seat reservation and release logic for the Ethio-Djibouti Railway passenger booking system. It allows passengers to book partial journeys while ensuring optimal seat utilization through automatic release when passengers reach their destinations.
Key Features
- Segment-based reservations: Book seats for specific route segments (e.g., Addis Ababa → Dire Dawa)
- Automatic seat release: Seats are released when passengers reach their destination
- Concurrency control: Database transactions ensure consistency
- Real-time updates: Event-driven notifications for seat availability changes
- Hold expiration: Automatic cleanup of expired seat holds
Route Example
Full Route: Addis Ababa → Adama → Awash → Dire Dawa → Djibouti
Passenger Journey: Addis Ababa → Dire Dawa
- Segments: [Addis→Adama, Adama→Awash, Awash→Dire Dawa]
- Seat Status: HELD → BOOKED → AVAILABLE (when reaching Dire Dawa)
Database Schema Integration
Core Tables Used
-- Trip and route structure
Trip, TripStopTime, Station
-- Seat management
Seat, SeatHold, BookingSeat, Booking
-- Journey tracking
JourneySegment (stores segment-to-seat mapping)
-- Real-time progress
TripLiveStatus (triggers seat releases)
Key Enums
enum SeatStatus {
AVAILABLE = 'AVAILABLE',
HELD = 'HELD',
BOOKED = 'BOOKED',
BLOCKED = 'BLOCKED'
}
API Endpoints
1. Check Seat Availability
GET /segments/seats/availability?tripId=trip_001&originStationId=st_ADD&destinationStationId=st_DRE
Response:
{
"segments": [
{ "fromName": "Addis Ababa", "toName": "Adama", "fromSequence": 0, "toSequence": 1 },
{ "fromName": "Adama", "toName": "Awash", "fromSequence": 1, "toSequence": 2 },
{ "fromName": "Awash", "toName": "Dire Dawa", "fromSequence": 2, "toSequence": 3 }
],
"availableSeats": [
{ "id": "seat_1", "label": "1A", "coach": "A", "serviceClass": "ECONOMY" }
],
"totalAvailable": 1
}
2. Hold Seats
POST /segments/seats/hold
Request:
{
"tripId": "trip_001",
"seatIds": ["seat_1", "seat_2"],
"passengerId": "passenger_123",
"originStationId": "st_ADD",
"destinationStationId": "st_DRE",
"fareQuoteId": "quote_456"
}
Response:
{
"holdId": "hold_789",
"expiresAt": "2024-01-15T10:10:00Z",
"segments": [
{ "fromName": "Addis Ababa", "toName": "Adama" },
{ "fromName": "Adama", "toName": "Awash" },
{ "fromName": "Awash", "toName": "Dire Dawa" }
],
"seats": ["seat_1", "seat_2"]
}
3. Confirm Booking
POST /segments/seats/confirm
Request:
{
"holdId": "hold_789",
"bookingId": "booking_123"
}
4. Release Seats (Automatic)
POST /segments/seats/release
Request:
{
"tripId": "trip_001",
"currentStationId": "st_DRE"
}
Database Transaction Flow
1. Seat Hold Transaction
async function holdSeatsTransaction(request: SeatHoldRequest) {
return prisma.$transaction(async (tx) => {
// 1. Validate seat availability
const seats = await tx.seat.findMany({
where: { id: { in: request.seatIds } }
});
// 2. Check for overlapping reservations
for (const seatId of request.seatIds) {
const overlaps = await checkOverlaps(tx, tripId, seatId, segments);
if (overlaps.length > 0) throw new ConflictException();
}
// 3. Create hold record
const hold = await tx.seatHold.create({
data: {
tripId: request.tripId,
seatIds: request.seatIds,
passengerId: request.passengerId,
expiresAt: new Date(Date.now() + 10 * 60 * 1000)
}
});
// 4. Update seat status
await tx.seat.updateMany({
where: { id: { in: request.seatIds } },
data: { status: 'HELD', heldUntil: hold.expiresAt }
});
return hold;
});
}
2. Booking Confirmation Transaction
async function confirmBookingTransaction(holdId: string, bookingId: string) {
return prisma.$transaction(async (tx) => {
// 1. Validate hold
const hold = await tx.seatHold.findUnique({ where: { id: holdId } });
if (!hold || hold.expiresAt < new Date()) {
throw new BadRequestException('Hold expired');
}
// 2. Create journey segments
for (const seatId of hold.seatIds) {
for (let i = 0; i < segments.length; i++) {
await tx.journeySegment.create({
data: {
journeyId: bookingId,
tripId: hold.tripId,
segmentOrder: i + 1,
seatId,
departureStationId: segments[i].fromStationId,
arrivalStationId: segments[i].toStationId
}
});
}
}
// 3. Update seat status to BOOKED
await tx.seat.updateMany({
where: { id: { in: hold.seatIds } },
data: { status: 'BOOKED', heldUntil: null }
});
// 4. Delete hold
await tx.seatHold.delete({ where: { id: holdId } });
return { bookingId, confirmedSeats: hold.seatIds };
});
}
3. Seat Release Transaction
async function releaseSeatsTransaction(tripId: string, currentStationId: string) {
return prisma.$transaction(async (tx) => {
// 1. Find completed journey segments
const completedSegments = await tx.journeySegment.findMany({
where: { tripId, arrivalStationId: currentStationId },
include: { journey: { include: { journeySegments: true } } }
});
const seatsToRelease = [];
// 2. Check if passenger's entire journey is complete
for (const segment of completedSegments) {
const allSegments = segment.journey.journeySegments
.filter(js => js.seatId === segment.seatId);
const maxOrder = Math.max(...allSegments.map(js => js.segmentOrder));
if (segment.segmentOrder === maxOrder) {
seatsToRelease.push(segment.seatId);
}
}
// 3. Release seats
if (seatsToRelease.length > 0) {
await tx.seat.updateMany({
where: { id: { in: seatsToRelease } },
data: { status: 'AVAILABLE' }
});
}
return { releasedSeats: seatsToRelease };
});
}
Real-Time Integration
Trip Progress Updates
// When train reaches a station
await tripProgressService.updateTripProgress(tripId, stationId, progressPercent);
// Automatically triggers seat release
this.eventEmitter.emit('trip.station.arrived', {
tripId,
stationId,
stationName: 'Dire Dawa'
});
Event Listeners
@OnEvent('trip.station.arrived')
async handleStationArrival(payload: { tripId: string, stationId: string }) {
await this.enhancedSeatsService.releaseSeats(payload.tripId, payload.stationId);
}
@OnEvent('seats.released')
async handleSeatsReleased(payload: { releasedSeats: string[] }) {
// Notify waiting passengers about newly available seats
this.notificationService.notifyAvailability(payload.releasedSeats);
}
Background Jobs
Hold Expiration (Every Minute)
@Cron(CronExpression.EVERY_MINUTE)
async expireHolds() {
const expired = await this.prisma.seatHold.findMany({
where: { expiresAt: { lt: new Date() } }
});
// Release expired seats
await this.prisma.seat.updateMany({
where: { id: { in: expiredSeatIds } },
data: { status: 'AVAILABLE', heldUntil: null }
});
}
Usage Examples
Complete Booking Flow
// 1. Check availability
const availability = await segmentSeatsService.getSeatAvailability(
'trip_001', 'st_ADD', 'st_DRE'
);
// 2. Hold seats (10-minute expiry)
const hold = await segmentSeatsService.holdSeats({
tripId: 'trip_001',
seatIds: ['seat_1'],
passengerId: 'passenger_123',
originStationId: 'st_ADD',
destinationStationId: 'st_DRE'
});
// 3. Process payment...
await paymentService.processPayment(bookingId);
// 4. Confirm booking
const booking = await segmentSeatsService.confirmBooking({
holdId: hold.holdId,
bookingId: 'booking_456'
});
// 5. Seats automatically released when train reaches Dire Dawa
Error Handling
- Seat Conflicts:
ConflictExceptionwhen seats overlap with existing reservations - Expired Holds:
BadRequestExceptionwhen trying to confirm expired holds - Invalid Segments:
BadRequestExceptionfor invalid origin/destination combinations - Transaction Rollback: Automatic rollback on any failure within transactions
Performance Considerations
- Indexing: Ensure indexes on
tripId,seatId,stationId,expiresAt - Batch Operations: Use
updateManyfor bulk seat status updates - Event Queuing: Consider message queues for high-volume seat release events
- Caching: Cache frequently accessed route/station data
Integration Notes
- Existing Booking System: Extends current booking flow with segment awareness
- Payment Integration: Hold expiry provides payment processing window
- Real-time Updates: WebSocket notifications for seat availability changes
- Mobile Apps: Push notifications when seats become available on preferred routes
- Analytics: Track seat utilization patterns by segment for route optimization
Testing
Run the example booking flow:
cd apps/edr-passenger-api
npx ts-node src/modules/segments/booking-flow-example.ts
This demonstrates the complete segment-based reservation lifecycle with database transactions and real-time seat releases.