Files
edr-platform/apps/edr-passenger-api/src/main.ts
2026-07-08 09:01:07 +03:00

431 lines
24 KiB
TypeScript

// Load .env into process.env BEFORE the module graph is built. Required because the @tria-plc IAM
// modules read process.env at module-load time (e.g. MinioModule.register reads MINIO_ENDPOINT),
// which happens before ConfigModule.forRoot() would populate it. Must be the very first import.
import "dotenv/config";
import "reflect-metadata";
import { NestFactory } from "@nestjs/core";
import { Logger, ValidationPipe, VersioningType } from "@nestjs/common";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import helmet from "helmet";
import { AppModule } from "./app.module";
import { HttpExceptionFilter } from "./common/filters/http-exception.filter";
import { ResponseTransformInterceptor } from "./common/interceptors/response-transform.interceptor";
import { SessionActivityInterceptor } from "./common/interceptors/session-activity.interceptor";
// Set timezone to Africa/Addis_Ababa (EAT - UTC+3) for Ethiopian Railway operations
process.env.TZ = 'Africa/Addis_Ababa';
// Safety guard: prevent insecure TLS from being enabled in production
if (process.env.NODE_ENV === 'production' && process.env.WAAFI_INSECURE_TLS === 'true') {
throw new Error('WAAFI_INSECURE_TLS=true is not allowed in production');
}
async function bootstrap() {
// rawBody: true buffers the unparsed request body onto req.rawBody so webhook handlers
// (e.g. Waafi HMAC verification) can sign over the exact bytes the provider signed.
const app = await NestFactory.create(AppModule, { rawBody: true });
// Security headers
app.use(helmet());
// URI versioning: the @tria-plc IAM controllers declare `version: "1"` so they register under
// `/v1/...` (e.g. /v1/auth/login). Passenger controllers declare no version, so they stay
// version-neutral at their existing paths (e.g. /search, /bookings) — unchanged for the frontend.
app.enableVersioning({ type: VersioningType.URI });
app.enableCors({
origin: [
process.env.PORTAL_URL ?? "http://localhost:5174",
process.env.BACK_OFFICE_URL ?? "http://localhost:5184",
],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'Accept-Language', 'X-Request-ID'],
credentials: true,
});
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(
new ResponseTransformInterceptor(),
app.get(SessionActivityInterceptor),
);
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true, forbidUnknownValues: false }));
if (process.env.NODE_ENV !== 'production') {
const config = new DocumentBuilder()
.setTitle("EDR Passenger API")
.setDescription(
`# Ethio-Djibouti Railway Passenger Booking API
## Overview
Enterprise-grade REST API for the Ethio-Djibouti Railway passenger booking and management platform. Built with NestJS, TypeScript, PostgreSQL, and Prisma ORM.
## Latest Updates
- **Enhanced Module Coverage:** Complete API coverage with 25+ core modules including System Config, Excess Luggage, Packages, and comprehensive CRUD operations across all entities.
- **Health Check Endpoints:** Three public probes added under \`/health\`. Liveness (\`GET /health\`), readiness with live DB ping (\`GET /health/ready\`), and app info (\`GET /health/info\`). All are exempt from rate limiting.
- **Rate Limiting:** Global throttle enforced via ThrottlerGuard with three named tiers: auth (5 req/min on \`/auth\` and \`/fayda/verification\`), strict (20 req/min on \`/bookings\`, \`/passengers\`, \`/payments\`, \`/wallet\`), default (100 req/min everywhere else). Health probes, webhook handlers, and internal service endpoints are exempt.
- **Boarding Pass on Gate Validation:** Every successful gate validation at \`POST /tickets/:ref/validate\` now automatically delivers a boarding pass to the passenger via email (full HTML with QR code, route, seat table) and SMS (compact text with ref, route, seats, barcode). The leg label (OUTBOUND, RETURN, LEG1, etc.) is included so passengers know which boarding it covers.
- **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.
- **Multi-Currency Display:** Bookings track display currency and converted amounts.
- **Ticket Lifecycle:** Tickets now include validatedAt, outboundBoardedAt, returnBoardedAt for complete audit trail.
## Key Features
### Booking Lifecycle
- Search trips with real-time availability
- Age-based passenger categorization (Adult 5+ years, Child under 5)
- Nationality-based verification (Ethiopian Fayda, International Passport)
- Passenger information collection with verification
- Coach and seat selection with real-time availability
- Seat holding (15-minute expiry)
- Create bookings with verified passenger data
- Modify bookings (seat changes, passenger updates)
- Cancel bookings with automatic refunds
- 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
- 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:
- Automatic Fayda verification for adults (5+ years)
- Real-time national ID verification via government database
- Retrieves verified passenger data (name, DOB, gender)
- National IDs not stored (policy compliant)
2. International Passengers:
- Passport information collection
- Manual verification for Djiboutian and other nationals
- No government database verification required
### Age-Based Pricing
- ADULT (5+ years): Pay 100% of base fare
- CHILD (under 5): First child travels FREE, subsequent children pay 100%
- Automatic age calculation from date of birth
- Example: 2 adults + 3 children = 4x base fare (first child free)
- NEW: Premium charges and insurance fees per seat class
- NEW: Transparent fee breakdown in pricing calculations
### Payment Integration
1. Ethiopian Payment Methods: Telebirr, CBE Birr
2. Djiboutian Payment Methods: Waafi
3. International Payment Methods: Card, Wallet
### Seat Management
- Real-time seat availability by coach and class
- Seat holds with 15-minute expiry
- Auto-assign seats with contiguous algorithm
- Seat blocking for maintenance
- Coach-level seat maps (ordered by sequence)
- Class-based seating (Economy Regular, Economy Bed, VIP Bed)
- NEW: Sequence-based coach ordering for consistent display
### Ticketing
- QR code and barcode generation
- PDF ticket generation
- Gate validation with audit logs
- Offline validation support
- Multi-passenger tickets
- Ticket lifecycle tracking (validatedAt, outboundBoardedAt, returnBoardedAt timestamps)
- Gate validation accepts leg (OUTBOUND or RETURN) for round-trip tickets
- Complete audit trail per leg for compliance and reporting
- **Boarding pass delivered via email + SMS on every successful gate validation** — includes route, train, departure/arrival, QR code (email), seat assignments per passenger, and barcode
### 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:
- 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 & 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
- Points accumulation on trips
- Reward redemption
- Tier-based benefits
### Wallet System
- Top-up via payment methods
- Pay with wallet balance
- Transaction ledger
- Refund to wallet
### Live Tracking
- Real-time trip status
- Location updates
- Delay notifications
- Station crowd signals
### Fraud Detection
- Velocity checks (multiple bookings)
- High-value transaction monitoring
- Failed payment pattern detection
- Automatic user blocking
### Passenger Profiles
- Comprehensive profile data: gender, date of birth, nationality
- National ID for Ethiopian citizens (Fayda verified)
- Passport information for international passengers
- NEW: Complete demographic data for personalized services
### Internationalization
- Multi-language support (English, Amharic, French, Oromo)
- Locale-based responses
- Currency formatting (ETB, DJF, USD)
- NEW: Multi-currency display per booking (ETB, DJF, USD)
### Transit Stop Management
- Automatic detection of cross-border journeys (Ethiopia to Djibouti)
- Dire Dawa as mandatory transit hub for international journeys
- Dual-leg fare calculation (domestic + international)
- Age-based pricing applied independently per leg
- Seamless multi-segment booking workflow
### Coach Type & Class Selection
- Browse available coach types per route
- View seat classes per coach (Economy Regular, Economy Bed, VIP Bed)
- Compare base prices by coach type and class
- Real-time availability per coach configuration
- NEW: Sequence-based coach ordering for consistent UI
- NEW: Premium and insurance fee transparency per class
### Data Organization
- Stations ordered by sequence (1-15) for consistent route display
- Coaches ordered by sequence (1+) per type for predictable configuration
- Booking history sorted chronologically with filtering options
## Authentication
### Passenger Authentication (JWT-auth)
Used for passenger-facing endpoints. Obtain token via \`POST /auth/login\`.
**Usage:** Add header \`Authorization: Bearer <token>\`
### Back-office Authentication (IAM-auth)
Used for agent, fraud, and reporting endpoints. Requires corporate IAM token.
**Usage:** Add header \`Authorization: Bearer <iam-token>\`
## Passenger Booking Flow
### Step 1: Search Trips
\`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.
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)
**For International Passengers:**
\`POST /passengers/register-international\` — Passport information collection
### Step 4: View Seat Map
\`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: 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
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)
### Step 8: Get Tickets
\`GET /payments/{paymentId}/status\` to confirm payment and retrieve tickets with QR codes
## Rate Limiting
| Tier | Limit | Applied to |
|---|---|---|
| auth | 5 req/min | \`/auth\` (all), \`/fayda/verification\` (all) |
| strict | 20 req/min | \`/bookings\`, \`/passengers\`, \`/payments\`, \`/wallet\` |
| default | 100 req/min | All other endpoints |
Exempt from rate limiting: \`/health/*\`, \`/internal/payments/*\`, payment webhook handlers.
## Error Handling
All errors follow standard format:
\`\`\`json
{
"statusCode": 400,
"message": "Validation failed",
"error": "Bad Request",
"timestamp": "2026-05-20T14:30:00.000Z",
"path": "/bookings"
}
\`\`\`
## Pagination
List endpoints support pagination:
- \`limit\`: Number of items (default: 20, max: 100)
- \`offset\`: Skip items (default: 0)
## Webhooks
Payment providers send notifications to:
- \`POST /payments/webhooks/telebirr\` (Ethiopia)
- \`POST /payments/webhooks/cbe-birr\` (Ethiopia)
- \`POST /payments/webhooks/waafi\` (Djibouti)
- \`POST /payments/webhooks/card\` (International)
## Support
- **Email:** support@edr-platform.com
- **Documentation:** https://docs.edr-platform.com
- **Status Page:** https://status.edr-platform.com
`,
)
.setVersion("1.0.0")
.addBearerAuth(
{ type: "http", scheme: "bearer", bearerFormat: "JWT", in: "header" },
"JWT-auth",
)
.addTag("Agents", "Counter booking, shift management, commission tracking, and reconciliation")
.addTag("Excess Luggage", "IAM-protected agent/supervisor endpoints to log excess baggage charges, waive fees, resend payment links, and manage allowance rules per seat class. Public token-based endpoints let passengers self-pay outstanding charges.")
.addTag("Packages", "Bundled travel packages with tiered pricing. Public endpoints for browsing and booking; JWT-authenticated endpoints for purchase history; IAM-protected endpoints for admin CRUD and tier management.")
.addTag("Config", "System-wide configuration management including feature flags, maintenance modes, and operational parameters. IAM-protected endpoints for administrative control.")
.addTag("Audit", "User activity logging, system changes, compliance tracking, and audit trails")
.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("Currencies", "Multi-currency support, exchange rates, and currency conversion")
.addTag("Dashboard", "Home screen aggregations: trips, loyalty, wallet, notifications")
.addTag("Fare Engine", "Distance-based fare calculation with age-based pricing and multi-currency")
.addTag("Fayda Verification", "Ethiopian national ID verification via Verifayda 2.0 government API")
.addTag("Fleet", "Train services, coaches, coach types, seat classes, amenities, and configurations")
.addTag("Fraud Detection", "Velocity checks, monitoring alerts, pattern detection, and user blocking")
.addTag("Health", "Liveness (GET /health), readiness with DB check (GET /health/ready), and app info (GET /health/info). All probes are public and exempt from rate limiting.")
.addTag("Internal Payments", "Service-to-service payment event handler (mark-paid). Requires service auth token. Exempt from rate limiting.")
.addTag("Live Tracking", "Real-time trip status, location updates, delays, and crowd signals")
.addTag("Loyalty", "Points ledger, tier management (Bronze/Silver/Gold/Platinum), rewards")
.addTag("Notifications", "Multi-channel delivery (email, SMS, push) and preference management. Boarding pass email+SMS sent automatically on gate validation.")
.addTag("Configurable Fares", "Advanced fare management system with flexible configurations, rate rules, components, age-based pricing, and migration tools. Supports nationality-based rates, bed position pricing, and dynamic component calculations.")
.addTag("Passenger Auth", "JWT-authenticated passenger login, profile, and session management")
.addTag("Passengers", "Registration, Fayda verification, international passports, saved profiles. Rate limited: 20 req/min.")
.addTag("Payment", "Telebirr, CBE Birr, Waafi, Card, Wallet payment processing and refunds. Rate limited: 20 req/min.")
.addTag("Promotions", "Promo codes, campaigns, discounts, and redemption tracking")
.addTag("Reports", "Revenue analytics, occupancy reports, agent sales, and KPI dashboards")
.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")
.addTag("Seat Classes", "Economy Regular, Economy Bed, VIP Bed class configuration and pricing")
.addTag("Seats", "Seat maps, holds (15-min expiry), releases, blocking, and inventory")
.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, gate validation with per-leg tracking (OUTBOUND/RETURN/LEG1/LEG2/OUTBOUND_LEG1/OUTBOUND_LEG2/RETURN_LEG1/RETURN_LEG2), audit trails. Boarding pass email+SMS sent automatically on every successful validation.")
.addTag("Wallet", "Balance management, top-ups, withdrawals, and transaction ledger. Rate limited: 20 req/min.")
//.addServer('http://localhost:4000', 'Development')
// .addServer("https://api.edr-platform.com", "Production")
.build();
const document = SwaggerModule.createDocument(app, config);
// Collapse all IAM / platform-infrastructure tags into one Swagger tag so every
// endpoint from @tria-plc/iamapi-common and @tria-plc/api-common appears under
// a single "Corporate IAM & Platform Infrastructure" section.
const IAM_UNIFIED_TAG = 'Corporate IAM & Platform Infrastructure';
const IAM_SOURCE_TAGS = new Set([
'Auth', 'Sessions', 'API_COMMON_File Settings',
'IAM_USER__Users', 'IAM_USER__User Document', 'IAM_USER__User Roles',
'IAM_USER__Roles', 'IAM_USER__Role Permissions', 'IAM_USER__Permissions',
'IAM_USER__Applications', 'IAM_USER__Account Configurations', 'IAM_USER__Documentary Requirements',
'IAM_ORGANISATION_STRUCTURE__Organizations', 'IAM_ORGANISATION_STRUCTURE__Organization Types',
'IAM_ORGANISATION_STRUCTURE__Organization Configurations',
'IAM_ORGANISATION_STRUCTURE__Global Organization Configurations',
'IAM_ORGANISATION_STRUCTURE__Organization Settings',
'IAM_ORGANISATION_STRUCTURE__Units', 'IAM_ORGANISATION_STRUCTURE__Unit Settings',
'IAM_ORGANISATION_STRUCTURE__Global Unit Configurations', 'IAM_ORGANISATION_STRUCTURE__Unit Clusters',
'IAM_ORGANISATION_STRUCTURE__Positions', 'IAM_ORGANISATION_STRUCTURE__Position Types',
'IAM_ORGANISATION_STRUCTURE__Position Configurations',
'IAM_ORGANISATION_STRUCTURE__Position Type Configurations',
'IAM_ORGANISATION_STRUCTURE__Position Permissions', 'IAM_ORGANISATION_STRUCTURE__Position Type Permissions',
'IAM_ORGANISATION_STRUCTURE__Employees', 'IAM_ORGANISATION_STRUCTURE__Employee Positions',
'IAM_ORGANISATION_STRUCTURE__Locations', 'IAM_ORGANISATION_STRUCTURE__Location Types',
'IAM_ORGANISATION_STRUCTURE__Default Units', 'IAM_ORGANISATION_STRUCTURE__Default Positions',
'IAM_ORGANISATION_STRUCTURE__Projects', 'IAM_ORGANISATION_STRUCTURE__Migrate',
'IAM_RECORD__Headers', 'IAM_RECORD__Footers', 'IAM_RECORD__Seals',
'IAM_RECORD__Employee Signatures', 'IAM_RECORD__Employee Stamps',
]);
// Re-tag every operation whose tags overlap with IAM_SOURCE_TAGS
for (const pathItem of Object.values(document.paths)) {
for (const operation of Object.values(pathItem as Record<string, any>)) {
if (Array.isArray(operation?.tags)) {
const hasIam = operation.tags.some((t: string) => IAM_SOURCE_TAGS.has(t));
if (hasIam) operation.tags = [IAM_UNIFIED_TAG];
}
}
}
// Replace the individual source tag definitions with the single unified tag
document.tags = [
...(document.tags ?? []).filter((t: any) => !IAM_SOURCE_TAGS.has(t.name)),
{ name: IAM_UNIFIED_TAG, description: 'Back-office staff authentication, session management, organisation structure, user/role/permission management, and file settings. Provided by @tria-plc/iamapi-common and @tria-plc/api-common.' },
];
SwaggerModule.setup("api-docs", app, document, {
customSiteTitle: "EDR Passenger API",
swaggerOptions: {
persistAuthorization: true,
docExpansion: "none",
filter: true,
tagsSorter: "alpha",
operationsSorter: "alpha",
},
});
} // end if (NODE_ENV !== 'production')
const port = process.env.PORT ?? 4000;
await app.listen(port);
const logger = new Logger('Bootstrap');
logger.log(`EDR Passenger API running on port ${port}`);
logger.log(`Swagger: http://localhost:${port}/api-docs`);
}
bootstrap();