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); } }