Initial implementation: Brazil SWIFT Operations Platform
- Complete monorepo structure with pnpm workspaces and Turborepo - All packages implemented: types, utils, rules-engine, iso20022, treasury, risk-models, audit - React web application with TypeScript and Tailwind CSS - Full Brazil regulatory compliance (BCB requirements) - ISO 20022 message support (pacs.008, pacs.009, pain.001) - Treasury and subledger management - Risk, capital, and liquidity stress allocation - Audit logging and BCB reporting - E&O +10% uplift implementation
This commit is contained in:
236
packages/rules-engine/src/aml.ts
Normal file
236
packages/rules-engine/src/aml.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* AML (Anti-Money Laundering) and anti-structuring detection
|
||||
*/
|
||||
|
||||
import type {
|
||||
Transaction,
|
||||
AMLCheckResult,
|
||||
SingleTransactionAMLResult,
|
||||
StructuringCheckResult,
|
||||
RuleResult,
|
||||
RuleSeverity,
|
||||
RuleDecision,
|
||||
} from '@brazil-swift-ops/types';
|
||||
import { getDefaultConverter } from '@brazil-swift-ops/utils';
|
||||
import { calculateRollingWindow, filterDatesInWindow } from '@brazil-swift-ops/utils';
|
||||
import { getConfig } from './config';
|
||||
|
||||
/**
|
||||
* Transaction history for structuring detection (in production, this would be a database)
|
||||
*/
|
||||
interface TransactionHistory {
|
||||
transactionId: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
usdEquivalent: number;
|
||||
date: Date;
|
||||
orderingCustomerTaxId?: string;
|
||||
beneficiaryTaxId?: string;
|
||||
}
|
||||
|
||||
class TransactionHistoryStore {
|
||||
private history: TransactionHistory[] = [];
|
||||
|
||||
add(entry: TransactionHistory): void {
|
||||
this.history.push(entry);
|
||||
}
|
||||
|
||||
getByDateRange(startDate: Date, endDate: Date): TransactionHistory[] {
|
||||
return this.history.filter(
|
||||
(entry) => entry.date >= startDate && entry.date <= endDate
|
||||
);
|
||||
}
|
||||
|
||||
getByCustomer(
|
||||
taxId: string,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): TransactionHistory[] {
|
||||
return this.history.filter(
|
||||
(entry) =>
|
||||
(entry.orderingCustomerTaxId === taxId ||
|
||||
entry.beneficiaryTaxId === taxId) &&
|
||||
entry.date >= startDate &&
|
||||
entry.date <= endDate
|
||||
);
|
||||
}
|
||||
|
||||
getAll(): TransactionHistory[] {
|
||||
return [...this.history];
|
||||
}
|
||||
}
|
||||
|
||||
const historyStore = new TransactionHistoryStore();
|
||||
|
||||
export function getHistoryStore(): TransactionHistoryStore {
|
||||
return historyStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check single transaction AML threshold
|
||||
*/
|
||||
export function checkSingleTransactionAML(
|
||||
transaction: Transaction
|
||||
): SingleTransactionAMLResult {
|
||||
const config = getConfig();
|
||||
const converter = getDefaultConverter();
|
||||
|
||||
const usdEquivalent = converter.getUSDEquivalent(
|
||||
transaction.amount,
|
||||
transaction.currency
|
||||
);
|
||||
|
||||
const threshold = config.aml.singleTransactionThreshold;
|
||||
const requiresEnhancedReview = usdEquivalent >= threshold;
|
||||
|
||||
let riskLevel: 'Low' | 'Medium' | 'High';
|
||||
if (usdEquivalent >= threshold) {
|
||||
riskLevel = 'High';
|
||||
} else if (usdEquivalent >= threshold * 0.5) {
|
||||
riskLevel = 'Medium';
|
||||
} else {
|
||||
riskLevel = 'Low';
|
||||
}
|
||||
|
||||
return {
|
||||
passed: true, // AML check doesn't fail, it flags for review
|
||||
transactionAmount: transaction.amount,
|
||||
usdEquivalent,
|
||||
threshold,
|
||||
requiresEnhancedReview,
|
||||
riskLevel,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for structuring patterns (multiple small transactions that sum above threshold)
|
||||
*/
|
||||
export function checkStructuring(
|
||||
transaction: Transaction,
|
||||
historicalTransactions?: TransactionHistory[]
|
||||
): StructuringCheckResult | undefined {
|
||||
const config = getConfig();
|
||||
const converter = getDefaultConverter();
|
||||
|
||||
// Calculate rolling window
|
||||
const window = calculateRollingWindow(
|
||||
transaction.createdAt || new Date(),
|
||||
config.aml.structuringWindowDays
|
||||
);
|
||||
|
||||
// Get historical transactions if not provided
|
||||
if (!historicalTransactions) {
|
||||
const customerTaxId =
|
||||
transaction.orderingCustomerTaxId || transaction.beneficiary?.taxId;
|
||||
if (customerTaxId) {
|
||||
historicalTransactions = historyStore.getByCustomer(
|
||||
customerTaxId,
|
||||
window.startDate,
|
||||
window.endDate
|
||||
);
|
||||
} else {
|
||||
historicalTransactions = historyStore.getByDateRange(
|
||||
window.startDate,
|
||||
window.endDate
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter to transactions in window
|
||||
const windowTransactions = historicalTransactions.filter((t) =>
|
||||
filterDatesInWindow([t.date], window).length > 0
|
||||
);
|
||||
|
||||
// Calculate totals
|
||||
const totalAmount = windowTransactions.reduce(
|
||||
(sum, t) => sum + t.amount,
|
||||
transaction.amount
|
||||
);
|
||||
const totalUsdEquivalent = windowTransactions.reduce(
|
||||
(sum, t) => sum + t.usdEquivalent,
|
||||
converter.getUSDEquivalent(transaction.amount, transaction.currency)
|
||||
);
|
||||
|
||||
const individualAmounts = [
|
||||
...windowTransactions.map((t) => t.usdEquivalent),
|
||||
converter.getUSDEquivalent(transaction.amount, transaction.currency),
|
||||
];
|
||||
|
||||
// Check if structuring detected
|
||||
const threshold = config.aml.structuringThreshold;
|
||||
const detected =
|
||||
totalUsdEquivalent >= threshold &&
|
||||
individualAmounts.every((amt) => amt < threshold);
|
||||
|
||||
return {
|
||||
detected,
|
||||
windowDays: window.days,
|
||||
transactionCount: windowTransactions.length + 1,
|
||||
totalAmount,
|
||||
totalUsdEquivalent,
|
||||
individualAmounts,
|
||||
rationale: detected
|
||||
? `Structuring detected: ${windowTransactions.length + 1} transactions totaling ${totalUsdEquivalent.toFixed(2)} USD over ${window.days} days, each below ${threshold} USD threshold.`
|
||||
: `No structuring pattern detected. Total: ${totalUsdEquivalent.toFixed(2)} USD over ${window.days} days.`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform complete AML check
|
||||
*/
|
||||
export function performAMLCheck(
|
||||
transaction: Transaction,
|
||||
historicalTransactions?: TransactionHistory[]
|
||||
): AMLCheckResult {
|
||||
const singleCheck = checkSingleTransactionAML(transaction);
|
||||
const structuringCheck = checkStructuring(transaction, historicalTransactions);
|
||||
|
||||
// Determine overall risk level
|
||||
let overallRiskLevel: 'Low' | 'Medium' | 'High';
|
||||
if (singleCheck.riskLevel === 'High' || structuringCheck?.detected) {
|
||||
overallRiskLevel = 'High';
|
||||
} else if (singleCheck.riskLevel === 'Medium') {
|
||||
overallRiskLevel = 'Medium';
|
||||
} else {
|
||||
overallRiskLevel = 'Low';
|
||||
}
|
||||
|
||||
const passed = overallRiskLevel !== 'High';
|
||||
|
||||
return {
|
||||
passed,
|
||||
singleTransactionCheck: singleCheck,
|
||||
structuringCheck,
|
||||
overallRiskLevel,
|
||||
rationale: passed
|
||||
? `AML check passed. Risk level: ${overallRiskLevel}.`
|
||||
: `AML check flagged for review. Risk level: ${overallRiskLevel}. ${structuringCheck?.detected ? 'Structuring pattern detected.' : ''}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create rule result for AML check
|
||||
*/
|
||||
export function createAMLRuleResult(check: AMLCheckResult): RuleResult {
|
||||
const severity: RuleSeverity =
|
||||
check.overallRiskLevel === 'High'
|
||||
? 'Critical'
|
||||
: check.overallRiskLevel === 'Medium'
|
||||
? 'Warning'
|
||||
: 'Info';
|
||||
const decision: RuleDecision = check.passed ? 'Allow' : 'Escalate';
|
||||
|
||||
return {
|
||||
ruleId: 'aml-check',
|
||||
ruleName: 'AML & Anti-Structuring Check',
|
||||
passed: check.passed,
|
||||
severity,
|
||||
decision,
|
||||
rationale: check.rationale,
|
||||
details: {
|
||||
overallRiskLevel: check.overallRiskLevel,
|
||||
singleTransactionCheck: check.singleTransactionCheck,
|
||||
structuringCheck: check.structuringCheck,
|
||||
},
|
||||
};
|
||||
}
|
||||
157
packages/rules-engine/src/fx-contract.ts
Normal file
157
packages/rules-engine/src/fx-contract.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import type {
|
||||
Transaction,
|
||||
FXContract,
|
||||
FXContractCheckResult,
|
||||
RuleResult,
|
||||
RuleSeverity,
|
||||
RuleDecision,
|
||||
} from '@brazil-swift-ops/types';
|
||||
import { isEffectiveDate } from '@brazil-swift-ops/utils';
|
||||
|
||||
class FXContractStore {
|
||||
private contracts: Map<string, FXContract> = new Map();
|
||||
|
||||
add(contract: FXContract): void {
|
||||
this.contracts.set(contract.contractId, contract);
|
||||
}
|
||||
|
||||
get(contractId: string): FXContract | undefined {
|
||||
return this.contracts.get(contractId);
|
||||
}
|
||||
|
||||
getAll(): FXContract[] {
|
||||
return Array.from(this.contracts.values());
|
||||
}
|
||||
|
||||
updateRemainingAmount(contractId: string, usedAmount: number): void {
|
||||
const contract = this.contracts.get(contractId);
|
||||
if (contract) {
|
||||
contract.usedAmount += usedAmount;
|
||||
contract.remainingAmount = contract.amount - contract.usedAmount;
|
||||
contract.updatedAt = new Date();
|
||||
if (contract.remainingAmount <= 0) {
|
||||
contract.status = 'exhausted';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const contractStore = new FXContractStore();
|
||||
|
||||
export function getContractStore(): FXContractStore {
|
||||
return contractStore;
|
||||
}
|
||||
|
||||
export function validateFXContract(
|
||||
transaction: Transaction,
|
||||
contract?: FXContract
|
||||
): FXContractCheckResult {
|
||||
if (!transaction.fxContractId) {
|
||||
return {
|
||||
passed: false,
|
||||
contractExists: false,
|
||||
contractType: undefined,
|
||||
contractAmount: 0,
|
||||
contractRemainingAmount: 0,
|
||||
transactionAmount: transaction.amount,
|
||||
amountWithinLimit: false,
|
||||
rationale: 'FX contract ID is required for cross-border transactions.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!contract) {
|
||||
contract = contractStore.get(transaction.fxContractId);
|
||||
}
|
||||
|
||||
if (!contract) {
|
||||
return {
|
||||
passed: false,
|
||||
fxContractId: transaction.fxContractId,
|
||||
contractExists: false,
|
||||
contractType: undefined,
|
||||
contractAmount: 0,
|
||||
contractRemainingAmount: 0,
|
||||
transactionAmount: transaction.amount,
|
||||
amountWithinLimit: false,
|
||||
rationale: `FX contract ${transaction.fxContractId} not found.`,
|
||||
};
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const contractActive =
|
||||
contract.status === 'active' &&
|
||||
isEffectiveDate(now, contract.effectiveDate, contract.expiryDate);
|
||||
|
||||
if (!contractActive) {
|
||||
return {
|
||||
passed: false,
|
||||
fxContractId: contract.contractId,
|
||||
contractExists: true,
|
||||
contractActive: false,
|
||||
contractType: contract.type,
|
||||
contractAmount: contract.amount,
|
||||
contractRemainingAmount: contract.remainingAmount,
|
||||
transactionAmount: transaction.amount,
|
||||
amountWithinLimit: false,
|
||||
rationale: `FX contract ${contract.contractId} is not active (status: ${contract.status}).`,
|
||||
};
|
||||
}
|
||||
|
||||
if (contract.type !== transaction.direction) {
|
||||
return {
|
||||
passed: false,
|
||||
fxContractId: contract.contractId,
|
||||
contractExists: true,
|
||||
contractActive: false,
|
||||
contractType: contract.type,
|
||||
contractAmount: contract.amount,
|
||||
contractRemainingAmount: contract.remainingAmount,
|
||||
transactionAmount: transaction.amount,
|
||||
amountWithinLimit: false,
|
||||
rationale: `FX contract type (${contract.type}) does not match transaction direction (${transaction.direction}).`,
|
||||
};
|
||||
}
|
||||
|
||||
const amountWithinLimit = transaction.amount <= contract.remainingAmount;
|
||||
|
||||
return {
|
||||
passed: amountWithinLimit && contractActive,
|
||||
fxContractId: contract.contractId,
|
||||
contractExists: true,
|
||||
contractActive,
|
||||
contractType: contract.type,
|
||||
contractAmount: contract.amount,
|
||||
contractRemainingAmount: contract.remainingAmount,
|
||||
transactionAmount: transaction.amount,
|
||||
amountWithinLimit,
|
||||
rationale: amountWithinLimit
|
||||
? `Transaction amount (${transaction.amount} ${transaction.currency}) is within FX contract limit (${contract.remainingAmount} ${contract.currency} remaining).`
|
||||
: `Transaction amount (${transaction.amount} ${transaction.currency}) exceeds FX contract remaining amount (${contract.remainingAmount} ${contract.currency}).`,
|
||||
};
|
||||
}
|
||||
|
||||
export function createFXContractRuleResult(
|
||||
check: FXContractCheckResult
|
||||
): RuleResult {
|
||||
const severity: RuleSeverity = check.passed ? 'Info' : 'Critical';
|
||||
const decision: RuleDecision = check.passed ? 'Allow' : 'Hold';
|
||||
|
||||
return {
|
||||
ruleId: 'fx-contract-check',
|
||||
ruleName: 'FX Contract Validation',
|
||||
passed: check.passed,
|
||||
severity,
|
||||
decision,
|
||||
rationale: check.rationale,
|
||||
details: {
|
||||
fxContractId: check.fxContractId,
|
||||
contractExists: check.contractExists,
|
||||
contractActive: check.contractActive,
|
||||
contractType: check.contractType,
|
||||
contractAmount: check.contractAmount,
|
||||
contractRemainingAmount: check.contractRemainingAmount,
|
||||
transactionAmount: check.transactionAmount,
|
||||
amountWithinLimit: check.amountWithinLimit,
|
||||
},
|
||||
};
|
||||
}
|
||||
13
packages/rules-engine/src/index.ts
Normal file
13
packages/rules-engine/src/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @brazil-swift-ops/rules-engine
|
||||
*
|
||||
* Brazil regulatory rules engine for cross-border payments
|
||||
*/
|
||||
|
||||
export * from './config';
|
||||
export * from './threshold';
|
||||
export * from './documentation';
|
||||
export * from './fx-contract';
|
||||
export * from './iof';
|
||||
export * from './aml';
|
||||
export * from './orchestrator';
|
||||
73
packages/rules-engine/src/iof.ts
Normal file
73
packages/rules-engine/src/iof.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import Decimal from 'decimal.js';
|
||||
import type {
|
||||
Transaction,
|
||||
IOFCalculationResult,
|
||||
RuleResult,
|
||||
} from '@brazil-swift-ops/types';
|
||||
import { getDefaultConverter } from '@brazil-swift-ops/utils';
|
||||
import { getConfig } from './config';
|
||||
|
||||
export function calculateIOF(transaction: Transaction): IOFCalculationResult {
|
||||
const config = getConfig();
|
||||
const converter = getDefaultConverter();
|
||||
|
||||
const brlAmount = converter.convert(
|
||||
transaction.amount,
|
||||
transaction.currency,
|
||||
'BRL'
|
||||
);
|
||||
|
||||
const iofRate =
|
||||
transaction.direction === 'inbound'
|
||||
? config.iof.inboundRate
|
||||
: config.iof.outboundRate;
|
||||
|
||||
const brlDecimal = new Decimal(brlAmount);
|
||||
const rateDecimal = new Decimal(iofRate);
|
||||
const iofDecimal = brlDecimal.mul(rateDecimal);
|
||||
|
||||
const iofAmount = iofDecimal.toNumber();
|
||||
|
||||
let netAmount: number;
|
||||
if (transaction.direction === 'inbound') {
|
||||
netAmount = brlAmount - iofAmount;
|
||||
} else {
|
||||
netAmount = brlAmount + iofAmount;
|
||||
}
|
||||
|
||||
return {
|
||||
direction: transaction.direction,
|
||||
transactionAmount: transaction.amount,
|
||||
currency: transaction.currency,
|
||||
brlAmount,
|
||||
iofRate,
|
||||
iofAmount,
|
||||
netAmount,
|
||||
effectiveDate: config.iof.effectiveDate,
|
||||
rateVersion: config.iof.rateVersion,
|
||||
};
|
||||
}
|
||||
|
||||
export function createIOFRuleResult(
|
||||
calculation: IOFCalculationResult
|
||||
): RuleResult {
|
||||
return {
|
||||
ruleId: 'iof-calculation',
|
||||
ruleName: 'IOF Tax Calculation',
|
||||
passed: true,
|
||||
severity: 'Info',
|
||||
decision: 'Allow',
|
||||
rationale: `IOF calculated: ${calculation.iofAmount.toFixed(2)} BRL (${(calculation.iofRate * 100).toFixed(2)}% rate) for ${calculation.direction} transaction. Net amount: ${calculation.netAmount.toFixed(2)} BRL.`,
|
||||
details: {
|
||||
direction: calculation.direction,
|
||||
transactionAmount: calculation.transactionAmount,
|
||||
currency: calculation.currency,
|
||||
brlAmount: calculation.brlAmount,
|
||||
iofRate: calculation.iofRate,
|
||||
iofAmount: calculation.iofAmount,
|
||||
netAmount: calculation.netAmount,
|
||||
effectiveDate: calculation.effectiveDate,
|
||||
rateVersion: calculation.rateVersion,
|
||||
},
|
||||
};
|
||||
}
|
||||
95
packages/rules-engine/src/orchestrator.ts
Normal file
95
packages/rules-engine/src/orchestrator.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Rules engine orchestrator - coordinates all regulatory rule evaluations
|
||||
*/
|
||||
|
||||
import type {
|
||||
Transaction,
|
||||
BrazilRegulatoryResult,
|
||||
RuleDecision,
|
||||
RuleSeverity,
|
||||
} from '@brazil-swift-ops/types';
|
||||
import { getConfig } from './config';
|
||||
import { evaluateThreshold, createThresholdRuleResult } from './threshold';
|
||||
import {
|
||||
validateDocumentation,
|
||||
createDocumentationRuleResult,
|
||||
} from './documentation';
|
||||
import {
|
||||
validateFXContract,
|
||||
createFXContractRuleResult,
|
||||
getContractStore,
|
||||
} from './fx-contract';
|
||||
import { calculateIOF, createIOFRuleResult } from './iof';
|
||||
import {
|
||||
performAMLCheck,
|
||||
createAMLRuleResult,
|
||||
getHistoryStore,
|
||||
} from './aml';
|
||||
|
||||
/**
|
||||
* Evaluate all Brazil regulatory rules for a transaction
|
||||
*/
|
||||
export function evaluateTransaction(
|
||||
transaction: Transaction
|
||||
): BrazilRegulatoryResult {
|
||||
const config = getConfig();
|
||||
const timestamp = new Date();
|
||||
|
||||
// Run all rule checks
|
||||
const thresholdCheck = evaluateThreshold(transaction);
|
||||
const documentationCheck = validateDocumentation(transaction);
|
||||
const fxContract = getContractStore().get(transaction.fxContractId || '');
|
||||
const fxContractCheck = validateFXContract(transaction, fxContract);
|
||||
const iofCalculation = calculateIOF(transaction);
|
||||
const amlCheck = performAMLCheck(transaction);
|
||||
|
||||
// Create rule results
|
||||
const rules = [
|
||||
createThresholdRuleResult(thresholdCheck),
|
||||
createDocumentationRuleResult(documentationCheck),
|
||||
createFXContractRuleResult(fxContractCheck),
|
||||
createIOFRuleResult(iofCalculation),
|
||||
createAMLRuleResult(amlCheck),
|
||||
];
|
||||
|
||||
// Determine overall decision and severity
|
||||
const criticalRules = rules.filter((r) => r.severity === 'Critical' && !r.passed);
|
||||
const warningRules = rules.filter((r) => r.severity === 'Warning' && !r.passed);
|
||||
|
||||
let overallDecision: RuleDecision;
|
||||
let overallSeverity: RuleSeverity;
|
||||
|
||||
if (criticalRules.length > 0) {
|
||||
overallDecision = 'Hold';
|
||||
overallSeverity = 'Critical';
|
||||
} else if (warningRules.length > 0) {
|
||||
overallDecision = 'Escalate';
|
||||
overallSeverity = 'Warning';
|
||||
} else {
|
||||
overallDecision = 'Allow';
|
||||
overallSeverity = 'Info';
|
||||
}
|
||||
|
||||
return {
|
||||
transactionId: transaction.id,
|
||||
timestamp,
|
||||
ruleSetVersion: config.ruleSetVersion,
|
||||
overallDecision,
|
||||
overallSeverity,
|
||||
rules,
|
||||
thresholdCheck,
|
||||
documentationCheck,
|
||||
fxContractCheck,
|
||||
iofCalculation,
|
||||
amlCheck,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a batch of transactions
|
||||
*/
|
||||
export function evaluateBatch(
|
||||
transactions: Transaction[]
|
||||
): BrazilRegulatoryResult[] {
|
||||
return transactions.map((txn) => evaluateTransaction(txn));
|
||||
}
|
||||
Reference in New Issue
Block a user