Files
edr-platform/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts
2026-06-26 13:57:26 +03:00

401 lines
12 KiB
TypeScript

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 { 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 passengers with filters (Admin/Agent)',
description: 'Returns paginated list of passengers with search filters'
})
@ApiQuery({ name: 'search', required: false })
@ApiQuery({ name: 'verified', required: false })
@ApiQuery({ name: 'gender', required: false })
@ApiQuery({ name: 'nationality', required: false })
@ApiQuery({ name: 'dateFrom', required: false })
@ApiQuery({ name: 'dateTo', required: false })
@ApiQuery({ name: 'page', required: false })
@ApiQuery({ name: 'pageSize', required: false })
findAll(
@Query('search') search?: string,
@Query('verified') verified?: string,
@Query('gender') gender?: string,
@Query('nationality') nationality?: string,
@Query('dateFrom') dateFrom?: string,
@Query('dateTo') dateTo?: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
return this.service.findAll({
search,
verified: verified ? verified === 'true' : undefined,
gender,
nationality,
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')
@SetMetadata('isPublic', true)
@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')
@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);
}
}