diff --git a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts index cf0dc4dd6..b4814d903 100644 --- a/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-passenger-api/src/modules/bookings/bookings.service.ts @@ -196,11 +196,26 @@ export class BookingsService { const where: any = {}; if (search) { + const iamRows = await this.dataSource.query<{ id: string }[]>( + `SELECT u.id FROM iam.users u + WHERE (u.name->>'en') ILIKE $1 OR (u.name->>'am') ILIKE $1 + OR u.email ILIKE $1 OR u.phone_number ILIKE $1`, + [`%${search}%`], + ); + const matchedPassengers = iamRows.length > 0 + ? await this.prisma.passenger.findMany({ + where: { iamUserId: { in: iamRows.map(r => r.id) } }, + select: { id: true }, + }) + : []; + where.OR = [ { bookingRef: { contains: search, mode: 'insensitive' } }, { contactEmail: { contains: search, mode: 'insensitive' } }, { contactPhone: { contains: search, mode: 'insensitive' } }, - { passenger: { user: { fullName: { contains: search, mode: 'insensitive' } } } }, + ...(matchedPassengers.length > 0 + ? [{ passengerId: { in: matchedPassengers.map(p => p.id) } }] + : []), ]; } diff --git a/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts b/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts index ca1c7d17b..2d52879a8 100644 --- a/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts +++ b/apps/edr-passenger-api/src/modules/dashboard/dashboard.service.ts @@ -1,14 +1,19 @@ import { Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; import { PrismaService } from '../../common/prisma.service'; @Injectable() export class DashboardService { - constructor(private prisma: PrismaService) {} + constructor( + private prisma: PrismaService, + @InjectDataSource() private dataSource: DataSource, + ) {} async getHomeDashboard(passengerId: string) { const now = new Date(); const [passenger, upcomingBooking, wallet, promos, weatherAlerts, stationSignals, savedRoutes] = await Promise.all([ - this.prisma.passenger.findUnique({ where: { id: passengerId }, include: { user: { select: { fullName: true } }, loyalty: true } }), + this.prisma.passenger.findUnique({ where: { id: passengerId }, include: { loyalty: true } }), this.prisma.booking.findFirst({ where: { passengerId, status: 'CONFIRMED', schedule: { departureAt: { gte: now } } }, include: { @@ -27,7 +32,16 @@ export class DashboardService { const hour = now.getHours(); const greetingKey = hour < 12 ? 'MORNING' : hour < 17 ? 'AFTERNOON' : 'EVENING'; - const firstName = passenger?.user?.fullName?.split(' ')[0] ?? ''; + + let firstName = ''; + if (passenger?.iamUserId) { + const iamRows = await this.dataSource.query<{ name: { en?: string; am?: string } | null }[]>( + `SELECT name FROM iam.users WHERE id = $1 LIMIT 1`, + [passenger.iamUserId], + ); + const name = iamRows[0]?.name; + firstName = (name?.en ?? name?.am ?? '').split(' ')[0]; + } const seat = upcomingBooking?.seats[0]; return { diff --git a/apps/edr-passenger-api/src/modules/fraud/fraud.controller.ts b/apps/edr-passenger-api/src/modules/fraud/fraud.controller.ts index de6ef9ef5..a01524042 100644 --- a/apps/edr-passenger-api/src/modules/fraud/fraud.controller.ts +++ b/apps/edr-passenger-api/src/modules/fraud/fraud.controller.ts @@ -54,8 +54,8 @@ export class FraudController { */ @Post('actions/block') @ApiOperation({ summary: 'Block user temporarily' }) - async blockUser(@Body() body: { userId: string; durationMinutes: number }) { - await this.fraudService.blockUserTemporarily(body.userId, body.durationMinutes); + async blockUser(@Body() body: { iamUserId: string; durationMinutes: number }) { + await this.fraudService.blockUserTemporarily(body.iamUserId, body.durationMinutes); return { message: `User blocked for ${body.durationMinutes} minutes` }; } @@ -64,8 +64,8 @@ export class FraudController { */ @Post('actions/unblock') @ApiOperation({ summary: 'Unblock user' }) - async unblockUser(@Body() body: { userId: string }) { - await this.fraudService.unblockUser(body.userId); + async unblockUser(@Body() body: { iamUserId: string }) { + await this.fraudService.unblockUser(body.iamUserId); return { message: 'User unblocked' }; } } diff --git a/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts b/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts index c7541667e..a75db4449 100644 --- a/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts +++ b/apps/edr-passenger-api/src/modules/fraud/fraud.service.ts @@ -1,5 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; import { PrismaService } from '../../common/prisma.service'; export interface FraudRuleConfig { @@ -14,44 +16,37 @@ export interface FraudRuleConfig { export class FraudService { private readonly logger = new Logger(FraudService.name); - constructor(private prisma: PrismaService) {} + constructor( + private prisma: PrismaService, + @InjectDataSource() private dataSource: DataSource, + ) {} /** * Evaluate fraud rules and create alerts if triggered */ async evaluateRules( - userId: string, + passengerId: string, eventType: 'booking.created' | 'payment.failed' | 'auth.login.failed', context: Record, ): Promise<{ triggered: boolean; rules: string[] }> { const triggeredRules: string[] = []; - // Check velocity rule (multiple bookings in short time) if (eventType === 'booking.created') { - const velocityTriggered = await this.checkVelocityRule(userId); - if (velocityTriggered) { - triggeredRules.push('VELOCITY'); - } + const velocityTriggered = await this.checkVelocityRule(passengerId); + if (velocityTriggered) triggeredRules.push('VELOCITY'); - // Check high-value booking const amount = (context.amountMinor as number) || 0; const highValueTriggered = await this.checkHighValueRule(amount); - if (highValueTriggered) { - triggeredRules.push('HIGH_VALUE'); - } + if (highValueTriggered) triggeredRules.push('HIGH_VALUE'); } - // Check repeated failed payments if (eventType === 'payment.failed') { - const failedPaymentTriggered = await this.checkFailedPaymentRule(userId); - if (failedPaymentTriggered) { - triggeredRules.push('FAILED_PAYMENTS'); - } + const failedPaymentTriggered = await this.checkFailedPaymentRule(passengerId); + if (failedPaymentTriggered) triggeredRules.push('FAILED_PAYMENTS'); } - // Create alert if rules triggered if (triggeredRules.length > 0) { - await this.createFraudAlert(userId, eventType, triggeredRules, context); + await this.createFraudAlert(passengerId, eventType, triggeredRules, context); return { triggered: true, rules: triggeredRules }; } @@ -61,7 +56,7 @@ export class FraudService { /** * Check velocity rule: X bookings in Y minutes */ - private async checkVelocityRule(userId: string): Promise { + private async checkVelocityRule(passengerId: string): Promise { const rule = await this.prisma.fraudRule.findFirst({ where: { type: 'VELOCITY', enabled: true }, }); @@ -69,18 +64,14 @@ export class FraudService { if (!rule) return false; const timeWindowMinutes = (rule.config as any)?.timeWindowMinutes || 30; - const threshold = rule.threshold; - const bookingCount = await this.prisma.booking.count({ where: { - passengerId: userId, - createdAt: { - gte: new Date(Date.now() - timeWindowMinutes * 60 * 1000), - }, + passengerId, + createdAt: { gte: new Date(Date.now() - timeWindowMinutes * 60 * 1000) }, }, }); - return bookingCount > threshold; + return bookingCount > rule.threshold; } /** @@ -101,7 +92,7 @@ export class FraudService { /** * Check failed payment rule: X failed attempts in Y minutes */ - private async checkFailedPaymentRule(userId: string): Promise { + private async checkFailedPaymentRule(passengerId: string): Promise { const rule = await this.prisma.fraudRule.findFirst({ where: { type: 'FAILED_PAYMENTS', enabled: true }, }); @@ -109,33 +100,33 @@ export class FraudService { if (!rule) return false; const timeWindowMinutes = (rule.config as any)?.timeWindowMinutes || 60; - const threshold = rule.threshold; - const failedCount = await this.prisma.paymentIntent.count({ where: { - booking: { passengerId: userId }, + booking: { passengerId }, status: 'FAILED', - updatedAt: { - gte: new Date(Date.now() - timeWindowMinutes * 60 * 1000), - }, + updatedAt: { gte: new Date(Date.now() - timeWindowMinutes * 60 * 1000) }, }, }); - return failedCount > threshold; + return failedCount > rule.threshold; } /** * Create a fraud alert */ private async createFraudAlert( - userId: string, + passengerId: string, eventType: string, triggeredRules: string[], context: Record, ): Promise { + const passenger = await this.prisma.passenger.findUnique({ + where: { id: passengerId }, + select: { iamUserId: true }, + }); const alert = await this.prisma.fraudAlert.create({ data: { - iamUserId: userId, + iamUserId: passenger?.iamUserId ?? passengerId, eventType, triggeredRules, context: context as any, @@ -143,11 +134,10 @@ export class FraudService { }, }); - this.logger.warn(`Fraud alert created: ${alert.id} for user ${userId} - rules: ${triggeredRules.join(', ')}`); + this.logger.warn(`Fraud alert created: ${alert.id} for passenger ${passengerId} - rules: ${triggeredRules.join(', ')}`); - // Trigger blocking if needed if (triggeredRules.includes('HIGH_VALUE') || triggeredRules.length > 1) { - await this.blockUserTemporarily(userId, 30); // Block for 30 minutes + if (passenger?.iamUserId) await this.blockUserTemporarily(passenger.iamUserId, 30); } } @@ -231,9 +221,10 @@ export class FraudService { * Event listener for payment failed */ @OnEvent('payment.failed') - async onPaymentFailed(payload: { intentId: string; userId: string }) { - await this.evaluateRules(payload.userId, 'payment.failed', { - intentId: payload.intentId, + async onPaymentFailed(payload: { booking: { passengerId: string; id: string } }) { + if (!payload.booking?.passengerId) return; + await this.evaluateRules(payload.booking.passengerId, 'payment.failed', { + bookingId: payload.booking.id, }); } @@ -241,9 +232,18 @@ export class FraudService { * Event listener for auth login failed */ @OnEvent('auth.login.failed') - async onLoginFailed(payload: { userId: string; email: string }) { - await this.evaluateRules(payload.userId, 'auth.login.failed', { - email: payload.email, + async onLoginFailed(payload: { email: string }) { + if (!payload.email) return; + const iamRows = await this.dataSource.query<{ id: string }[]>( + `SELECT id FROM iam.users WHERE email = $1 LIMIT 1`, + [payload.email], + ); + if (!iamRows.length) return; + const passenger = await this.prisma.passenger.findUnique({ + where: { iamUserId: iamRows[0].id }, + select: { id: true }, }); + if (!passenger) return; + await this.evaluateRules(passenger.id, 'auth.login.failed', { email: payload.email }); } } diff --git a/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts b/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts index a1fa749ee..a03750965 100644 --- a/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts +++ b/apps/edr-passenger-api/src/modules/passengers/passengers.controller.ts @@ -242,7 +242,7 @@ The API automatically detects: description: 'Invalid JWT token (only if token provided but invalid)' }) registerPassenger(@Body() dto: RegisterPassengerDto, @Request() req: any) { - const userId = req.user?.id ?? req.user?.userId; + const userId = req.user?.id; return this.service.registerPassenger({ ...dto, userId }); }