mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 04:20:55 +00:00
601 lines
17 KiB
TypeScript
601 lines
17 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 { 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);
|
|
}
|
|
}
|