mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 22:58:17 +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 = {};
|
const where: any = {};
|
||||||
|
|
||||||
if (search) {
|
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 = [
|
where.OR = [
|
||||||
{ bookingRef: { contains: search, mode: 'insensitive' } },
|
{ bookingRef: { contains: search, mode: 'insensitive' } },
|
||||||
{ contactEmail: { contains: search, mode: 'insensitive' } },
|
{ contactEmail: { contains: search, mode: 'insensitive' } },
|
||||||
{ contactPhone: { 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 { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectDataSource } from '@nestjs/typeorm';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
import { PrismaService } from '../../common/prisma.service';
|
import { PrismaService } from '../../common/prisma.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DashboardService {
|
export class DashboardService {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(
|
||||||
|
private prisma: PrismaService,
|
||||||
|
@InjectDataSource() private dataSource: DataSource,
|
||||||
|
) {}
|
||||||
|
|
||||||
async getHomeDashboard(passengerId: string) {
|
async getHomeDashboard(passengerId: string) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const [passenger, upcomingBooking, wallet, promos, weatherAlerts, stationSignals, savedRoutes] = await Promise.all([
|
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({
|
this.prisma.booking.findFirst({
|
||||||
where: { passengerId, status: 'CONFIRMED', schedule: { departureAt: { gte: now } } },
|
where: { passengerId, status: 'CONFIRMED', schedule: { departureAt: { gte: now } } },
|
||||||
include: {
|
include: {
|
||||||
@@ -27,7 +32,16 @@ export class DashboardService {
|
|||||||
|
|
||||||
const hour = now.getHours();
|
const hour = now.getHours();
|
||||||
const greetingKey = hour < 12 ? 'MORNING' : hour < 17 ? 'AFTERNOON' : 'EVENING';
|
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];
|
const seat = upcomingBooking?.seats[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ export class FraudController {
|
|||||||
*/
|
*/
|
||||||
@Post('actions/block')
|
@Post('actions/block')
|
||||||
@ApiOperation({ summary: 'Block user temporarily' })
|
@ApiOperation({ summary: 'Block user temporarily' })
|
||||||
async blockUser(@Body() body: { userId: string; durationMinutes: number }) {
|
async blockUser(@Body() body: { iamUserId: string; durationMinutes: number }) {
|
||||||
await this.fraudService.blockUserTemporarily(body.userId, body.durationMinutes);
|
await this.fraudService.blockUserTemporarily(body.iamUserId, body.durationMinutes);
|
||||||
return { message: `User blocked for ${body.durationMinutes} minutes` };
|
return { message: `User blocked for ${body.durationMinutes} minutes` };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +64,8 @@ export class FraudController {
|
|||||||
*/
|
*/
|
||||||
@Post('actions/unblock')
|
@Post('actions/unblock')
|
||||||
@ApiOperation({ summary: 'Unblock user' })
|
@ApiOperation({ summary: 'Unblock user' })
|
||||||
async unblockUser(@Body() body: { userId: string }) {
|
async unblockUser(@Body() body: { iamUserId: string }) {
|
||||||
await this.fraudService.unblockUser(body.userId);
|
await this.fraudService.unblockUser(body.iamUserId);
|
||||||
return { message: 'User unblocked' };
|
return { message: 'User unblocked' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { InjectDataSource } from '@nestjs/typeorm';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
import { PrismaService } from '../../common/prisma.service';
|
import { PrismaService } from '../../common/prisma.service';
|
||||||
|
|
||||||
export interface FraudRuleConfig {
|
export interface FraudRuleConfig {
|
||||||
@@ -14,44 +16,37 @@ export interface FraudRuleConfig {
|
|||||||
export class FraudService {
|
export class FraudService {
|
||||||
private readonly logger = new Logger(FraudService.name);
|
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
|
* Evaluate fraud rules and create alerts if triggered
|
||||||
*/
|
*/
|
||||||
async evaluateRules(
|
async evaluateRules(
|
||||||
userId: string,
|
passengerId: string,
|
||||||
eventType: 'booking.created' | 'payment.failed' | 'auth.login.failed',
|
eventType: 'booking.created' | 'payment.failed' | 'auth.login.failed',
|
||||||
context: Record<string, unknown>,
|
context: Record<string, unknown>,
|
||||||
): Promise<{ triggered: boolean; rules: string[] }> {
|
): Promise<{ triggered: boolean; rules: string[] }> {
|
||||||
const triggeredRules: string[] = [];
|
const triggeredRules: string[] = [];
|
||||||
|
|
||||||
// Check velocity rule (multiple bookings in short time)
|
|
||||||
if (eventType === 'booking.created') {
|
if (eventType === 'booking.created') {
|
||||||
const velocityTriggered = await this.checkVelocityRule(userId);
|
const velocityTriggered = await this.checkVelocityRule(passengerId);
|
||||||
if (velocityTriggered) {
|
if (velocityTriggered) triggeredRules.push('VELOCITY');
|
||||||
triggeredRules.push('VELOCITY');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check high-value booking
|
|
||||||
const amount = (context.amountMinor as number) || 0;
|
const amount = (context.amountMinor as number) || 0;
|
||||||
const highValueTriggered = await this.checkHighValueRule(amount);
|
const highValueTriggered = await this.checkHighValueRule(amount);
|
||||||
if (highValueTriggered) {
|
if (highValueTriggered) triggeredRules.push('HIGH_VALUE');
|
||||||
triggeredRules.push('HIGH_VALUE');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check repeated failed payments
|
|
||||||
if (eventType === 'payment.failed') {
|
if (eventType === 'payment.failed') {
|
||||||
const failedPaymentTriggered = await this.checkFailedPaymentRule(userId);
|
const failedPaymentTriggered = await this.checkFailedPaymentRule(passengerId);
|
||||||
if (failedPaymentTriggered) {
|
if (failedPaymentTriggered) triggeredRules.push('FAILED_PAYMENTS');
|
||||||
triggeredRules.push('FAILED_PAYMENTS');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create alert if rules triggered
|
|
||||||
if (triggeredRules.length > 0) {
|
if (triggeredRules.length > 0) {
|
||||||
await this.createFraudAlert(userId, eventType, triggeredRules, context);
|
await this.createFraudAlert(passengerId, eventType, triggeredRules, context);
|
||||||
return { triggered: true, rules: triggeredRules };
|
return { triggered: true, rules: triggeredRules };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +56,7 @@ export class FraudService {
|
|||||||
/**
|
/**
|
||||||
* Check velocity rule: X bookings in Y minutes
|
* 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({
|
const rule = await this.prisma.fraudRule.findFirst({
|
||||||
where: { type: 'VELOCITY', enabled: true },
|
where: { type: 'VELOCITY', enabled: true },
|
||||||
});
|
});
|
||||||
@@ -69,18 +64,14 @@ export class FraudService {
|
|||||||
if (!rule) return false;
|
if (!rule) return false;
|
||||||
|
|
||||||
const timeWindowMinutes = (rule.config as any)?.timeWindowMinutes || 30;
|
const timeWindowMinutes = (rule.config as any)?.timeWindowMinutes || 30;
|
||||||
const threshold = rule.threshold;
|
|
||||||
|
|
||||||
const bookingCount = await this.prisma.booking.count({
|
const bookingCount = await this.prisma.booking.count({
|
||||||
where: {
|
where: {
|
||||||
passengerId: userId,
|
passengerId,
|
||||||
createdAt: {
|
createdAt: { gte: new Date(Date.now() - timeWindowMinutes * 60 * 1000) },
|
||||||
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
|
* 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({
|
const rule = await this.prisma.fraudRule.findFirst({
|
||||||
where: { type: 'FAILED_PAYMENTS', enabled: true },
|
where: { type: 'FAILED_PAYMENTS', enabled: true },
|
||||||
});
|
});
|
||||||
@@ -109,33 +100,33 @@ export class FraudService {
|
|||||||
if (!rule) return false;
|
if (!rule) return false;
|
||||||
|
|
||||||
const timeWindowMinutes = (rule.config as any)?.timeWindowMinutes || 60;
|
const timeWindowMinutes = (rule.config as any)?.timeWindowMinutes || 60;
|
||||||
const threshold = rule.threshold;
|
|
||||||
|
|
||||||
const failedCount = await this.prisma.paymentIntent.count({
|
const failedCount = await this.prisma.paymentIntent.count({
|
||||||
where: {
|
where: {
|
||||||
booking: { passengerId: userId },
|
booking: { passengerId },
|
||||||
status: 'FAILED',
|
status: 'FAILED',
|
||||||
updatedAt: {
|
updatedAt: { gte: new Date(Date.now() - timeWindowMinutes * 60 * 1000) },
|
||||||
gte: new Date(Date.now() - timeWindowMinutes * 60 * 1000),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return failedCount > threshold;
|
return failedCount > rule.threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a fraud alert
|
* Create a fraud alert
|
||||||
*/
|
*/
|
||||||
private async createFraudAlert(
|
private async createFraudAlert(
|
||||||
userId: string,
|
passengerId: string,
|
||||||
eventType: string,
|
eventType: string,
|
||||||
triggeredRules: string[],
|
triggeredRules: string[],
|
||||||
context: Record<string, unknown>,
|
context: Record<string, unknown>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const passenger = await this.prisma.passenger.findUnique({
|
||||||
|
where: { id: passengerId },
|
||||||
|
select: { iamUserId: true },
|
||||||
|
});
|
||||||
const alert = await this.prisma.fraudAlert.create({
|
const alert = await this.prisma.fraudAlert.create({
|
||||||
data: {
|
data: {
|
||||||
iamUserId: userId,
|
iamUserId: passenger?.iamUserId ?? passengerId,
|
||||||
eventType,
|
eventType,
|
||||||
triggeredRules,
|
triggeredRules,
|
||||||
context: context as any,
|
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) {
|
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
|
* Event listener for payment failed
|
||||||
*/
|
*/
|
||||||
@OnEvent('payment.failed')
|
@OnEvent('payment.failed')
|
||||||
async onPaymentFailed(payload: { intentId: string; userId: string }) {
|
async onPaymentFailed(payload: { booking: { passengerId: string; id: string } }) {
|
||||||
await this.evaluateRules(payload.userId, 'payment.failed', {
|
if (!payload.booking?.passengerId) return;
|
||||||
intentId: payload.intentId,
|
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
|
* Event listener for auth login failed
|
||||||
*/
|
*/
|
||||||
@OnEvent('auth.login.failed')
|
@OnEvent('auth.login.failed')
|
||||||
async onLoginFailed(payload: { userId: string; email: string }) {
|
async onLoginFailed(payload: { email: string }) {
|
||||||
await this.evaluateRules(payload.userId, 'auth.login.failed', {
|
if (!payload.email) return;
|
||||||
email: payload.email,
|
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)'
|
description: 'Invalid JWT token (only if token provided but invalid)'
|
||||||
})
|
})
|
||||||
registerPassenger(@Body() dto: RegisterPassengerDto, @Request() req: any) {
|
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 });
|
return this.service.registerPassenger({ ...dto, userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user