mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 07:22:53 +00:00
530 lines
16 KiB
TypeScript
530 lines
16 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 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')
|
|
@SetMetadata('isPublic', true)
|
|
@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);
|
|
}
|
|
}
|