Skip to content

Commit 09c9f18

Browse files
authored
bal 2980 audit when alert has been changed (#2807)
1 parent 3bb9c87 commit 09c9f18

File tree

5 files changed

+155
-49
lines changed

5 files changed

+155
-49
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- AlterTable
2+
ALTER TABLE "Alert" ADD COLUMN "decisionAt" TIMESTAMP(3),
3+
ADD COLUMN "dedupedAt" TIMESTAMP(3);

services/workflows-service/prisma/schema.prisma

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -777,10 +777,12 @@ model Alert {
777777
project Project @relation(fields: [projectId], references: [id], onUpdate: Cascade, onDelete: NoAction)
778778
779779
dataTimestamp DateTime
780-
state AlertState
781780
status AlertStatus
782781
tags String[]
783782
severity AlertSeverity?
783+
784+
state AlertState
785+
decisionAt DateTime?
784786
785787
executionDetails Json
786788
additionalInfo Json?
@@ -789,6 +791,7 @@ model Alert {
789791
assigneeId String?
790792
assignedAt DateTime?
791793
794+
dedupedAt DateTime?
792795
createdAt DateTime @default(now())
793796
updatedAt DateTime @updatedAt
794797

services/workflows-service/src/alert/alert.service.intg.test.ts

Lines changed: 141 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { faker } from '@faker-js/faker';
1818
import { Test } from '@nestjs/testing';
1919
import {
2020
AlertDefinition,
21+
AlertState,
22+
AlertStatus,
2123
Counterparty,
2224
Customer,
2325
PaymentMethod,
@@ -316,6 +318,145 @@ describe('AlertService', () => {
316318
const alerts = await prismaService.alert.findMany();
317319
expect(alerts).toHaveLength(0);
318320
});
321+
322+
test('Assigning and deciding alerts should set audit timestamps', async () => {
323+
// Arrange
324+
await baseTransactionFactory
325+
.withBusinessBeneficiary()
326+
.withEndUserOriginator()
327+
.amount(ALERT_DEFINITIONS.STRUC_CC.inlineRule.options.amountBetween.min + 1)
328+
.direction(TransactionDirection.inbound)
329+
.paymentMethod(PaymentMethod.credit_card)
330+
.count(ALERT_DEFINITIONS.STRUC_CC.inlineRule.options.amountThreshold + 1)
331+
.create();
332+
333+
// Act
334+
await alertService.checkAllAlerts();
335+
336+
// Assert
337+
const alerts = await prismaService.alert.findMany();
338+
339+
expect(alerts).toHaveLength(1);
340+
341+
const user = await prismaService.user.create({
342+
data: {
343+
firstName: 'Test',
344+
lastName: 'User',
345+
password: '',
346+
email: faker.internet.email(),
347+
roles: [],
348+
},
349+
});
350+
351+
await alertService.updateAlertsAssignee(
352+
alerts.map(alert => alert.id),
353+
project.id,
354+
user.id,
355+
);
356+
357+
const assignedAlerts = await prismaService.alert.findMany({
358+
where: {
359+
assignedAt: {
360+
not: null,
361+
},
362+
},
363+
});
364+
expect(assignedAlerts).toHaveLength(1);
365+
expect(assignedAlerts[0]?.assignedAt).toBeInstanceOf(Date);
366+
expect(assignedAlerts[0]?.assignedAt).not.toBeNull();
367+
// whenever update Decision we set the assignee to the authicated user
368+
await alertService.updateAlertsDecision(
369+
alerts.map(alert => alert.id),
370+
project.id,
371+
AlertState.rejected,
372+
);
373+
374+
const updatedAlerts = await prismaService.alert.findMany({
375+
where: {
376+
decisionAt: {
377+
not: null,
378+
},
379+
},
380+
});
381+
382+
expect(updatedAlerts).toHaveLength(1);
383+
expect(updatedAlerts[0]?.decisionAt).toBeInstanceOf(Date);
384+
expect(updatedAlerts[0]?.decisionAt).not.toBeNull();
385+
expect(updatedAlerts[0]?.status).toBe(AlertStatus.completed);
386+
});
387+
388+
test('Dedupe - Alert should be deduped', async () => {
389+
// Arrange
390+
await baseTransactionFactory
391+
.withBusinessBeneficiary()
392+
.withEndUserOriginator()
393+
.amount(ALERT_DEFINITIONS.STRUC_CC.inlineRule.options.amountBetween.min + 1)
394+
.direction(TransactionDirection.inbound)
395+
.paymentMethod(PaymentMethod.credit_card)
396+
.count(ALERT_DEFINITIONS.STRUC_CC.inlineRule.options.amountThreshold + 1)
397+
.create();
398+
399+
await alertService.checkAllAlerts();
400+
401+
const alerts = await prismaService.alert.findMany();
402+
403+
expect(alerts).toHaveLength(1);
404+
405+
// Act
406+
await alertService.checkAllAlerts();
407+
408+
// Assert
409+
const updatedAlerts = await prismaService.alert.findMany({
410+
where: {
411+
dedupedAt: {
412+
not: null,
413+
},
414+
},
415+
});
416+
417+
expect(updatedAlerts).toHaveLength(1);
418+
expect(updatedAlerts[0]?.dedupedAt).toBeInstanceOf(Date);
419+
expect(updatedAlerts[0]?.dedupedAt).not.toBeNull();
420+
});
421+
422+
test('Dedupe - Only non completed alerts will be dedupe', async () => {
423+
// Arrange
424+
await baseTransactionFactory
425+
.withBusinessBeneficiary()
426+
.withEndUserOriginator()
427+
.amount(ALERT_DEFINITIONS.STRUC_CC.inlineRule.options.amountBetween.min + 1)
428+
.direction(TransactionDirection.inbound)
429+
.paymentMethod(PaymentMethod.credit_card)
430+
.count(ALERT_DEFINITIONS.STRUC_CC.inlineRule.options.amountThreshold + 1)
431+
.create();
432+
433+
await alertService.checkAllAlerts();
434+
435+
const alerts = await prismaService.alert.findMany();
436+
437+
expect(alerts).toHaveLength(1);
438+
439+
// whenever update Decision we set the assignee to the authicated user
440+
await alertService.updateAlertsDecision(
441+
alerts.map(alert => alert.id),
442+
project.id,
443+
AlertState.rejected,
444+
);
445+
446+
// Act
447+
await alertService.checkAllAlerts();
448+
449+
// Assert
450+
const updatedAlerts = await prismaService.alert.findMany({
451+
where: {
452+
dedupedAt: {
453+
not: null,
454+
},
455+
},
456+
});
457+
458+
expect(updatedAlerts).toHaveLength(0);
459+
});
319460
});
320461

321462
describe('Rule: STRUC_APM', () => {
@@ -1013,28 +1154,6 @@ describe('AlertService', () => {
10131154
);
10141155
});
10151156

1016-
it('When there more than 1k credit card transactions, an alert should be created', async () => {
1017-
// Arrange
1018-
await baseTransactionFactory
1019-
.withBusinessBeneficiary()
1020-
.direction(TransactionDirection.inbound)
1021-
.paymentMethod(PaymentMethod.credit_card)
1022-
.amount(ALERT_DEFINITIONS.PAY_HCA_CC.inlineRule.options.amountThreshold + 1)
1023-
.count(1)
1024-
.create();
1025-
1026-
// Act
1027-
await alertService.checkAllAlerts();
1028-
1029-
// Assert
1030-
const alerts = await prismaService.alert.findMany();
1031-
expect(alerts).toHaveLength(1);
1032-
expect(alerts[0]?.alertDefinitionId).toEqual(alertDefinition.id);
1033-
expect(alerts[0] as any).toMatchObject({
1034-
executionDetails: { executionRow: { transactionCount: '1', totalAmount: 1001 } },
1035-
});
1036-
});
1037-
10381157
it('When there are few transaction, no alert should be created', async () => {
10391158
// Arrange
10401159
await baseTransactionFactory
@@ -1113,28 +1232,6 @@ describe('AlertService', () => {
11131232
});
11141233
});
11151234

1116-
it('When there more than 1k credit card transactions, an alert should be created', async () => {
1117-
// Arrange
1118-
await baseTransactionFactory
1119-
.withBusinessBeneficiary()
1120-
.direction(TransactionDirection.inbound)
1121-
.paymentMethod(PaymentMethod.debit_card)
1122-
.amount(2)
1123-
.count(ALERT_DEFINITIONS.PAY_HCA_APM.inlineRule.options.amountThreshold + 1)
1124-
.create();
1125-
1126-
// Act
1127-
await alertService.checkAllAlerts();
1128-
1129-
// Assert
1130-
const alerts = await prismaService.alert.findMany();
1131-
expect(alerts).toHaveLength(1);
1132-
expect(alerts[0]?.alertDefinitionId).toEqual(alertDefinition.id);
1133-
expect(alerts[0] as any).toMatchObject({
1134-
executionDetails: { executionRow: { transactionCount: '1001', totalAmount: 2002 } },
1135-
});
1136-
});
1137-
11381235
it('When there are few transaction, no alert should be created', async () => {
11391236
// Arrange
11401237
await baseTransactionFactory

services/workflows-service/src/alert/alert.service.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export class AlertService {
8282
return await this.alertRepository.updateMany(alertIds, projectId, {
8383
data: {
8484
state: decision,
85+
decisionAt: new Date(),
8586
status: this.getStatusFromState(decision),
8687
},
8788
});
@@ -96,6 +97,7 @@ export class AlertService {
9697
return await this.alertRepository.updateMany(alertIds, projectId, {
9798
data: {
9899
assigneeId: assigneeId,
100+
assignedAt: new Date(),
99101
},
100102
});
101103
} catch (error) {
@@ -300,7 +302,7 @@ export class AlertService {
300302
});
301303
}
302304
} catch (error) {
303-
console.error(error);
305+
this.logger.error('Failed to check alert', { error });
304306

305307
return alertResponse.rejected.push({
306308
status: AlertExecutionStatus.FAILED,
@@ -382,7 +384,7 @@ export class AlertService {
382384
if (existingAlert.status !== AlertStatus.completed) {
383385
await this.alertRepository.updateById(existingAlert.id, {
384386
data: {
385-
updatedAt: new Date(),
387+
dedupedAt: new Date(),
386388
},
387389
});
388390

@@ -473,7 +475,7 @@ export class AlertService {
473475
startDate: undefined,
474476
};
475477

476-
const endDate = alert.updatedAt || alert.createdAt;
478+
const endDate = alert.dedupedAt || alert.createdAt;
477479
endDate.setHours(23, 59, 59, 999);
478480
filters.endDate = endDate;
479481

services/workflows-service/src/alert/dtos/decision-alert.dto.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ApiProperty } from '@nestjs/swagger';
22
import { AlertState } from '@prisma/client';
3-
import { ArrayMinSize, IsArray, IsEnum, IsString } from 'class-validator';
3+
import { ArrayMinSize, IsArray, IsEnum, IsNotEmpty, IsString } from 'class-validator';
44
import { Type } from 'class-transformer';
55

66
export class AlertDecisionDto {
@@ -12,6 +12,7 @@ export class AlertDecisionDto {
1212
@Type(() => Array<String>)
1313
@IsArray()
1414
@IsString({ each: true })
15+
@IsNotEmpty({ each: true })
1516
@ArrayMinSize(1)
1617
alertIds!: string[];
1718

0 commit comments

Comments
 (0)