import { Body, Controller, Get, Param, Post, UseGuards, Query, Request, UnauthorizedException, Patch, Delete } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse, ApiQuery } from '@nestjs/swagger'; import { PassengersService } from './passengers.service'; import { CreateTravelerProfileDto, CreateSavedRouteDto, VerifyFaydaDto, SavePassengersDto, RegisterPassengerDto } from './passengers.dto'; import { JwtGuard } from '../../common/jwt.guard'; import { IamGuard } from '../../common/iam-adapter'; import { VerifaydaService } from '../verifayda/verifayda.service'; import { OptionalJwtGuard } from '../verifayda/optional-jwt.guard'; import { PrismaService } from '../../common/prisma.service'; @ApiTags('Passengers') @Controller('passengers') export class PassengersController { constructor( private service: PassengersService, private verifaydaService: VerifaydaService, private prisma: PrismaService, ) {} @Get() @ApiOperation({ summary: 'List all passengers with filters (Admin/Agent)', description: 'Returns paginated list of passengers with search filters' }) @ApiQuery({ name: 'search', required: false, description: 'Search by name, email, or phone' }) @ApiQuery({ name: 'verified', required: false, description: 'Filter by verification status' }) @ApiQuery({ name: 'page', required: false, description: 'Page number' }) @ApiQuery({ name: 'pageSize', required: false, description: 'Items per page' }) findAll( @Query('search') search?: string, @Query('verified') verified?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string, ) { return this.service.findAll({ search, verified: verified ? verified === 'true' : undefined, page: page ? parseInt(page) : 1, pageSize: pageSize ? parseInt(pageSize) : 20 }); } @Get('me') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Get current passenger profile', description: 'Returns complete profile for authenticated passenger including passport details and verification status. Returns null if no passenger profile exists.' }) @ApiResponse({ status: 200, description: 'Passenger profile retrieved successfully or null if not found' }) @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing token' }) async getMe(@Request() req: any) { if (!req.user || !req.user.userId) { throw new UnauthorizedException('User not authenticated'); } try { const user = await this.prisma.user.findUnique({ where: { id: req.user.userId }, include: { passenger: true, }, }); if (!user || !user.passenger) { return null; } return this.service.getProfile(user.passenger.id); } catch (error) { // If profile lookup fails for any reason, return null to allow app to continue return null; } } @Get(':id/profile') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Get passenger profile' }) getProfile(@Param('id') id: string) { return this.service.getProfile(id); } @Get(':id/stats') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Get passenger stats' }) getStats(@Param('id') id: string) { return this.service.getStats(id); } @Post('verify-fayda') @ApiOperation({ summary: 'Verify Ethiopian national ID via Verifayda 2.0', description: `**Standalone endpoint for pre-verification of Ethiopian national IDs** --- ### Purpose Pre-verify national ID to auto-fill passenger registration form before submission. --- ### Flow 1. User enters national ID in form 2. Frontend calls \`POST /passengers/verify-fayda\` 3. API queries Verifayda 2.0 government database 4. Returns verified passenger data (name, DOB, gender) 5. Frontend auto-fills form with verified data 6. User submits form via \`POST /passengers/register\` --- ### Features - Real-time verification via Verifayda 2.0 API - Retrieves verified data: name, date of birth, gender, nationality - **National IDs NOT stored** (policy compliant) - Only for Ethiopian nationals with national ID - Non-Ethiopians use passport (no verification) --- ### Important Notes - This is a **read-only** verification endpoint - Does NOT save passenger data to database - Use \`POST /passengers/register\` to actually register - Falls back to manual entry if Verifayda disabled or fails --- ### Authentication - **Public endpoint** (no authentication required) - Can be called before login/registration`, }) @ApiResponse({ status: 200, description: 'Verification successful with passenger data', schema: { example: { verified: true, passengerData: { fullName: 'Abebe Kebede', dateOfBirth: '1985-03-15T00:00:00.000Z', gender: 'Male', nationality: 'Ethiopian' } } } }) @ApiResponse({ status: 400, description: 'Verification failed or Verifayda disabled' }) verifyFayda(@Body() dto: VerifyFaydaDto) { return this.verifaydaService.verifyNationalId(dto.nationalId); } @Post('register') @UseGuards(OptionalJwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Universal passenger registration endpoint', description: `**Single endpoint for all passenger registration scenarios** --- ### Automatic Detection The API automatically detects: - **Passenger Type**: Ethiopian (nationalId) vs International (passportNumber) - **Authentication**: Logged-in (JWT token) vs Guest (deviceId) - **Verification**: Auto-attempts Fayda for Ethiopian nationals --- ### Scenarios Handled #### 1. Guest Ethiopian Passenger - Provide: \`nationalId\`, \`deviceId\` - Behavior: Attempts Fayda verification → Saves to SavedPassengerProfile - Response: \`verified: true/false\`, \`linked: false\` #### 2. Guest International Passenger - Provide: \`passportNumber\`, \`passportCountry\`, \`deviceId\` - Behavior: No verification → Saves to SavedPassengerProfile - Response: \`verified: false\`, \`linked: false\` #### 3. Logged-in Ethiopian Passenger - Provide: JWT token + \`nationalId\` - Behavior: Attempts Fayda verification → Updates user profile - Response: \`verified: true/false\`, \`linked: true\` #### 4. Logged-in International Passenger - Provide: JWT token + \`passportNumber\`, \`passportCountry\` - Behavior: No verification → Updates user profile - Response: \`verified: false\`, \`linked: true\` --- ### Authentication - **Optional JWT Bearer Token** (OptionalJwtGuard) - Token present → Links to user account - No token → Saves as guest (requires deviceId) --- ### Benefits - Single endpoint for all scenarios - Auto-detects passenger type and flow - Graceful fallback if Fayda fails - Consistent response structure --- ### Replaces - Manual verification + save flows`, }) @ApiResponse({ status: 201, description: 'Passenger registered successfully', schema: { example: { id: 'uuid-123', passengerName: 'Abebe Kebede', dateOfBirth: '1985-03-15T00:00:00.000Z', nationality: 'Ethiopian', verified: true, linked: false, message: 'Passenger details saved for guest booking' } } }) @ApiResponse({ status: 400, description: 'Validation error or verification failed', schema: { example: { statusCode: 400, message: 'Validation failed', error: 'Bad Request' } } }) @ApiResponse({ status: 401, description: 'Invalid JWT token (only if token provided but invalid)' }) registerPassenger(@Body() dto: RegisterPassengerDto, @Request() req: any) { const userId = req.user?.userId; return this.service.registerPassenger({ ...dto, userId }); } @Post('save-details') @ApiOperation({ summary: 'Bulk save passenger details from booking flow', description: `**Endpoint for saving multiple passengers in a single booking** --- ### Purpose Save all passenger details for a multi-passenger booking before proceeding to seat selection. Optimized for batch operations where all passengers are collected upfront. --- ### Use Cases 1. **Multi-passenger bookings** - Save all passengers in a single request 2. **Batch registration** - Admin/Agent registering multiple passengers at once 3. **Data preservation** - Save passenger data before proceeding to seat selection 4. **Guest bookings** - Multiple guests booking together --- ### Differences from /register | Feature | /register | /save-details | |---------|-----------|---------------| | Purpose | Single passenger registration with optional verification | Bulk save multiple passengers | | Passengers | One at a time | Multiple in array | | Verification | Auto-attempts for Ethiopian nationals (if enabled) | No automatic verification | | Use case | Individual registration flow | Booking flow with all passengers | | Authentication | Optional JWT | Optional JWT | --- ### Response Returns saved passenger details with generated IDs and confirmation.`, }) @ApiResponse({ status: 201, description: 'All passenger details saved successfully', schema: { example: { count: 2, passengerIds: ['uuid-1', 'uuid-2'], passengers: [ { id: 'uuid-1', passengerName: 'Abebe Kebede', dateOfBirth: '1985-03-15T00:00:00.000Z', nationality: 'Ethiopian', nationalId: 'ET123456789' }, { id: 'uuid-2', passengerName: 'Sara Ketsela', dateOfBirth: '1990-08-22T00:00:00.000Z', nationality: 'Ethiopian', nationalId: 'ET987654321' } ], message: 'Passenger details saved successfully' } } }) @ApiResponse({ status: 400, description: 'Validation error - passengers array required' }) savePassengers(@Body() dto: SavePassengersDto) { return this.service.savePassengers(dto.passengers, dto.userId, dto.deviceId); } @Post('traveler-profiles') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Add traveler profile (family member)' }) createTravelerProfile(@Body() dto: CreateTravelerProfileDto) { return this.service.createTravelerProfile(dto); } @Get(':id/traveler-profiles') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Get traveler profiles for passenger' }) getTravelerProfiles(@Param('id') id: string) { return this.service.getTravelerProfiles(id); } @Post('saved-routes') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Save a route' }) createSavedRoute(@Body() dto: CreateSavedRouteDto) { return this.service.createSavedRoute(dto); } @Get(':id/saved-routes') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Get saved routes' }) getSavedRoutes(@Param('id') id: string) { return this.service.getSavedRoutes(id); } @Patch(':id') @ApiOperation({ summary: 'Update passenger details', description: 'Updates passenger information for admin/agent operations' }) @ApiResponse({ status: 200, description: 'Passenger updated successfully' }) @ApiResponse({ status: 404, description: 'Passenger not found' }) updatePassenger(@Param('id') id: string, @Body() dto: any) { return this.service.updatePassenger(id, dto); } @Delete(':id') @ApiOperation({ summary: 'Delete passenger (admin only)', description: 'Permanently deletes a passenger record and associated data' }) @ApiResponse({ status: 200, description: 'Passenger deleted successfully' }) @ApiResponse({ status: 404, description: 'Passenger not found' }) deletePassenger(@Param('id') id: string) { return this.service.deletePassenger(id); } @Get(':id/usage') @ApiOperation({ summary: 'Check if passenger is in use', description: 'Returns list of modules/data that reference this passenger' }) @ApiResponse({ status: 200, description: 'Usage information retrieved' }) @ApiResponse({ status: 404, description: 'Passenger not found' }) checkUsage(@Param('id') id: string) { return this.service.checkPassengerUsage(id); } }