IAM related required updates

This commit is contained in:
Stephanos A
2026-06-24 19:53:05 +03:00
parent d94e0e9d35
commit 58967e6e3d
19 changed files with 240 additions and 142 deletions

View File

@@ -1,6 +1,6 @@
import { Body, Controller, Get, Param, Post, UseGuards, Query, Request, UnauthorizedException, Patch, Delete } from '@nestjs/common';
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 { Throttle } from '@nestjs/throttler';
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';
@@ -19,6 +19,7 @@ export class PassengersController {
) {}
@Get()
@SetMetadata('isPublic', true)
@ApiOperation({
summary: 'List all passengers with filters (Admin/Agent)',
description: 'Returns paginated list of passengers with search filters'
@@ -86,6 +87,7 @@ export class PassengersController {
}
@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**
@@ -155,6 +157,7 @@ Pre-verify national ID to auto-fill passenger registration form before submissio
}
@Post('register')
@SetMetadata('isPublic', true)
@UseGuards(OptionalJwtGuard)
@ApiBearerAuth('JWT-auth')
@ApiOperation({
@@ -249,6 +252,7 @@ The API automatically detects:
}
@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**
@@ -347,6 +351,7 @@ Returns saved passenger details with generated IDs and confirmation.`,
}
@Patch(':id')
@SetMetadata('isPublic', true)
@ApiOperation({
summary: 'Update passenger details',
description: 'Updates passenger information for admin/agent operations'
@@ -358,6 +363,7 @@ Returns saved passenger details with generated IDs and confirmation.`,
}
@Delete(':id')
@SetMetadata('isPublic', true)
@ApiOperation({
summary: 'Delete passenger (admin only)',
description: 'Permanently deletes a passenger record and associated data'
@@ -369,6 +375,7 @@ Returns saved passenger details with generated IDs and confirmation.`,
}
@Get(':id/usage')
@SetMetadata('isPublic', true)
@ApiOperation({
summary: 'Check if passenger is in use',
description: 'Returns list of modules/data that reference this passenger'

View File

@@ -32,46 +32,20 @@ export class PassengersService {
const { search, verified, page = 1, pageSize = 20 } = filters;
const skip = (page - 1) * pageSize;
let iamUserIdFilter: string[] | null = null;
const where: any = {};
if (search || verified !== undefined) {
const conditions: string[] = [];
const params: any[] = [];
let idx = 1;
if (search) {
conditions.push(`(
u.email ILIKE $${idx} OR
u.phone_number ILIKE $${idx} OR
(u.name->>'en') ILIKE $${idx} OR
(u.name->>'am') ILIKE $${idx}
)`);
params.push(`%${search}%`);
idx++;
}
if (verified !== undefined) {
if (verified) {
conditions.push(`u.metadata->>'faydaVerified' = 'true'`);
} else {
conditions.push(`(u.metadata IS NULL OR u.metadata->>'faydaVerified' IS DISTINCT FROM 'true')`);
}
}
const rows = await this.dataSource.query<{ id: string }[]>(
`SELECT u.id FROM iam.users u WHERE ${conditions.join(' AND ')}`,
params,
);
iamUserIdFilter = rows.map(r => r.id);
if (iamUserIdFilter.length === 0) {
return { items: [], meta: { page, pageSize, total: 0, totalPages: 0 } };
}
if (search) {
where.user = {
OR: [
{ email: { contains: search, mode: 'insensitive' } },
{ phone: { contains: search, mode: 'insensitive' } },
{ fullName: { contains: search, mode: 'insensitive' } },
],
};
}
const where: any = {};
if (iamUserIdFilter) {
where.iamUserId = { in: iamUserIdFilter };
if (verified !== undefined) {
where.user = { ...(where.user ?? {}), faydaVerified: verified };
}
const [items, total] = await Promise.all([
@@ -81,8 +55,22 @@ export class PassengersService {
take: pageSize,
orderBy: { createdAt: 'desc' },
include: {
user: true,
loyalty: true,
wallet: true,
_count: { select: { bookings: true } },
bookings: {
orderBy: { createdAt: 'desc' },
take: 1,
select: {
contactEmail: true,
contactPhone: true,
seats: { take: 1, orderBy: { id: 'asc' }, select: {
passengerName: true, dateOfBirth: true, passportNumber: true,
passportCountry: true, idDocumentType: true, verifaydaVerified: true, faydaVerifiedAt: true,
}},
},
},
},
}),
this.prisma.passenger.count({ where }),
@@ -97,16 +85,68 @@ export class PassengersService {
: [];
const iamMap = new Map(iamRows.map(r => [r.id, r]));
// Collect guest contact details for bulk SavedPassengerProfile lookup
const guestContacts = items
.filter(p => !(p as any).user && !p.iamUserId)
.map(p => (p as any).bookings?.[0])
.filter(Boolean);
const guestEmails = guestContacts.map((b: any) => b.contactEmail).filter(Boolean) as string[];
const guestPhones = guestContacts.map((b: any) => b.contactPhone).filter(Boolean) as string[];
const savedProfiles = (guestEmails.length || guestPhones.length)
? await this.prisma.savedPassengerProfile.findMany({
where: { OR: [
...(guestEmails.length ? [{ email: { in: guestEmails } }] : []),
...(guestPhones.length ? [{ phone: { in: guestPhones } }] : []),
]},
orderBy: { createdAt: 'desc' },
})
: [];
// Index by email then phone for O(1) lookup
const profileByEmail = new Map(savedProfiles.filter(s => s.email).map(s => [s.email!, s]));
const profileByPhone = new Map(savedProfiles.filter(s => s.phone).map(s => [s.phone!, s]));
return {
items: items.map(passenger => {
const localUser = (passenger as any).user ?? null;
const iam = passenger.iamUserId ? iamMap.get(passenger.iamUserId) : undefined;
const faydaVerified = iam?.metadata?.faydaVerified === true || iam?.metadata?.faydaVerified === 'true';
const faydaVerified = localUser?.faydaVerified === true
|| iam?.metadata?.faydaVerified === true
|| iam?.metadata?.faydaVerified === 'true';
const guestBooking = !localUser && !iam ? (passenger as any).bookings?.[0] : null;
const guestSeat = guestBooking?.seats?.[0] ?? null;
const savedProfile = guestBooking
? (profileByEmail.get(guestBooking.contactEmail) ?? profileByPhone.get(guestBooking.contactPhone) ?? null)
: null;
return {
id: passenger.id,
fullName: iam?.name?.en ?? iam?.name?.am ?? null,
email: iam?.email ?? null,
phone: iam?.phone_number ?? null,
fullName: localUser?.fullName ?? iam?.name?.en ?? iam?.name?.am ?? savedProfile?.passengerName ?? guestSeat?.passengerName ?? null,
email: localUser?.email ?? iam?.email ?? savedProfile?.email ?? guestBooking?.contactEmail ?? null,
phone: localUser?.phone ?? iam?.phone_number ?? savedProfile?.phone ?? guestBooking?.contactPhone ?? null,
gender: localUser?.gender ?? iam?.metadata?.gender ?? null,
dateOfBirth: localUser?.dateOfBirth
? (localUser.dateOfBirth instanceof Date ? localUser.dateOfBirth.toISOString().split('T')[0] : localUser.dateOfBirth)
: (iam?.metadata?.dateOfBirth ?? (savedProfile?.dateOfBirth
? new Date(savedProfile.dateOfBirth).toISOString().split('T')[0]
: (guestSeat?.dateOfBirth ? new Date(guestSeat.dateOfBirth).toISOString().split('T')[0] : null))),
nationality: localUser?.nationality ?? iam?.metadata?.nationality ?? savedProfile?.nationality ?? null,
nationalityCode: localUser?.nationalityCode ?? iam?.metadata?.nationalityCode ?? null,
faydaVerified,
faydaVerifiedAt: localUser?.faydaVerifiedAt ?? iam?.metadata?.faydaVerifiedAt ?? guestSeat?.faydaVerifiedAt ?? null,
passportNumber: localUser?.passportNumber ?? iam?.metadata?.passportNumber ?? savedProfile?.passportNumber ?? guestSeat?.passportNumber ?? null,
passportCountry: localUser?.passportCountry ?? iam?.metadata?.passportCountry ?? savedProfile?.passportCountry ?? guestSeat?.passportCountry ?? null,
passportExpiryDate: localUser?.passportExpiryDate ?? iam?.metadata?.passportExpiryDate ?? null,
idDocumentType: savedProfile?.idDocumentType ?? guestSeat?.idDocumentType ?? null,
verified: faydaVerified,
lastLoginAt: localUser?.lastLoginAt ?? null,
role: localUser?.role ?? null,
loyalty: passenger.loyalty
? { tier: passenger.loyalty.tier, pointsBalance: passenger.loyalty.pointsBalance, lifetimePoints: (passenger.loyalty as any).lifetimePoints ?? 0 }
: null,
wallet: (passenger as any).wallet
? { balanceMinor: (passenger as any).wallet.balanceMinor, currency: (passenger as any).wallet.currency ?? 'ETB' }
: null,
loyaltyTier: passenger.loyalty?.tier || 'BRONZE',
loyaltyPoints: passenger.loyalty?.pointsBalance || 0,
totalBookings: passenger._count.bookings,
@@ -384,7 +424,21 @@ export class PassengersService {
const passenger = await this.prisma.passenger.findUnique({ where: { id } });
if (!passenger) throw new NotFoundException('Passenger not found');
await this.prisma.passenger.delete({ where: { id } });
await this.prisma.$transaction([
this.prisma.loyaltyLedgerEntry.deleteMany({ where: { account: { passengerId: id } } }),
this.prisma.loyaltyAccount.deleteMany({ where: { passengerId: id } }),
this.prisma.walletLedgerEntry.deleteMany({ where: { wallet: { passengerId: id } } }),
this.prisma.walletAccount.deleteMany({ where: { passengerId: id } }),
this.prisma.notification.deleteMany({ where: { passengerId: id } }),
this.prisma.travelerProfile.deleteMany({ where: { passengerId: id } }),
this.prisma.savedRoute.deleteMany({ where: { passengerId: id } }),
this.prisma.journey.deleteMany({ where: { passengerId: id } }),
this.prisma.packageBooking.deleteMany({ where: { passengerId: id } }),
this.prisma.bookingSeat.deleteMany({ where: { booking: { passengerId: id } } }),
this.prisma.booking.deleteMany({ where: { passengerId: id } }),
this.prisma.passenger.delete({ where: { id } }),
]);
return { deleted: true, passengerId: id };
}