mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
364 lines
9.3 KiB
Markdown
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. |