mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 22:30:55 +00:00
fix(iam): resolve post-merge type errors, migration drift, and IAM integration bugs
This commit is contained in:
@@ -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) } }]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user