fix(iam): resolve post-merge type errors, migration drift, and IAM integration bugs

This commit is contained in:
Abubeker Yasin
2026-06-22 12:12:35 +03:00
parent 7108035f7e
commit e2071b4ff3
5 changed files with 83 additions and 54 deletions

View File

@@ -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) } }]
: []),
];
}

View File

@@ -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 {

View File

@@ -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' };
}
}

View File

@@ -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<string, unknown>,
): 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<boolean> {
private async checkVelocityRule(passengerId: string): Promise<boolean> {
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<boolean> {
private async checkFailedPaymentRule(passengerId: string): Promise<boolean> {
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<string, unknown>,
): Promise<void> {
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 });
}
}

View File

@@ -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 });
}