import { Body, Controller, Get, Param, Post, UseGuards, Query, Request, UnauthorizedException, Patch, Delete, SetMetadata } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse, ApiQuery } from '@nestjs/swagger'; import { SkipThrottle, Throttle } from '@nestjs/throttler'; import { PassengersService } from './passengers.service'; import { CreateTravelerProfileDto, CreateSavedRouteDto, VerifyFaydaDto, SavePassengersDto, RegisterPassengerDto } from './passengers.dto'; import { JwtGuard } from '../../common/jwt.guard'; import { PassengerAdmin } from '../../common/passenger-guards'; import { VerifaydaService } from '../verifayda/verifayda.service'; import { OptionalJwtGuard } from '../verifayda/optional-jwt.guard'; import { PrismaService } from '../../common/prisma.service'; @ApiTags('Passengers') @Controller('passengers') @Throttle({ strict: { limit: 20, ttl: 60_000 } }) export class PassengersController { constructor( private service: PassengersService, private verifaydaService: VerifaydaService, private prisma: PrismaService, ) {} @Get() @SetMetadata('isPublic', true) @ApiOperation({ summary: 'List all travelers with filters (Admin/Agent)', description: `**Returns paginated list of all travelers in the system** --- ### Data Source - Fetches from **TravelerProfile** table (created during booking) - Shows ALL passengers from ALL bookings (including guest bookings) - Each row represents a unique traveler, not a user account --- ### Features - Search by name, email, phone - Filter by gender - Date range filtering (createdAt) - Pagination support (page, pageSize) - Includes loyalty and wallet info if linked to user account - Shows booking count per traveler --- ### Response Fields - **id**: TravelerProfile ID - **fullName**: Passenger name - **email/phone**: Contact info (from linked user or booking) - **gender**: Male/Female/Other (from Verifayda or manual entry) - **dateOfBirth**: Birth date in YYYY-MM-DD format - **nationality**: Passenger nationality - **faydaVerified**: Whether verified via Verifayda - **loyaltyTier/loyaltyPoints**: If linked to user account - **totalBookings**: Number of bookings - **createdAt**: When traveler was first added to system` }) @ApiQuery({ name: 'search', required: false, description: 'Search by name, email, or phone' }) @ApiQuery({ name: 'gender', required: false, description: 'Filter by gender (Male, Female, Other)' }) @ApiQuery({ name: 'dateFrom', required: false, description: 'Filter by creation date from (YYYY-MM-DD)' }) @ApiQuery({ name: 'dateTo', required: false, description: 'Filter by creation date to (YYYY-MM-DD)' }) @ApiQuery({ name: 'page', required: false, description: 'Page number (default: 1)' }) @ApiQuery({ name: 'pageSize', required: false, description: 'Items per page (default: 20)' }) @ApiResponse({ status: 200, description: 'Travelers retrieved successfully', schema: { example: { items: [ { id: 'uuid-123', fullName: 'Abebe Kebede', email: 'abebe@example.com', phone: '+251911234567', gender: 'Male', dateOfBirth: '1985-03-15', nationality: 'Ethiopian', faydaVerified: true, loyaltyTier: 'SILVER', loyaltyPoints: 1500, totalBookings: 5, createdAt: '2024-01-10T12:00:00.000Z' } ], meta: { page: 1, pageSize: 20, total: 150, totalPages: 8 } } } }) @ApiResponse({ status: 200, description: 'Travelers retrieved successfully', schema: { example: { items: [ { id: 'uuid-123', fullName: 'Abebe Kebede', email: 'abebe@example.com', phone: '+251911234567', gender: 'Male', dateOfBirth: '1985-03-15', nationality: 'Ethiopian', nationalityCode: 'ET', faydaVerified: true, faydaVerifiedAt: '2024-01-15T10:30:00.000Z', passportNumber: null, passportCountry: null, passportExpiryDate: null, idDocumentType: 'NATIONAL_ID', verified: true, lastLoginAt: '2024-01-20T08:15:00.000Z', role: 'PASSENGER', loyalty: { tier: 'SILVER', pointsBalance: 1500, lifetimePoints: 3000 }, wallet: { balanceMinor: 50000, currency: 'ETB' }, loyaltyTier: 'SILVER', loyaltyPoints: 1500, totalBookings: 5, createdAt: '2024-01-10T12:00:00.000Z' }, { id: 'uuid-456', fullName: 'Sara Ketsela', email: null, phone: null, gender: 'Female', dateOfBirth: '1990-08-22', nationality: 'Ethiopian', nationalityCode: null, faydaVerified: false, faydaVerifiedAt: null, passportNumber: null, passportCountry: null, passportExpiryDate: null, idDocumentType: null, verified: false, lastLoginAt: null, role: null, loyalty: null, wallet: null, loyaltyTier: 'BRONZE', loyaltyPoints: 0, totalBookings: 1, createdAt: '2024-01-18T14:30:00.000Z' } ], meta: { page: 1, pageSize: 20, total: 150, totalPages: 8 } } } }) findAll( @Query('search') search?: string, @Query('gender') gender?: string, @Query('dateFrom') dateFrom?: string, @Query('dateTo') dateTo?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string, ) { return this.service.findAll({ search, gender, dateFrom, dateTo, 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.id) { throw new UnauthorizedException('User not authenticated'); } try { const passenger = await this.prisma.passenger.findUnique({ where: { iamUserId: req.user.id }, }); if (!passenger) return null; return this.service.getProfile(passenger.id); } catch (error) { 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') @SetMetadata('isPublic', true) @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') @SetMetadata('isPublic', true) @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?.id; return this.service.registerPassenger({ ...dto, userId }); } @Post('save-details') @SetMetadata('isPublic', true) @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') @SetMetadata('isPublic', true) @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') @PassengerAdmin() @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Delete passenger (admin only)', description: 'Permanently deletes a passenger record and associated data' }) @ApiQuery({ name: 'cascade', required: false, type: Boolean, description: 'Force delete with all related bookings and data' }) @ApiResponse({ status: 200, description: 'Passenger deleted successfully' }) @ApiResponse({ status: 404, description: 'Passenger not found' }) deletePassenger(@Param('id') id: string, @Query('cascade') cascade?: string) { return this.service.deletePassenger(id, cascade === 'true'); } @Get(':id/usage') @SetMetadata('isPublic', true) @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); } }