Files
edr-platform/apps/edr-passenger-api/SEGMENT_BASED_SEATS.md
2026-05-17 11:11:39 +03:00

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: ConflictException when seats overlap with existing reservations
  • Expired Holds: BadRequestException when trying to confirm expired holds
  • Invalid Segments: BadRequestException for 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 updateMany for bulk seat status updates
  • Event Queuing: Consider message queues for high-volume seat release events
  • Caching: Cache frequently accessed route/station data

Integration Notes

  1. Existing Booking System: Extends current booking flow with segment awareness
  2. Payment Integration: Hold expiry provides payment processing window
  3. Real-time Updates: WebSocket notifications for seat availability changes
  4. Mobile Apps: Push notifications when seats become available on preferred routes
  5. 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.