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 { type: 'VELOCITY' | 'HIGH_VALUE' | 'FAILED_PAYMENTS' | 'MULTIPLE_METHODS'; enabled: boolean; threshold: number; timeWindowMinutes?: number; blockDurationMinutes?: number; } @Injectable() export class FraudService { private readonly logger = new Logger(FraudService.name); constructor( private prisma: PrismaService, @InjectDataSource() private dataSource: DataSource, ) {} /** * Evaluate fraud rules and create alerts if triggered */ async evaluateRules( passengerId: string, eventType: 'booking.created' | 'payment.failed' | 'auth.login.failed', context: Record, ): Promise<{ triggered: boolean; rules: string[] }> { const triggeredRules: string[] = []; if (eventType === 'booking.created') { const velocityTriggered = await this.checkVelocityRule(passengerId); if (velocityTriggered) triggeredRules.push('VELOCITY'); const amount = (context.amountMinor as number) || 0; const highValueTriggered = await this.checkHighValueRule(amount); if (highValueTriggered) triggeredRules.push('HIGH_VALUE'); } if (eventType === 'payment.failed') { const failedPaymentTriggered = await this.checkFailedPaymentRule(passengerId); if (failedPaymentTriggered) triggeredRules.push('FAILED_PAYMENTS'); } if (triggeredRules.length > 0) { await this.createFraudAlert(passengerId, eventType, triggeredRules, context); return { triggered: true, rules: triggeredRules }; } return { triggered: false, rules: [] }; } /** * Check velocity rule: X bookings in Y minutes */ private async checkVelocityRule(passengerId: string): Promise { const rule = await this.prisma.fraudRule.findFirst({ where: { type: 'VELOCITY', enabled: true }, }); if (!rule) return false; const timeWindowMinutes = (rule.config as any)?.timeWindowMinutes || 30; const bookingCount = await this.prisma.booking.count({ where: { passengerId, createdAt: { gte: new Date(Date.now() - timeWindowMinutes * 60 * 1000) }, }, }); return bookingCount > rule.threshold; } /** * Check high-value booking rule */ private async checkHighValueRule(amountMinor: number): Promise { const rule = await this.prisma.fraudRule.findFirst({ where: { type: 'HIGH_VALUE', enabled: true }, }); if (!rule) return false; // threshold is in ETB (convert minor units to ETB) const amountEtb = amountMinor / 100; return amountEtb > rule.threshold; } /** * Check failed payment rule: X failed attempts in Y minutes */ private async checkFailedPaymentRule(passengerId: string): Promise { const rule = await this.prisma.fraudRule.findFirst({ where: { type: 'FAILED_PAYMENTS', enabled: true }, }); if (!rule) return false; const timeWindowMinutes = (rule.config as any)?.timeWindowMinutes || 60; const failedCount = await this.prisma.paymentIntent.count({ where: { booking: { passengerId }, status: 'FAILED', updatedAt: { gte: new Date(Date.now() - timeWindowMinutes * 60 * 1000) }, }, }); return failedCount > rule.threshold; } /** * Create a fraud alert */ private async createFraudAlert( 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: passenger?.iamUserId ?? passengerId, eventType, triggeredRules, context: context as any, severity: triggeredRules.length > 1 ? 'HIGH' : 'MEDIUM', }, }); this.logger.warn(`Fraud alert created: ${alert.id} for passenger ${passengerId} - rules: ${triggeredRules.join(', ')}`); if (triggeredRules.includes('HIGH_VALUE') || triggeredRules.length > 1) { if (passenger?.iamUserId) await this.blockUserTemporarily(passenger.iamUserId, 30); } } /** * Block user temporarily */ async blockUserTemporarily(iamUserId: string, durationMinutes: number): Promise { const blockedUntil = new Date(Date.now() + durationMinutes * 60 * 1000); await this.prisma.passenger.updateMany({ where: { iamUserId }, data: { blockedUntil }, }); this.logger.warn(`Passenger (iamUserId=${iamUserId}) blocked until ${blockedUntil.toISOString()}`); } /** * Unblock user */ async unblockUser(iamUserId: string): Promise { await this.prisma.passenger.updateMany({ where: { iamUserId }, data: { blockedUntil: null }, }); this.logger.log(`Passenger (iamUserId=${iamUserId}) unblocked`); } /** * Get all fraud alerts */ async getAlerts(userId?: string, limit = 100, offset = 0) { return this.prisma.fraudAlert.findMany({ where: userId ? { iamUserId: userId } : {}, orderBy: { createdAt: 'desc' }, take: limit, skip: offset, }); } /** * Create or update a fraud rule */ async upsertRule( type: string, config: FraudRuleConfig, ) { return this.prisma.fraudRule.upsert({ where: { type: type as any }, update: { enabled: config.enabled, threshold: config.threshold, config: config as any, }, create: { type: type as any, enabled: config.enabled, threshold: config.threshold, config: config as any, }, }); } /** * Get all fraud rules */ async getRules() { return this.prisma.fraudRule.findMany(); } /** * Event listener for booking created */ @OnEvent('booking.created') async onBookingCreated(payload: { booking: any }) { await this.evaluateRules(payload.booking.passengerId, 'booking.created', { bookingId: payload.booking.id, amountMinor: payload.booking.totalMinor, }); } /** * Event listener for payment failed */ @OnEvent('payment.failed') 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, }); } /** * Event listener for auth login failed */ @OnEvent('auth.login.failed') 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 }); } }