import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; import { PrismaService } from '../../common/prisma.service'; import { CreateTravelerProfileDto, CreateSavedRouteDto, RegisterPassengerDto } from './passengers.dto'; import { VerifaydaService } from '../verifayda/verifayda.service'; interface PassengerFilters { search?: string; verified?: boolean; page?: number; pageSize?: number; } type IamUserRow = { id: string; email: string; name: { en: string; am: string } | null; phone_number: string | null; metadata: Record | null; }; @Injectable() export class PassengersService { constructor( private readonly prisma: PrismaService, @InjectDataSource() private readonly dataSource: DataSource, private readonly verifaydaService: VerifaydaService, ) {} async findAll(filters: PassengerFilters = {}) { const { search, verified, page = 1, pageSize = 20 } = filters; const skip = (page - 1) * pageSize; const where: any = {}; if (search) { where.user = { OR: [ { email: { contains: search, mode: 'insensitive' } }, { phone: { contains: search, mode: 'insensitive' } }, { fullName: { contains: search, mode: 'insensitive' } }, ], }; } if (verified !== undefined) { where.user = { ...(where.user ?? {}), faydaVerified: verified }; } const [items, total] = await Promise.all([ this.prisma.passenger.findMany({ where, skip, 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 }), ]); const iamUserIds = items.map(p => p.iamUserId).filter(Boolean) as string[]; const iamRows = iamUserIds.length > 0 ? await this.dataSource.query( `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE id = ANY($1)`, [iamUserIds], ) : []; 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 = 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: 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, createdAt: passenger.createdAt, }; }), meta: { page, pageSize, total, totalPages: Math.ceil(total / pageSize), }, }; } async getProfile(passengerId: string) { const passenger = await this.prisma.passenger.findUnique({ where: { id: passengerId }, include: { bookings: { orderBy: { createdAt: 'desc' }, take: 10, include: { schedule: { include: { originStation: true, destinationStation: true, train: true } }, seats: { include: { seat: { include: { coach: true } } } }, }, }, loyalty: true, wallet: true, travelerProfiles: true, savedRoutes: true, }, }); if (!passenger) throw new NotFoundException('Passenger not found'); let iamUser: IamUserRow | null = null; if (passenger.iamUserId) { const rows = await this.dataSource.query( `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE id = $1 LIMIT 1`, [passenger.iamUserId], ); iamUser = rows[0] ?? null; } return { id: passenger.id, fullName: iamUser?.name?.en ?? iamUser?.name?.am ?? null, email: iamUser?.email ?? null, phone: iamUser?.phone_number ?? null, createdAt: passenger.createdAt, bookings: passenger.bookings.map((b) => ({ id: b.id, bookingRef: b.bookingRef, status: b.status, totalFare: b.totalMinor / 100, createdAt: b.createdAt, trip: { number: b.schedule.train.number, origin: { id: b.schedule.originStation.id, name: b.schedule.originStation.name, code: b.schedule.originStation.code, city: b.schedule.originStation.city }, destination: { id: b.schedule.destinationStation.id, name: b.schedule.destinationStation.name, code: b.schedule.destinationStation.code, city: b.schedule.destinationStation.city }, departureAt: b.schedule.departureAt, }, passengers: b.seats.map((bs) => ({ fullName: bs.passengerName, seat: { number: bs.seat.seatNumber, coach: bs.seat.coach.number, class: 'N/A' }, })), })), }; } async getStats(passengerId: string) { const [totalTrips, totalSpendResult, loyalty] = await Promise.all([ this.prisma.booking.count({ where: { passengerId, status: 'BOARDED' as any } }), this.prisma.booking.aggregate({ where: { passengerId, status: 'BOARDED' as any }, _sum: { totalMinor: true } }), this.prisma.loyaltyAccount.findUnique({ where: { passengerId } }), ]); const totalSpend = ((totalSpendResult._sum?.totalMinor ?? 0) as number) / 100; return { totalTrips, totalSpend, loyaltyPoints: loyalty?.pointsBalance ?? 0, co2Saved: totalTrips * 6 }; } async savePassengers(passengers: any[], userId?: string, deviceId?: string) { if (!passengers || !Array.isArray(passengers)) { throw new BadRequestException('Passengers array is required'); } if (passengers.length === 0) { throw new BadRequestException('At least one passenger is required'); } const savedProfiles = await Promise.all( passengers.map((p) => this.prisma.savedPassengerProfile.create({ data: { userId, deviceId, passengerName: p.name || p.passengerName, dateOfBirth: new Date(p.dateOfBirth), idDocumentType: p.nationalId ? 'NATIONAL_ID' : 'PASSPORT', passportNumber: p.passportNumber, passportCountry: p.passportCountry, nationality: p.nationality, phone: p.phone, email: p.email, }, }) ) ); return { count: savedProfiles.length, passengerIds: savedProfiles.map(p => p.id), passengers: savedProfiles.map(p => ({ id: p.id, passengerName: p.passengerName, dateOfBirth: p.dateOfBirth, nationality: p.nationality, })), message: 'Passenger details saved successfully', }; } createTravelerProfile(dto: CreateTravelerProfileDto) { return this.prisma.travelerProfile.create({ data: { ...dto, dateOfBirth: dto.dateOfBirth ? new Date(dto.dateOfBirth) : null } }); } getTravelerProfiles(passengerId: string) { return this.prisma.travelerProfile.findMany({ where: { passengerId } }); } createSavedRoute(dto: CreateSavedRouteDto) { return this.prisma.savedRoute.create({ data: dto }); } getSavedRoutes(passengerId: string) { return this.prisma.savedRoute.findMany({ where: { passengerId }, orderBy: { tripCount: 'desc' } }); } async updatePassenger(id: string, dto: any) { const passenger = await this.prisma.passenger.findUnique({ where: { id } }); if (!passenger) throw new NotFoundException('Passenger not found'); if (passenger.iamUserId && (dto.fullName || dto.email || dto.phone)) { const updates: string[] = []; const params: any[] = []; let idx = 1; if (dto.fullName) { updates.push(`name = COALESCE(name, '{}') || jsonb_build_object('en', $${idx}::text, 'am', $${idx}::text)`); params.push(dto.fullName); idx++; } if (dto.email) { updates.push(`email = $${idx}`); params.push(dto.email); idx++; } if (dto.phone) { updates.push(`phone_number = $${idx}`); params.push(dto.phone); idx++; } params.push(passenger.iamUserId); await this.dataSource.query( `UPDATE iam.users SET ${updates.join(', ')} WHERE id = $${idx}`, params, ); } const [updated, iamRows] = await Promise.all([ this.prisma.passenger.findUnique({ where: { id }, include: { loyalty: true } }), passenger.iamUserId ? this.dataSource.query( `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE id = $1 LIMIT 1`, [passenger.iamUserId], ) : Promise.resolve([] as IamUserRow[]), ]); const iamUser = iamRows[0] ?? null; return { id: updated!.id, fullName: iamUser?.name?.en ?? iamUser?.name?.am ?? null, email: iamUser?.email ?? null, phone: iamUser?.phone_number ?? null, loyalty: updated!.loyalty, }; } async registerPassenger(dto: RegisterPassengerDto) { const isEthiopian = !!dto.nationalId; const isLoggedIn = !!dto.userId; let verifiedData: any = null; if (isEthiopian && dto.verifyWithFayda !== false) { try { const verification = await this.verifaydaService.verifyNationalId(dto.nationalId!); if (verification.verified && verification.passengerData) { verifiedData = verification.passengerData; } } catch (error) { console.warn('Fayda verification failed, using manual data:', error); } } const finalData = { passengerName: verifiedData?.fullName || dto.passengerName, dateOfBirth: verifiedData?.dateOfBirth || new Date(dto.dateOfBirth), nationality: verifiedData?.nationality || dto.nationality || (isEthiopian ? 'Ethiopian' : null), gender: verifiedData?.gender || dto.gender, phone: dto.phone, email: dto.email, }; if (isLoggedIn) { const linkedPassenger = await this.prisma.passenger.findUnique({ where: { iamUserId: dto.userId }, }); if (!linkedPassenger) { throw new BadRequestException('Passenger not found'); } return { id: linkedPassenger.id, passengerName: finalData.passengerName, dateOfBirth: finalData.dateOfBirth, nationality: finalData.nationality, verified: !!verifiedData, linked: true, message: 'Passenger details saved and linked to user account', }; } const profile = await this.prisma.savedPassengerProfile.create({ data: { deviceId: dto.deviceId, passengerName: finalData.passengerName, dateOfBirth: finalData.dateOfBirth, idDocumentType: isEthiopian ? 'NATIONAL_ID' : 'PASSPORT', passportNumber: dto.passportNumber, passportCountry: dto.passportCountry, nationality: finalData.nationality, phone: dto.phone, email: dto.email, }, }); return { id: profile.id, passengerName: finalData.passengerName, dateOfBirth: finalData.dateOfBirth, nationality: finalData.nationality, verified: !!verifiedData, linked: false, message: 'Passenger details saved for guest booking', }; } async deletePassenger(id: string) { const passenger = await this.prisma.passenger.findUnique({ where: { id } }); if (!passenger) throw new NotFoundException('Passenger not found'); 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.packageBooking.deleteMany({ where: { passengerId: id } }), this.prisma.ticket.deleteMany({ where: { booking: { passengerId: id } } }), this.prisma.bookingSeat.deleteMany({ where: { booking: { passengerId: id } } }), this.prisma.booking.deleteMany({ where: { passengerId: id } }), this.prisma.journeySegment.deleteMany({ where: { journey: { passengerId: id } } }), this.prisma.journey.deleteMany({ where: { passengerId: id } }), this.prisma.passenger.delete({ where: { id } }), ]); return { deleted: true, passengerId: id }; } async checkPassengerUsage(id: string) { const [bookingCount, loyaltyAccount, walletAccount] = await Promise.all([ this.prisma.booking.count({ where: { passengerId: id } }), this.prisma.loyaltyAccount.findUnique({ where: { passengerId: id } }), this.prisma.walletAccount.findUnique({ where: { passengerId: id } }), ]); const usage = []; if (bookingCount > 0) usage.push(`${bookingCount} booking(s)`); if (loyaltyAccount) usage.push('Loyalty account'); if (walletAccount) usage.push('Wallet account'); return { isInUse: usage.length > 0, affectedModules: usage, }; } }