mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Seat hold updates
This commit is contained in:
@@ -35,14 +35,16 @@ async function bootstrap() {
|
||||
Enterprise-grade REST API for the Ethio-Djibouti Railway passenger booking and management platform. Built with NestJS, TypeScript, PostgreSQL, and Prisma ORM.
|
||||
|
||||
## Latest Updates
|
||||
- **Round-Trip Leg Tracking:** returnLegStatus on every booking tracks outbound/return leg usage (NEITHER_USED, OUTBOUND_ONLY, INBOUND_ONLY, BOTH_USED). Gate validation accepts a leg field (OUTBOUND or RETURN).
|
||||
- **TRANSIT & ROUND_TRIP_TRANSIT Booking Types:** Full multi-leg booking support. TRANSIT = single journey via connecting train (single PNR). ROUND_TRIP_TRANSIT = round trip where one or both directions use a connecting train (4 holds, 4 seat sets).
|
||||
- **returnSeatId on Passenger Payloads:** For ROUND_TRIP and ROUND_TRIP_TRANSIT bookings each passenger object must include \`returnSeatId\` (the seat on the return leg-1). Guest and authenticated booking endpoints both enforce this.
|
||||
- **Unified Booking Type Matrix:** bookingType field on Booking now accepts ONE_WAY | ROUND_TRIP | TRANSIT | ROUND_TRIP_TRANSIT across all create endpoints (POST /bookings and POST /bookings/guest).
|
||||
- **Round-Trip Leg Tracking:** returnLegStatus on every booking tracks outbound/return leg usage (NEITHER_USED, OUTBOUND_ONLY, INBOUND_ONLY, BOTH_USED). Gate validation accepts a leg field (OUTBOUND | RETURN | LEG1 | LEG2 | OUTBOUND_LEG1 | OUTBOUND_LEG2 | RETURN_LEG1 | RETURN_LEG2).
|
||||
- **Auto No-Show Detection:** Cron marks OUTBOUND_ONLY 30 min after return departure when return leg was never scanned.
|
||||
- **Offline Batch Validation:** validateOfflineBatch now accepts leg per entry and handles both legs of a round-trip in one batch.
|
||||
- **Booking Filters:** GET /bookings now accepts ?returnLegStatus= to filter no-show/inbound-only cases in back-office.
|
||||
- **Sequence Ordering:** Stations and coaches now sorted by sequence field for consistent UI display.
|
||||
- **User Profile Data:** Gender, DOB, passport, and national ID fields for comprehensive passenger profiles.
|
||||
- **Seat Class Fees:** Premium charges and insurance fees per seat class for transparent pricing.
|
||||
- **Booking Types:** Support for ONE_WAY and ROUND_TRIP booking categories.
|
||||
- **Multi-Currency Display:** Bookings track display currency and converted amounts.
|
||||
- **Ticket Lifecycle:** Tickets now include validatedAt, outboundBoardedAt, returnBoardedAt for complete audit trail.
|
||||
|
||||
@@ -61,11 +63,13 @@ Enterprise-grade REST API for the Ethio-Djibouti Railway passenger booking and m
|
||||
- Multi-segment journey support
|
||||
- Cross-border journeys via Dire Dawa transit (Ethiopia to Djibouti)
|
||||
- Round-trip booking with return journey scheduling
|
||||
- Transit booking (single journey via connecting train, single PNR, single ticket)
|
||||
- Round-trip transit booking (round trip where one or both directions use a connecting train)
|
||||
- Coach type selection with seat class and pricing options
|
||||
- NEW: Booking type tracking (ONE_WAY vs ROUND_TRIP)
|
||||
- NEW: Display currency and converted pricing per booking
|
||||
- NEW: returnLegStatus field tracks which legs of a round-trip were used
|
||||
- NEW: GET /bookings?returnLegStatus=OUTBOUND_ONLY filters no-show returns in back-office
|
||||
- Booking type field: ONE_WAY | ROUND_TRIP | TRANSIT | ROUND_TRIP_TRANSIT
|
||||
- Display currency and converted pricing per booking
|
||||
- returnLegStatus field tracks which legs of a round-trip were used
|
||||
- GET /bookings?returnLegStatus=OUTBOUND_ONLY filters no-show returns in back-office
|
||||
|
||||
### Passenger Verification
|
||||
1. Ethiopian Nationals:
|
||||
@@ -111,21 +115,33 @@ Enterprise-grade REST API for the Ethio-Djibouti Railway passenger booking and m
|
||||
- NEW: Gate validation accepts leg (OUTBOUND or RETURN) for round-trip tickets
|
||||
- NEW: Complete audit trail per leg for compliance and reporting
|
||||
|
||||
### Round-Trip Leg Tracking (NEW)
|
||||
### Booking Type Matrix
|
||||
|
||||
| bookingType | Holds required | Passenger seat fields | Legs in DB |
|
||||
|---|---|---|---|
|
||||
| ONE_WAY | holdId | seatId | 1 |
|
||||
| ROUND_TRIP | holdId + returnHoldId | seatId + returnSeatId | 2 (leg=1 outbound, leg=2 return) |
|
||||
| TRANSIT | holdId + leg2HoldId | seatId + leg2SeatId | 2 (leg=1, leg=2 on same direction) |
|
||||
| ROUND_TRIP_TRANSIT | holdId + leg2HoldId + returnHoldId + returnLeg2HoldId | seatId + leg2SeatId + returnSeatId + returnLeg2SeatId | 4 |
|
||||
|
||||
### Round-Trip Leg Tracking
|
||||
- returnLegStatus on Booking: NOT_APPLICABLE, NEITHER_USED, OUTBOUND_ONLY, INBOUND_ONLY, BOTH_USED
|
||||
- Gate validation POST /tickets/:ref/validate accepts optional leg field (OUTBOUND or RETURN)
|
||||
- Gate validation POST /tickets/:ref/validate accepts optional leg field:
|
||||
- ONE_WAY: omit
|
||||
- TRANSIT: LEG1 | LEG2
|
||||
- ROUND_TRIP: OUTBOUND | RETURN
|
||||
- ROUND_TRIP_TRANSIT: OUTBOUND_LEG1 | OUTBOUND_LEG2 | RETURN_LEG1 | RETURN_LEG2
|
||||
- Auto no-show cron: sets OUTBOUND_ONLY 30 min after return departure when return leg unscanned
|
||||
- Back-office filter: GET /bookings?returnLegStatus=OUTBOUND_ONLY surfaces no-shows
|
||||
- Offline batch: validateOfflineBatch accepts leg per entry, handles both legs of same booking
|
||||
|
||||
### Round-Trip Booking
|
||||
- One-way and round-trip journey options
|
||||
- Flexible return date selection
|
||||
- Combined pricing for outbound + return legs
|
||||
- Separate seat management per leg
|
||||
- Independent modification/cancellation per leg
|
||||
- Return journey tracking and notifications
|
||||
- NEW: Booking type stored for analytics and reporting
|
||||
### Round-Trip & Transit Bookings
|
||||
- ONE_WAY and ROUND_TRIP for direct routes
|
||||
- TRANSIT for single connecting journey (Dire Dawa hub), single PNR
|
||||
- ROUND_TRIP_TRANSIT for round trips via connecting trains
|
||||
- Combined pricing: total = sum of all leg base fares, single promo/loyalty deduction
|
||||
- Separate seat management per leg; each leg stored with its scheduleId and leg number
|
||||
- returnLegStatus tracks which legs have been boarded for no-show management
|
||||
|
||||
### Loyalty Program
|
||||
- 4 tiers: Bronze, Silver, Gold, Platinum
|
||||
@@ -198,26 +214,37 @@ Used for agent, fraud, and reporting endpoints. Requires corporate IAM token.
|
||||
## Passenger Booking Flow
|
||||
|
||||
### Step 1: Search Trips
|
||||
\`POST /search\` with origin, destination, date, passenger counts, and nationality
|
||||
\`POST /search\` with origin, destination, date, passenger counts, and nationality.
|
||||
For round-trips also pass \`journeyType=ROUND_TRIP\` and \`returnDate\`.
|
||||
|
||||
### Step 2: Get Fare Quote
|
||||
\`POST /search/fare-quote\` with passenger counts and display currency
|
||||
\`POST /search/fare-quote\` with passenger counts and display currency.
|
||||
For round-trips also pass \`returnScheduleId\`, \`returnOriginStationId\`, \`returnDestinationStationId\`.
|
||||
|
||||
### Step 3: Passenger Information & Verification
|
||||
**For Ethiopian Passengers:**
|
||||
\`POST /passengers/verify-fayda\` - Automatic Fayda verification for adults (5+ years)
|
||||
\`POST /passengers/verify-fayda\` — Automatic Fayda verification for adults (5+ years)
|
||||
|
||||
**For International Passengers:**
|
||||
\`POST /passengers/register-international\` - Passport information collection
|
||||
\`POST /passengers/register-international\` — Passport information collection
|
||||
|
||||
### Step 4: View Seat Map
|
||||
\`GET /seats/seatmap/{scheduleId}\` - Show available coaches and seats
|
||||
\`GET /seats/seatmap/{scheduleId}\` — Show available coaches and seats.
|
||||
For round-trips, call this twice: once for outbound scheduleId, once for return scheduleId.
|
||||
|
||||
### Step 5: Login & Hold Seats
|
||||
\`POST /auth/login\` then \`POST /seats/hold\` to reserve seats for 15 minutes
|
||||
### Step 5: Hold Seats
|
||||
\`POST /seats/hold\` to reserve seats for 15 minutes.
|
||||
- ONE_WAY / TRANSIT outbound leg: one hold call → \`holdId\`
|
||||
- TRANSIT leg-2: second hold call → \`leg2HoldId\`
|
||||
- ROUND_TRIP return: second hold call → \`returnHoldId\`
|
||||
- ROUND_TRIP_TRANSIT: four hold calls → \`holdId\`, \`leg2HoldId\`, \`returnHoldId\`, \`returnLeg2HoldId\`
|
||||
|
||||
### Step 6: Create Booking
|
||||
\`POST /bookings/guest\` with verified passenger details and held seats
|
||||
Choose the right endpoint and bookingType:
|
||||
- **ONE_WAY** → \`POST /bookings/guest\` or \`POST /bookings\` with \`bookingType: ONE_WAY\`, passenger \`seatId\`
|
||||
- **ROUND_TRIP** → same endpoint with \`bookingType: ROUND_TRIP\`, \`returnScheduleId/returnHoldId/returnOriginStationId/returnDestinationStationId\`, passenger \`seatId + returnSeatId\`
|
||||
- **TRANSIT** → same endpoint with \`bookingType: TRANSIT\`, \`leg2ScheduleId/leg2HoldId/transitStationId/leg2DestinationStationId\`, passenger \`seatId + leg2SeatId\`
|
||||
- **ROUND_TRIP_TRANSIT** → same endpoint with \`bookingType: ROUND_TRIP_TRANSIT\`, all 4 sets of schedule/hold/station fields, passenger \`seatId + leg2SeatId + returnSeatId + returnLeg2SeatId\`
|
||||
|
||||
### Step 7: Process Payment
|
||||
\`POST /payments/telebirr\` (Ethiopian) or \`POST /payments/waafi\` (Djiboutian)
|
||||
@@ -268,7 +295,7 @@ Payment providers send notifications to:
|
||||
.addTag("Agents", "Counter booking, shift management, commission tracking, and reconciliation")
|
||||
.addTag("Audit", "User activity logging, system changes, compliance tracking, and audit trails")
|
||||
.addTag("Auth", "Passenger registration, login, OTP, password reset, and profile management")
|
||||
.addTag("Booking", "Complete booking lifecycle: create, modify, cancel, guest checkout. Supports returnLegStatus filter for round-trip no-show management")
|
||||
.addTag("Booking", "Complete booking lifecycle: create, modify, cancel, guest checkout. Supports ONE_WAY | ROUND_TRIP | TRANSIT | ROUND_TRIP_TRANSIT booking types. returnLegStatus filter for round-trip no-show management")
|
||||
.addTag("Config", "System settings, feature flags, and configuration management")
|
||||
.addTag("Currencies", "Multi-currency support, exchange rates, and currency conversion")
|
||||
.addTag("Dashboard", "Home screen aggregations: trips, loyalty, wallet, notifications")
|
||||
@@ -285,7 +312,6 @@ Payment providers send notifications to:
|
||||
.addTag("Payment Webhooks", "Payment provider webhook handlers and transaction confirmation")
|
||||
.addTag("Promotions", "Promo codes, campaigns, discounts, and redemption tracking")
|
||||
.addTag("Reports", "Revenue analytics, occupancy reports, agent sales, and KPI dashboards")
|
||||
.addTag("Round Trip", "Round-trip bookings, return scheduling, combined pricing, and management (NEW)")
|
||||
.addTag("Routes", "Route templates with ordered stops, fare rules, and baggage allowance")
|
||||
.addTag("Schedule", "Trip schedules, availability windows, status tracking, and timing")
|
||||
.addTag("Search", "Trip search, fare quotes, coach types, and real-time availability")
|
||||
@@ -294,8 +320,8 @@ Payment providers send notifications to:
|
||||
.addTag("Segment-based Seats", "Multi-leg journey seats, segment allocation, and per-leg availability")
|
||||
.addTag("Stations", "Station directory, location data, baggage facilities, and amenities")
|
||||
.addTag("Support", "FAQ management, search, live chat conversations, and ticket resolution")
|
||||
.addTag("Tickets", "QR/barcode generation, PDF tickets, gate validation with per-leg tracking (OUTBOUND/RETURN), and audit trails")
|
||||
.addTag("Transit Stops", "Cross-border journey management, Dire Dawa hub, multi-leg routing (NEW)")
|
||||
.addTag("Tickets", "QR/barcode generation, PDF tickets, gate validation with per-leg tracking (OUTBOUND/RETURN/LEG1/LEG2/OUTBOUND_LEG1/OUTBOUND_LEG2/RETURN_LEG1/RETURN_LEG2), and audit trails")
|
||||
.addTag("Transit Stops", "Cross-border journey management, Dire Dawa hub, TRANSIT and ROUND_TRIP_TRANSIT bookings")
|
||||
.addTag("Wallet", "Balance management, top-ups, withdrawals, and transaction ledger")
|
||||
//.addServer('http://localhost:4000', 'Development')
|
||||
// .addServer("https://api.edr-platform.com", "Production")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Patch, UseGuards, Query, Req, BadRequestException } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse, ApiQuery } from '@nestjs/swagger';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse, ApiQuery, ApiBody } from '@nestjs/swagger';
|
||||
import { BookingsService } from './bookings.service';
|
||||
import { GuestBookingService } from './guest-booking.service';
|
||||
import { CreateBookingDto, ModifyBookingDto, CancelBookingDto } from './bookings.dto';
|
||||
@@ -100,35 +100,153 @@ export class BookingsController {
|
||||
|
||||
@Post('guest')
|
||||
@ApiOperation({
|
||||
summary: 'Create guest booking without login (optional account creation)',
|
||||
description: `Creates a booking without requiring login. Features:
|
||||
|
||||
**Guest Checkout:**
|
||||
- No login required
|
||||
- Contact details from first passenger
|
||||
- Booking confirmation sent to email/phone
|
||||
summary: 'Create guest booking — ONE_WAY | ROUND_TRIP | TRANSIT | ROUND_TRIP_TRANSIT (no login required)',
|
||||
description: `Creates a booking without requiring login. Supports all four booking types.
|
||||
|
||||
**Optional Account Creation:**
|
||||
- Set createAccount=true with password
|
||||
- Account created using first passenger details
|
||||
- Automatic login after booking
|
||||
- Loyalty points and wallet created
|
||||
**bookingType: ONE_WAY (default)**
|
||||
- scheduleId, holdId, originStationId, destinationStationId, seatClassId
|
||||
- passengers[]: { seatId, passengerName, dateOfBirth, idDocumentType, … }
|
||||
|
||||
**Passenger Details Storage:**
|
||||
- savePassengerDetails=true: Save for future bookings
|
||||
- Stored by userId (if account created) or deviceId
|
||||
- Retrieve saved passengers for quick booking
|
||||
**bookingType: ROUND_TRIP**
|
||||
- Above + returnScheduleId, returnHoldId, returnOriginStationId, returnDestinationStationId, returnSeatClassId
|
||||
- passengers[]: each must include returnSeatId (seat on the return leg)
|
||||
|
||||
**Verifayda Verification:**
|
||||
- Ethiopian nationals: National ID verified via Verifayda
|
||||
- Other nationals: Passport details (no verification)
|
||||
**bookingType: TRANSIT**
|
||||
- scheduleId/holdId (leg-1) + leg2ScheduleId, leg2HoldId, transitStationId, leg2DestinationStationId
|
||||
- passengers[]: each must include leg2SeatId
|
||||
|
||||
**Age-Based Pricing:**
|
||||
- ADULT (≥5 years): Full fare
|
||||
- CHILD (<5 years): First child FREE, subsequent children full fare`
|
||||
**bookingType: ROUND_TRIP_TRANSIT**
|
||||
- All TRANSIT outbound fields + returnScheduleId/returnHoldId/returnOriginStationId/returnDestinationStationId/returnLeg2ScheduleId/returnLeg2HoldId/returnTransitStationId/returnLeg2DestinationStationId
|
||||
- passengers[]: each must include leg2SeatId, returnSeatId, returnLeg2SeatId
|
||||
|
||||
**Optional account creation:** set createAccount=true with password — creates account from first passenger details, loyalty + wallet initialised.
|
||||
|
||||
**Verifayda:** Ethiopian nationals verified; international passengers require passportNumber + passportCountry.`
|
||||
})
|
||||
@ApiResponse({ status: 201, description: 'Booking created successfully' })
|
||||
@ApiResponse({ status: 400, description: 'Verifayda verification failed or invalid data' })
|
||||
@ApiBody({
|
||||
type: CreateGuestBookingDto,
|
||||
examples: {
|
||||
ONE_WAY: {
|
||||
summary: 'ONE_WAY — single direct journey (guest)',
|
||||
value: {
|
||||
scheduleId: 'schedule-uuid',
|
||||
holdId: 'hold-uuid',
|
||||
originStationId: 'station-uuid',
|
||||
destinationStationId: 'station-uuid',
|
||||
seatClassId: 'seat-class-uuid',
|
||||
bookingType: 'ONE_WAY',
|
||||
displayCurrency: 'ETB',
|
||||
passengers: [{
|
||||
seatId: 'seat-uuid',
|
||||
passengerName: 'Abebe Kebede',
|
||||
dateOfBirth: '1990-05-15',
|
||||
idDocumentType: 'NATIONAL_ID',
|
||||
idDocumentNumber: 'ET123456789',
|
||||
nationality: 'Ethiopian',
|
||||
phone: '+251911234567',
|
||||
email: 'abebe@email.com',
|
||||
}],
|
||||
savePassengerDetails: true,
|
||||
deviceId: 'device-uuid-123',
|
||||
},
|
||||
},
|
||||
ROUND_TRIP: {
|
||||
summary: 'ROUND_TRIP — outbound + return, single PNR (guest)',
|
||||
value: {
|
||||
scheduleId: 'outbound-schedule-uuid',
|
||||
holdId: 'outbound-hold-uuid',
|
||||
originStationId: 'addis-station-uuid',
|
||||
destinationStationId: 'djibouti-station-uuid',
|
||||
seatClassId: 'seat-class-uuid',
|
||||
bookingType: 'ROUND_TRIP',
|
||||
returnScheduleId: 'return-schedule-uuid',
|
||||
returnHoldId: 'return-hold-uuid',
|
||||
returnOriginStationId: 'djibouti-station-uuid',
|
||||
returnDestinationStationId: 'addis-station-uuid',
|
||||
returnSeatClassId: 'seat-class-uuid',
|
||||
displayCurrency: 'ETB',
|
||||
passengers: [{
|
||||
seatId: 'outbound-seat-uuid',
|
||||
returnSeatId: 'return-seat-uuid',
|
||||
passengerName: 'Abebe Kebede',
|
||||
dateOfBirth: '1990-05-15',
|
||||
idDocumentType: 'NATIONAL_ID',
|
||||
idDocumentNumber: 'ET123456789',
|
||||
nationality: 'Ethiopian',
|
||||
phone: '+251911234567',
|
||||
}],
|
||||
savePassengerDetails: true,
|
||||
deviceId: 'device-uuid-123',
|
||||
},
|
||||
},
|
||||
TRANSIT: {
|
||||
summary: 'TRANSIT — connecting train, single PNR (guest)',
|
||||
value: {
|
||||
scheduleId: 'leg1-schedule-uuid',
|
||||
holdId: 'leg1-hold-uuid',
|
||||
originStationId: 'addis-station-uuid',
|
||||
destinationStationId: 'diredawa-station-uuid',
|
||||
seatClassId: 'seat-class-uuid',
|
||||
bookingType: 'TRANSIT',
|
||||
leg2ScheduleId: 'leg2-schedule-uuid',
|
||||
leg2HoldId: 'leg2-hold-uuid',
|
||||
transitStationId: 'diredawa-station-uuid',
|
||||
leg2DestinationStationId: 'djibouti-station-uuid',
|
||||
displayCurrency: 'ETB',
|
||||
passengers: [{
|
||||
seatId: 'leg1-seat-uuid',
|
||||
leg2SeatId: 'leg2-seat-uuid',
|
||||
passengerName: 'Abebe Kebede',
|
||||
dateOfBirth: '1990-05-15',
|
||||
idDocumentType: 'NATIONAL_ID',
|
||||
idDocumentNumber: 'ET123456789',
|
||||
nationality: 'Ethiopian',
|
||||
phone: '+251911234567',
|
||||
}],
|
||||
deviceId: 'device-uuid-123',
|
||||
},
|
||||
},
|
||||
ROUND_TRIP_TRANSIT: {
|
||||
summary: 'ROUND_TRIP_TRANSIT — round trip via connecting trains, 4 holds (guest)',
|
||||
value: {
|
||||
scheduleId: 'ob-leg1-schedule-uuid',
|
||||
holdId: 'ob-leg1-hold-uuid',
|
||||
originStationId: 'addis-station-uuid',
|
||||
destinationStationId: 'diredawa-station-uuid',
|
||||
seatClassId: 'seat-class-uuid',
|
||||
bookingType: 'ROUND_TRIP_TRANSIT',
|
||||
leg2ScheduleId: 'ob-leg2-schedule-uuid',
|
||||
leg2HoldId: 'ob-leg2-hold-uuid',
|
||||
transitStationId: 'diredawa-station-uuid',
|
||||
leg2DestinationStationId: 'djibouti-station-uuid',
|
||||
returnScheduleId: 'ret-leg1-schedule-uuid',
|
||||
returnHoldId: 'ret-leg1-hold-uuid',
|
||||
returnOriginStationId: 'djibouti-station-uuid',
|
||||
returnDestinationStationId: 'diredawa-station-uuid',
|
||||
returnLeg2ScheduleId: 'ret-leg2-schedule-uuid',
|
||||
returnLeg2HoldId: 'ret-leg2-hold-uuid',
|
||||
returnTransitStationId: 'diredawa-station-uuid',
|
||||
returnLeg2DestinationStationId: 'addis-station-uuid',
|
||||
displayCurrency: 'ETB',
|
||||
passengers: [{
|
||||
seatId: 'ob-leg1-seat-uuid',
|
||||
leg2SeatId: 'ob-leg2-seat-uuid',
|
||||
returnSeatId: 'ret-leg1-seat-uuid',
|
||||
returnLeg2SeatId: 'ret-leg2-seat-uuid',
|
||||
passengerName: 'Abebe Kebede',
|
||||
dateOfBirth: '1990-05-15',
|
||||
idDocumentType: 'NATIONAL_ID',
|
||||
idDocumentNumber: 'ET123456789',
|
||||
nationality: 'Ethiopian',
|
||||
phone: '+251911234567',
|
||||
}],
|
||||
deviceId: 'device-uuid-123',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({ status: 201, description: 'Booking created successfully with fareBreakdown' })
|
||||
@ApiResponse({ status: 400, description: 'Missing required seat IDs for bookingType, or Verifayda verification failed' })
|
||||
createGuest(@Body() dto: CreateGuestBookingDto) {
|
||||
return this.guestService.createGuestBooking(dto);
|
||||
}
|
||||
@@ -147,24 +265,149 @@ export class BookingsController {
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: 'Create booking (one-way or round-trip)',
|
||||
description: `Creates a one-way or round-trip booking for logged-in users.
|
||||
|
||||
ONE_WAY booking:
|
||||
- scheduleId, holdId, originStationId, destinationStationId
|
||||
- passengers: array of PassengerInputDto with seatId
|
||||
- Single PNR, single payment
|
||||
summary: 'Create booking — ONE_WAY | ROUND_TRIP | TRANSIT | ROUND_TRIP_TRANSIT',
|
||||
description: `Creates a booking for a logged-in passenger. bookingType controls which fields are required.
|
||||
|
||||
ROUND_TRIP booking:
|
||||
- Outbound: scheduleId, holdId, originStationId, destinationStationId, seatClassId
|
||||
- Return: returnScheduleId, returnHoldId, returnOriginStationId, returnDestinationStationId, returnSeatClassId
|
||||
- passengers: array of RoundTripPassengerDto with outboundSeatId and returnSeatId
|
||||
- Combined PNR, single payment for both legs
|
||||
- Fare = outbound_fare + return_fare, single total, single promo, single loyalty deduction`
|
||||
**ONE_WAY**
|
||||
- scheduleId, holdId, originStationId, destinationStationId, seatClassId
|
||||
- passengers[]: { seatId, passengerName, dateOfBirth, idDocumentType, … }
|
||||
|
||||
**ROUND_TRIP**
|
||||
- Above + returnScheduleId, returnHoldId, returnOriginStationId, returnDestinationStationId, returnSeatClassId
|
||||
- passengers[]: { seatId (outbound), returnSeatId (return), passengerName, … }
|
||||
- Combined fare = outbound fare + return fare; single promo/loyalty deduction
|
||||
|
||||
**TRANSIT** (connecting train, single PNR)
|
||||
- scheduleId/holdId for leg-1 + leg2ScheduleId, leg2HoldId, transitStationId, leg2DestinationStationId
|
||||
- passengers[]: { seatId (leg-1), leg2SeatId (leg-2), passengerName, … }
|
||||
|
||||
**ROUND_TRIP_TRANSIT** (round trip, each direction via connecting train)
|
||||
- All TRANSIT outbound fields + returnScheduleId, returnHoldId, returnOriginStationId, returnDestinationStationId, returnLeg2ScheduleId, returnLeg2HoldId, returnTransitStationId, returnLeg2DestinationStationId
|
||||
- passengers[]: { seatId, leg2SeatId, returnSeatId, returnLeg2SeatId, passengerName, … }
|
||||
- 4 holds required, 4 seat sets per passenger, single PNR, single payment
|
||||
|
||||
**Age-Based Pricing (all types)**
|
||||
- ADULT (≥5 years): full fare per leg
|
||||
- CHILD (<5 years): first child FREE per booking, subsequent children full fare`
|
||||
})
|
||||
@ApiBody({
|
||||
type: CreateBookingDto,
|
||||
examples: {
|
||||
ONE_WAY: {
|
||||
summary: 'ONE_WAY — single direct journey',
|
||||
value: {
|
||||
passengerId: 'passenger-uuid',
|
||||
scheduleId: 'schedule-uuid',
|
||||
holdId: 'hold-uuid',
|
||||
originStationId: 'station-uuid',
|
||||
destinationStationId: 'station-uuid',
|
||||
seatClassId: 'seat-class-uuid',
|
||||
bookingType: 'ONE_WAY',
|
||||
displayCurrency: 'ETB',
|
||||
passengers: [{
|
||||
seatId: 'seat-uuid',
|
||||
passengerName: 'Abebe Kebede',
|
||||
dateOfBirth: '1990-05-15',
|
||||
idDocumentType: 'NATIONAL_ID',
|
||||
idDocumentNumber: 'ET123456789',
|
||||
nationality: 'Ethiopian',
|
||||
}],
|
||||
},
|
||||
},
|
||||
ROUND_TRIP: {
|
||||
summary: 'ROUND_TRIP — outbound + return, single PNR',
|
||||
value: {
|
||||
passengerId: 'passenger-uuid',
|
||||
scheduleId: 'outbound-schedule-uuid',
|
||||
holdId: 'outbound-hold-uuid',
|
||||
originStationId: 'addis-station-uuid',
|
||||
destinationStationId: 'djibouti-station-uuid',
|
||||
seatClassId: 'seat-class-uuid',
|
||||
bookingType: 'ROUND_TRIP',
|
||||
returnScheduleId: 'return-schedule-uuid',
|
||||
returnHoldId: 'return-hold-uuid',
|
||||
returnOriginStationId: 'djibouti-station-uuid',
|
||||
returnDestinationStationId: 'addis-station-uuid',
|
||||
returnSeatClassId: 'seat-class-uuid',
|
||||
displayCurrency: 'ETB',
|
||||
passengers: [{
|
||||
seatId: 'outbound-seat-uuid',
|
||||
returnSeatId: 'return-seat-uuid',
|
||||
passengerName: 'Abebe Kebede',
|
||||
dateOfBirth: '1990-05-15',
|
||||
idDocumentType: 'NATIONAL_ID',
|
||||
idDocumentNumber: 'ET123456789',
|
||||
nationality: 'Ethiopian',
|
||||
}],
|
||||
},
|
||||
},
|
||||
TRANSIT: {
|
||||
summary: 'TRANSIT — connecting train, single PNR',
|
||||
value: {
|
||||
passengerId: 'passenger-uuid',
|
||||
scheduleId: 'leg1-schedule-uuid',
|
||||
holdId: 'leg1-hold-uuid',
|
||||
originStationId: 'addis-station-uuid',
|
||||
destinationStationId: 'diredawa-station-uuid',
|
||||
seatClassId: 'seat-class-uuid',
|
||||
bookingType: 'TRANSIT',
|
||||
leg2ScheduleId: 'leg2-schedule-uuid',
|
||||
leg2HoldId: 'leg2-hold-uuid',
|
||||
transitStationId: 'diredawa-station-uuid',
|
||||
leg2DestinationStationId: 'djibouti-station-uuid',
|
||||
displayCurrency: 'ETB',
|
||||
passengers: [{
|
||||
seatId: 'leg1-seat-uuid',
|
||||
leg2SeatId: 'leg2-seat-uuid',
|
||||
passengerName: 'Abebe Kebede',
|
||||
dateOfBirth: '1990-05-15',
|
||||
idDocumentType: 'NATIONAL_ID',
|
||||
idDocumentNumber: 'ET123456789',
|
||||
nationality: 'Ethiopian',
|
||||
}],
|
||||
},
|
||||
},
|
||||
ROUND_TRIP_TRANSIT: {
|
||||
summary: 'ROUND_TRIP_TRANSIT — round trip via connecting trains, 4 holds',
|
||||
value: {
|
||||
passengerId: 'passenger-uuid',
|
||||
scheduleId: 'ob-leg1-schedule-uuid',
|
||||
holdId: 'ob-leg1-hold-uuid',
|
||||
originStationId: 'addis-station-uuid',
|
||||
destinationStationId: 'diredawa-station-uuid',
|
||||
seatClassId: 'seat-class-uuid',
|
||||
bookingType: 'ROUND_TRIP_TRANSIT',
|
||||
leg2ScheduleId: 'ob-leg2-schedule-uuid',
|
||||
leg2HoldId: 'ob-leg2-hold-uuid',
|
||||
transitStationId: 'diredawa-station-uuid',
|
||||
leg2DestinationStationId: 'djibouti-station-uuid',
|
||||
returnScheduleId: 'ret-leg1-schedule-uuid',
|
||||
returnHoldId: 'ret-leg1-hold-uuid',
|
||||
returnOriginStationId: 'djibouti-station-uuid',
|
||||
returnDestinationStationId: 'diredawa-station-uuid',
|
||||
returnLeg2ScheduleId: 'ret-leg2-schedule-uuid',
|
||||
returnLeg2HoldId: 'ret-leg2-hold-uuid',
|
||||
returnTransitStationId: 'diredawa-station-uuid',
|
||||
returnLeg2DestinationStationId: 'addis-station-uuid',
|
||||
displayCurrency: 'ETB',
|
||||
passengers: [{
|
||||
seatId: 'ob-leg1-seat-uuid',
|
||||
leg2SeatId: 'ob-leg2-seat-uuid',
|
||||
returnSeatId: 'ret-leg1-seat-uuid',
|
||||
returnLeg2SeatId: 'ret-leg2-seat-uuid',
|
||||
passengerName: 'Abebe Kebede',
|
||||
dateOfBirth: '1990-05-15',
|
||||
idDocumentType: 'NATIONAL_ID',
|
||||
idDocumentNumber: 'ET123456789',
|
||||
nationality: 'Ethiopian',
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({ status: 201, description: 'Booking created with fare breakdown' })
|
||||
@ApiResponse({ status: 400, description: 'Verifayda verification failed or invalid passenger data' })
|
||||
@ApiResponse({ status: 404, description: 'Trip or seat hold not found' })
|
||||
@ApiResponse({ status: 400, description: 'Missing required fields for bookingType, or Verifayda verification failed' })
|
||||
@ApiResponse({ status: 404, description: 'Schedule or seat hold not found' })
|
||||
create(@Body() dto: CreateBookingDto) {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Currency, IdDocumentType } from '@prisma/client';
|
||||
|
||||
export class PassengerInputDto {
|
||||
@ApiProperty() @IsString() seatId: string;
|
||||
@ApiPropertyOptional({ description: '**TRANSIT / ROUND_TRIP_TRANSIT:** Seat ID on leg-2 schedule' }) @IsOptional() @IsString() leg2SeatId?: string;
|
||||
@ApiProperty({ example: 'seat-uuid', description: 'Outbound / leg-1 seat ID (all booking types)' }) @IsString() seatId: string;
|
||||
@ApiPropertyOptional({ example: 'leg2-seat-uuid', description: '**TRANSIT / ROUND_TRIP_TRANSIT:** Outbound leg-2 seat ID' }) @IsOptional() @IsString() leg2SeatId?: string;
|
||||
@ApiPropertyOptional({ example: 'return-seat-uuid', description: '**ROUND_TRIP / ROUND_TRIP_TRANSIT:** Return leg-1 seat ID' }) @IsOptional() @IsString() returnSeatId?: string;
|
||||
@ApiPropertyOptional({ example: 'ret-leg2-seat-uuid', description: '**ROUND_TRIP_TRANSIT:** Return leg-2 seat ID' }) @IsOptional() @IsString() returnLeg2SeatId?: string;
|
||||
@ApiProperty({ example: 'Abebe Kebede' }) @IsString() passengerName: string;
|
||||
@ApiProperty({ example: '1990-05-15', description: 'Date of birth (YYYY-MM-DD) for age calculation. Age <5 = CHILD (first free), Age ≥5 = ADULT (full fare)' }) @IsDateString() dateOfBirth: string;
|
||||
@ApiProperty({ example: 'NATIONAL_ID', enum: IdDocumentType, description: 'NATIONAL_ID for Ethiopians (Verifayda verified), PASSPORT for others' }) @IsEnum(IdDocumentType) idDocumentType: IdDocumentType;
|
||||
@@ -76,16 +78,16 @@ export class CreateBookingDto {
|
||||
@ApiProperty({ description: 'Passenger ID' })
|
||||
@IsString() passengerId: string;
|
||||
|
||||
@ApiProperty({ description: 'Outbound schedule ID' })
|
||||
@ApiProperty({ description: 'Outbound / leg-1 schedule ID' })
|
||||
@IsString() scheduleId: string;
|
||||
|
||||
@ApiProperty({ description: 'Outbound seat hold ID' })
|
||||
@ApiProperty({ description: 'Outbound / leg-1 seat hold ID' })
|
||||
@IsString() holdId: string;
|
||||
|
||||
@ApiProperty({ example: 'station-uuid', description: 'Outbound origin station UUID (must match the hold)' })
|
||||
@ApiProperty({ example: 'station-uuid', description: 'Outbound origin station UUID' })
|
||||
@IsString() originStationId: string;
|
||||
|
||||
@ApiProperty({ example: 'station-uuid', description: 'Outbound destination station UUID (must match the hold)' })
|
||||
@ApiProperty({ example: 'station-uuid', description: 'Outbound destination station UUID' })
|
||||
@IsString() destinationStationId: string;
|
||||
|
||||
@ApiProperty({ example: 'seat-class-uuid', description: 'Outbound seat class UUID (Economy Regular, Economy Bed, VIP Bed)' })
|
||||
@@ -94,14 +96,32 @@ export class CreateBookingDto {
|
||||
@ApiProperty({
|
||||
example: 'ONE_WAY',
|
||||
enum: ['ONE_WAY', 'ROUND_TRIP', 'TRANSIT', 'ROUND_TRIP_TRANSIT'],
|
||||
description: `Booking type:\n\n**ONE_WAY:** Single journey\n\n**ROUND_TRIP:** Outbound + return, single PNR\n\n**TRANSIT:** Single journey via connecting train, single PNR, single ticket\n\n**ROUND_TRIP_TRANSIT:** Round trip where one or both directions use a connecting train`,
|
||||
description: `Booking type:
|
||||
|
||||
**ONE_WAY:** Single direct journey — needs: scheduleId, holdId. Passenger: seatId.
|
||||
|
||||
**ROUND_TRIP:** Outbound + return, single PNR — needs above + returnScheduleId, returnHoldId, returnOriginStationId, returnDestinationStationId. Passenger: seatId + returnSeatId.
|
||||
|
||||
**TRANSIT:** Single journey via connecting train, single PNR — needs above + leg2ScheduleId, leg2HoldId, transitStationId, leg2DestinationStationId. Passenger: seatId + leg2SeatId.
|
||||
|
||||
**ROUND_TRIP_TRANSIT:** Round trip via connecting trains — needs all 4 hold sets + all station fields. Passenger: seatId + leg2SeatId + returnSeatId + returnLeg2SeatId.`,
|
||||
default: 'ONE_WAY'
|
||||
})
|
||||
@IsOptional() @IsString() bookingType?: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: [PassengerInputDto],
|
||||
description: `Passenger array - type depends on bookingType:\n\n**For ONE_WAY:** PassengerInputDto[]\n- Each passenger has: seatId, passengerName, dateOfBirth, etc.\n\n**For ROUND_TRIP:** RoundTripPassengerDto[]\n- Each passenger has: outboundSeatId, returnSeatId, passengerName, dateOfBirth, etc.\n\n**Age-based pricing:** First child (<5 years) travels FREE, subsequent children pay full fare`
|
||||
description: `Passenger array — required seat fields vary by bookingType:
|
||||
|
||||
**ONE_WAY:** { seatId, passengerName, dateOfBirth, idDocumentType, … }
|
||||
|
||||
**ROUND_TRIP:** { seatId (outbound leg-1), returnSeatId (return leg-1), passengerName, … }
|
||||
|
||||
**TRANSIT:** { seatId (leg-1), leg2SeatId (leg-2), passengerName, … }
|
||||
|
||||
**ROUND_TRIP_TRANSIT:** { seatId, leg2SeatId, returnSeatId, returnLeg2SeatId, passengerName, … }
|
||||
|
||||
**Age-based pricing:** First child (<5 years) travels FREE, subsequent children pay full fare.`
|
||||
})
|
||||
@IsArray() @ValidateNested({ each: true }) @Type(() => PassengerInputDto)
|
||||
passengers: PassengerInputDto[];
|
||||
|
||||
@@ -4,16 +4,16 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Currency, IdDocumentType } from '@prisma/client';
|
||||
|
||||
export class GuestPassengerDto {
|
||||
@ApiProperty({ example: 'seat-id-uuid', description: 'Outbound seat ID (or only seat for ONE_WAY)' })
|
||||
@ApiProperty({ example: 'seat-uuid', description: 'Outbound / leg-1 seat ID (all booking types)' })
|
||||
@IsString() seatId: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'seat-id-uuid', description: 'Return seat ID (ROUND_TRIP / ROUND_TRIP_TRANSIT outbound leg-1)' })
|
||||
@ApiPropertyOptional({ example: 'return-seat-uuid', description: '**ROUND_TRIP / ROUND_TRIP_TRANSIT:** Return leg-1 seat ID. Required for ROUND_TRIP and ROUND_TRIP_TRANSIT.' })
|
||||
@IsOptional() @IsString() returnSeatId?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'seat-id-uuid', description: 'Leg-2 seat ID (TRANSIT / ROUND_TRIP_TRANSIT outbound leg-2)' })
|
||||
@ApiPropertyOptional({ example: 'leg2-seat-uuid', description: '**TRANSIT / ROUND_TRIP_TRANSIT:** Outbound leg-2 seat ID. Required for TRANSIT and ROUND_TRIP_TRANSIT.' })
|
||||
@IsOptional() @IsString() leg2SeatId?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'seat-id-uuid', description: 'ROUND_TRIP_TRANSIT: return journey leg-2 seat ID' })
|
||||
@ApiPropertyOptional({ example: 'ret-leg2-seat-uuid', description: '**ROUND_TRIP_TRANSIT:** Return leg-2 seat ID. Required for ROUND_TRIP_TRANSIT.' })
|
||||
@IsOptional() @IsString() returnLeg2SeatId?: string;
|
||||
|
||||
@ApiProperty({ example: 'Abebe Kebede' })
|
||||
@@ -45,7 +45,16 @@ export class GuestPassengerDto {
|
||||
}
|
||||
|
||||
export class CreateGuestBookingDto {
|
||||
@ApiPropertyOptional({ example: 'ONE_WAY', enum: ['ONE_WAY', 'ROUND_TRIP', 'TRANSIT', 'ROUND_TRIP_TRANSIT'], default: 'ONE_WAY' })
|
||||
@ApiPropertyOptional({
|
||||
example: 'ONE_WAY',
|
||||
enum: ['ONE_WAY', 'ROUND_TRIP', 'TRANSIT', 'ROUND_TRIP_TRANSIT'],
|
||||
default: 'ONE_WAY',
|
||||
description: `Booking type:
|
||||
**ONE_WAY:** scheduleId + holdId. Passenger: seatId.
|
||||
**ROUND_TRIP:** above + returnScheduleId/returnHoldId/returnOriginStationId/returnDestinationStationId. Passenger: seatId + returnSeatId.
|
||||
**TRANSIT:** above + leg2ScheduleId/leg2HoldId/transitStationId/leg2DestinationStationId. Passenger: seatId + leg2SeatId.
|
||||
**ROUND_TRIP_TRANSIT:** all 4 hold sets + all station fields. Passenger: seatId + leg2SeatId + returnSeatId + returnLeg2SeatId.`
|
||||
})
|
||||
@IsOptional() @IsString() bookingType?: 'ONE_WAY' | 'ROUND_TRIP' | 'TRANSIT' | 'ROUND_TRIP_TRANSIT';
|
||||
|
||||
@ApiProperty({ example: 'schedule-uuid', description: 'Outbound / leg-1 schedule UUID' })
|
||||
@@ -108,7 +117,14 @@ export class CreateGuestBookingDto {
|
||||
@ApiPropertyOptional({ example: 'seat-class-uuid', description: 'ROUND_TRIP_TRANSIT: return leg-2 seat class UUID' })
|
||||
@IsOptional() @IsString() returnLeg2SeatClassId?: string;
|
||||
|
||||
@ApiProperty({ type: [GuestPassengerDto], description: 'Array of passengers. For ROUND_TRIP each passenger must include returnSeatId.' })
|
||||
@ApiProperty({
|
||||
type: [GuestPassengerDto],
|
||||
description: `Passenger array. Required seat fields vary by bookingType:
|
||||
- ONE_WAY: seatId
|
||||
- ROUND_TRIP: seatId + returnSeatId
|
||||
- TRANSIT: seatId + leg2SeatId
|
||||
- ROUND_TRIP_TRANSIT: seatId + leg2SeatId + returnSeatId + returnLeg2SeatId`
|
||||
})
|
||||
@IsArray() @ValidateNested({ each: true }) @Type(() => GuestPassengerDto) passengers: GuestPassengerDto[];
|
||||
|
||||
@ApiPropertyOptional({ example: 'WEEKEND15' })
|
||||
|
||||
@@ -285,6 +285,7 @@ export default function ReviewPage() {
|
||||
const isEthiopian = p.nationality === 'ETHIOPIAN' || p.nationality === 'Ethiopian';
|
||||
return {
|
||||
seatId: isRoundTrip ? (p as any).outboundSeatId : (p.seatId || ''),
|
||||
...(isRoundTrip && { returnSeatId: (p as any).inboundSeatId || '' }),
|
||||
passengerName: p.name,
|
||||
dateOfBirth: p.dateOfBirth,
|
||||
idDocumentType: isEthiopian ? 'NATIONAL_ID' : 'PASSPORT',
|
||||
@@ -323,6 +324,7 @@ export default function ReviewPage() {
|
||||
const isEthiopian = p.nationality === 'ETHIOPIAN' || p.nationality === 'Ethiopian';
|
||||
return {
|
||||
seatId: isRoundTrip ? (p as any).outboundSeatId : (p.seatId || ''),
|
||||
...(isRoundTrip && { returnSeatId: (p as any).inboundSeatId || '' }),
|
||||
passengerName: p.name,
|
||||
dateOfBirth: p.dateOfBirth,
|
||||
idDocumentType: isEthiopian ? 'NATIONAL_ID' : 'PASSPORT',
|
||||
|
||||
Reference in New Issue
Block a user