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

364 lines
9.3 KiB
Markdown

# 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
```sql
-- 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
```typescript
enum SeatStatus {
AVAILABLE = 'AVAILABLE',
HELD = 'HELD',
BOOKED = 'BOOKED',
BLOCKED = 'BLOCKED'
}
```
## API Endpoints
### 1. Check Seat Availability
```http
GET /segments/seats/availability?tripId=trip_001&originStationId=st_ADD&destinationStationId=st_DRE
```
**Response:**
```json
{
"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
```http
POST /segments/seats/hold
```
**Request:**
```json
{
"tripId": "trip_001",
"seatIds": ["seat_1", "seat_2"],
"passengerId": "passenger_123",
"originStationId": "st_ADD",
"destinationStationId": "st_DRE",
"fareQuoteId": "quote_456"
}
```
**Response:**
```json
{
"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
```http
POST /segments/seats/confirm
```
**Request:**
```json
{
"holdId": "hold_789",
"bookingId": "booking_123"
}
```
### 4. Release Seats (Automatic)
```http
POST /segments/seats/release
```
**Request:**
```json
{
"tripId": "trip_001",
"currentStationId": "st_DRE"
}
```
## Database Transaction Flow
### 1. Seat Hold Transaction
```typescript
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
```typescript
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
```typescript
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
```typescript
// 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
```typescript
@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)
```typescript
@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
```typescript
// 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:
```bash
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.