Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions backend/src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { RecurringPaymentService } from "./modules/recurring-payments/recurringP
import { RecurringPaymentWorker } from "./modules/recurring-payments/recurringPaymentWorker";
import { InMemoryRecurringPaymentRepository } from "./modules/recurring-payments/inMemoryRecurringPaymentRepository";
import { ComplianceLogService } from "./modules/compliance/complianceLogService";
import { ComplianceViolationAlertService } from "./modules/compliance/complianceViolationAlertService";
import { AuditStorageService } from "./modules/compliance/auditStorageService";
import { ReconciliationService } from "./modules/reconciliation/reconciliationService";
import { StressTestService } from "./modules/stress/stressTestService";
Expand Down Expand Up @@ -49,7 +50,9 @@ export interface AppContainer {
countryMetadata: CountryMetadataService;
compliance: ComplianceService;
complianceLog: ComplianceLogService;
complianceViolations: ComplianceViolationAlertService;
fraud: FraudService;
fraudReview: FraudReviewService;
notifications: NotificationService;
notification: NotificationService;
activity: ActivityService;
Expand All @@ -62,6 +65,8 @@ export interface AppContainer {
adminAlerts: AdminAlertService;
operationalMetrics: OperationalMetricsService;
auditStorage: AuditStorageService;
reconciliation: ReconciliationService;
stressTest: StressTestService;
authRiskEngine: AuthRiskEngine;
deadLetterQueue: DeadLetterQueue;
settlementAnalytics: SettlementAnalyticsService;
Expand All @@ -80,7 +85,7 @@ export function createContainer(): AppContainer {
const compliance = new ComplianceService();
const fraud = new FraudService();
const wallets = new WalletService();
const contracts = new ContractService();
const contracts = new ContractService(eventBus);
const countryMetadata = new CountryMetadataService();
const transferRepository = new InMemoryTransferRepository(
createDemoTransfers(),
Expand Down Expand Up @@ -124,6 +129,12 @@ export function createContainer(): AppContainer {
const authRiskEngine = new AuthRiskEngine(eventBus);
const settlementAnalytics = new SettlementAnalyticsService(eventBus);
const adminAlerts = new AdminAlertService(eventBus);
const complianceViolations = new ComplianceViolationAlertService(
eventBus,
complianceLog,
adminAlerts,
contracts.complianceLimits,
);
const operationalMetrics = new OperationalMetricsService();
const transactionApproval = new TransactionApprovalService();
const successRate = new SuccessRateService();
Expand Down Expand Up @@ -174,8 +185,10 @@ export function createContainer(): AppContainer {
countryMetadata,
compliance,
complianceLog,
complianceViolations,
fraud,
fraudReview,
fraudReview,
notifications,
notification: notifications,
activity,
health,
Expand All @@ -187,6 +200,8 @@ export function createContainer(): AppContainer {
adminAlerts,
operationalMetrics,
auditStorage,
reconciliation,
stressTest,
authRiskEngine,
deadLetterQueue,
settlementAnalytics,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { EventBus } from '../../../core/eventBus';
import { AdminAlertService } from '../../system/adminAlertService';
import { ComplianceLogService } from '../complianceLogService';
import { ComplianceViolationAlertService } from '../complianceViolationAlertService';
import type { ContractEvent } from '../../../services/contractService';

describe('ComplianceViolationAlertService', () => {
let eventBus: EventBus;
let complianceLogs: ComplianceLogService;
let adminAlerts: AdminAlertService;
let service: ComplianceViolationAlertService;

beforeEach(() => {
eventBus = new EventBus();
complianceLogs = new ComplianceLogService(eventBus);
adminAlerts = new AdminAlertService(eventBus);
service = new ComplianceViolationAlertService(
eventBus,
complianceLogs,
adminAlerts,
'compliance_contract',
);
});

it('creates an admin alert when a compliance log is blocked', async () => {
const log = complianceLogs.createLog({
userId: 'user_123',
transferId: 'transfer_123',
checkType: 'aml',
status: 'blocked',
riskScore: 85,
flags: ['high_risk_country', 'very_large_amount'],
metadata: { amount: 15000 },
checkedBy: 'system',
});

await new Promise(process.nextTick);

const violations = service.getViolations();
expect(violations).toHaveLength(1);
expect(violations[0]).toEqual(
expect.objectContaining({
source: 'compliance_log',
severity: 'critical',
userId: 'user_123',
transferId: 'transfer_123',
complianceLogId: log.id,
}),
);

const alerts = adminAlerts.getAlerts();
expect(alerts).toHaveLength(1);
expect(alerts[0]).toEqual(
expect.objectContaining({
type: 'compliance_violation',
severity: 'critical',
transferId: 'transfer_123',
userId: 'user_123',
}),
);
});

it('ignores passing compliance logs', async () => {
complianceLogs.createLog({
userId: 'user_123',
checkType: 'aml',
status: 'passed',
riskScore: 5,
flags: [],
metadata: {},
checkedBy: 'system',
});

await new Promise(process.nextTick);

expect(service.getViolations()).toHaveLength(0);
expect(adminAlerts.getAlerts()).toHaveLength(0);
});

it('detects compliance contract events that deny a transfer', async () => {
const contractEvent: ContractEvent = {
id: 'evt_blocked',
contractId: 'compliance_contract',
method: 'inspect',
args: ['user_abc', 12000],
result: {
allowed: false,
status: 'blocked',
riskScore: 90,
flags: ['daily_limit_exceeded'],
transferId: 'transfer_abc',
},
timestamp: new Date().toISOString(),
};

await eventBus.publish({
type: 'contract.event_recorded',
timestamp: contractEvent.timestamp,
payload: contractEvent,
});

const violations = service.getViolations({ source: 'contract_event' });
expect(violations).toHaveLength(1);
expect(violations[0]).toEqual(
expect.objectContaining({
severity: 'critical',
userId: 'user_abc',
transferId: 'transfer_abc',
sourceEventId: 'evt_blocked',
}),
);
expect(adminAlerts.getAlerts()[0].type).toBe('compliance_violation');
});

it('deduplicates repeated source events', async () => {
const contractEvent: ContractEvent = {
id: 'evt_duplicate',
contractId: 'compliance_contract',
method: 'limit_exceeded',
args: ['user_dup', 9000],
result: { violation: true, flags: ['monthly_limit_exceeded'] },
timestamp: new Date().toISOString(),
};

await eventBus.publish({
type: 'contract.event_recorded',
timestamp: contractEvent.timestamp,
payload: contractEvent,
});
await eventBus.publish({
type: 'contract.event_recorded',
timestamp: contractEvent.timestamp,
payload: contractEvent,
});

expect(service.getViolations()).toHaveLength(1);
expect(adminAlerts.getAlerts()).toHaveLength(1);
});

it('generates a compliance violation report', async () => {
complianceLogs.createLog({
userId: 'user_report',
checkType: 'sanctions',
status: 'flagged',
riskScore: 55,
flags: ['sanctions_review'],
metadata: {},
checkedBy: 'system',
});

await new Promise(process.nextTick);

const report = service.generateReport({ userId: 'user_report' });
expect(report.summary).toEqual(
expect.objectContaining({
total: 1,
medium: 1,
unresolved: 0,
}),
);
expect(report.summary.bySource.compliance_log).toBe(1);
expect(report.violations[0].userId).toBe('user_report');
});
});
4 changes: 1 addition & 3 deletions backend/src/modules/compliance/auditStorageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,6 @@ export class AuditStorageService {
private getLatestRecord(): AuditRecord | undefined {
const records = Array.from(this.store.values());
if (records.length === 0) return undefined;
return records.reduce((latest, record) =>
new Date(record.storedAt).getTime() > new Date(latest.storedAt).getTime() ? record : latest,
);
return records[records.length - 1];
}
}
4 changes: 4 additions & 0 deletions backend/src/modules/compliance/complianceLogService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ export class ComplianceLogService {
return log;
}

getLogById(logId: string): ComplianceLog | null {
return this.logs.find((log) => log.id === logId) ?? null;
}

/**
* Get logs for a specific user
*/
Expand Down
Loading
Loading