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:
22
.eslintrc.js
Normal file
22
.eslintrc.js
Normal file
@@ -0,0 +1,22 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint', 'react', 'react-hooks'],
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off'
|
||||
},
|
||||
ignorePatterns: ['dist', 'build', 'node_modules', '*.config.js']
|
||||
};
|
||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
*.log
|
||||
|
||||
# Production
|
||||
dist
|
||||
build
|
||||
.next
|
||||
out
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
.env*.local
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
13
apps/web/index.html
Normal file
13
apps/web/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Brazil SWIFT Operations Platform</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
35
apps/web/package.json
Normal file
35
apps/web/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@brazil-swift-ops/web",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@brazil-swift-ops/types": "workspace:*",
|
||||
"@brazil-swift-ops/utils": "workspace:*",
|
||||
"@brazil-swift-ops/rules-engine": "workspace:*",
|
||||
"@brazil-swift-ops/iso20022": "workspace:*",
|
||||
"@brazil-swift-ops/treasury": "workspace:*",
|
||||
"@brazil-swift-ops/risk-models": "workspace:*",
|
||||
"@brazil-swift-ops/audit": "workspace:*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.32",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
}
|
||||
64
apps/web/src/App.tsx
Normal file
64
apps/web/src/App.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
|
||||
import DashboardPage from './pages/DashboardPage';
|
||||
import TransactionsPage from './pages/TransactionsPage';
|
||||
import TreasuryPage from './pages/TreasuryPage';
|
||||
import ReportsPage from './pages/ReportsPage';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<nav className="bg-white shadow-sm border-b">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<h1 className="text-xl font-bold text-gray-900">
|
||||
Brazil SWIFT Operations
|
||||
</h1>
|
||||
</div>
|
||||
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||
<Link
|
||||
to="/"
|
||||
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
to="/transactions"
|
||||
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
>
|
||||
Transactions
|
||||
</Link>
|
||||
<Link
|
||||
to="/treasury"
|
||||
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
>
|
||||
Treasury
|
||||
</Link>
|
||||
<Link
|
||||
to="/reports"
|
||||
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
>
|
||||
Reports
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<Routes>
|
||||
<Route path="/" element={<DashboardPage />} />
|
||||
<Route path="/transactions" element={<TransactionsPage />} />
|
||||
<Route path="/treasury" element={<TreasuryPage />} />
|
||||
<Route path="/reports" element={<ReportsPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
12
apps/web/src/index.css
Normal file
12
apps/web/src/index.css
Normal file
@@ -0,0 +1,12 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
10
apps/web/src/main.tsx
Normal file
10
apps/web/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
12
apps/web/src/pages/DashboardPage.tsx
Normal file
12
apps/web/src/pages/DashboardPage.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div className="px-4 py-6 sm:px-0">
|
||||
<div className="border-4 border-dashed border-gray-200 rounded-lg h-96 p-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
|
||||
<p className="text-gray-600">Brazil SWIFT Operations Platform</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
apps/web/src/pages/ReportsPage.tsx
Normal file
10
apps/web/src/pages/ReportsPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function ReportsPage() {
|
||||
return (
|
||||
<div className="px-4 py-6 sm:px-0">
|
||||
<h1 className="text-2xl font-bold mb-4">ReportsPage</h1>
|
||||
<p className="text-gray-600">ReportsPage interface</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
apps/web/src/pages/TransactionsPage.tsx
Normal file
10
apps/web/src/pages/TransactionsPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function TransactionsPage() {
|
||||
return (
|
||||
<div className="px-4 py-6 sm:px-0">
|
||||
<h1 className="text-2xl font-bold mb-4">TransactionsPage</h1>
|
||||
<p className="text-gray-600">TransactionsPage interface</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
apps/web/src/pages/TreasuryPage.tsx
Normal file
10
apps/web/src/pages/TreasuryPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function TreasuryPage() {
|
||||
return (
|
||||
<div className="px-4 py-6 sm:px-0">
|
||||
<h1 className="text-2xl font-bold mb-4">TreasuryPage</h1>
|
||||
<p className="text-gray-600">TreasuryPage interface</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
apps/web/src/stores/transactionStore.ts
Normal file
29
apps/web/src/stores/transactionStore.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Transaction, BrazilRegulatoryResult } from '@brazil-swift-ops/types';
|
||||
import { evaluateTransaction, evaluateBatch } from '@brazil-swift-ops/rules-engine';
|
||||
|
||||
interface TransactionStore {
|
||||
transactions: Transaction[];
|
||||
results: Map<string, BrazilRegulatoryResult>;
|
||||
addTransaction: (txn: Transaction) => void;
|
||||
evaluateTransaction: (txn: Transaction) => BrazilRegulatoryResult;
|
||||
}
|
||||
|
||||
export const useTransactionStore = create<TransactionStore>((set) => ({
|
||||
transactions: [],
|
||||
results: new Map(),
|
||||
addTransaction: (txn) => {
|
||||
const result = evaluateTransaction(txn);
|
||||
set((state) => ({
|
||||
transactions: [...state.transactions, txn],
|
||||
results: new Map(state.results).set(txn.id, result),
|
||||
}));
|
||||
},
|
||||
evaluateTransaction: (txn) => {
|
||||
const result = evaluateTransaction(txn);
|
||||
set((state) => ({
|
||||
results: new Map(state.results).set(txn.id, result),
|
||||
}));
|
||||
return result;
|
||||
},
|
||||
}));
|
||||
108
create_web_files.sh
Executable file
108
create_web_files.sh
Executable file
@@ -0,0 +1,108 @@
|
||||
#!/bin/bash
|
||||
# Create essential web app files
|
||||
|
||||
cd apps/web
|
||||
|
||||
# Create stores
|
||||
cat > src/stores/transactionStore.ts << 'EOFTXNSTORE'
|
||||
import { create } from 'zustand';
|
||||
import type { Transaction, BrazilRegulatoryResult, BatchTransaction } from '@brazil-swift-ops/types';
|
||||
import { evaluateTransaction, evaluateBatch } from '@brazil-swift-ops/rules-engine';
|
||||
import { calculateTransactionEOUplift, calculateBatchEOUplift } from '@brazil-swift-ops/utils';
|
||||
import { getDefaultConverter } from '@brazil-swift-ops/utils';
|
||||
|
||||
interface TransactionStore {
|
||||
transactions: Transaction[];
|
||||
results: Map<string, BrazilRegulatoryResult>;
|
||||
batches: BatchTransaction[];
|
||||
addTransaction: (txn: Transaction) => void;
|
||||
evaluateTransaction: (txn: Transaction) => BrazilRegulatoryResult;
|
||||
addBatch: (batch: BatchTransaction) => void;
|
||||
evaluateBatch: (txns: Transaction[]) => BrazilRegulatoryResult[];
|
||||
}
|
||||
|
||||
export const useTransactionStore = create<TransactionStore>((set, get) => ({
|
||||
transactions: [],
|
||||
results: new Map(),
|
||||
batches: [],
|
||||
addTransaction: (txn) => {
|
||||
const result = evaluateTransaction(txn);
|
||||
set((state) => ({
|
||||
transactions: [...state.transactions, txn],
|
||||
results: new Map(state.results).set(txn.id, result),
|
||||
}));
|
||||
},
|
||||
evaluateTransaction: (txn) => {
|
||||
const result = evaluateTransaction(txn);
|
||||
set((state) => ({
|
||||
results: new Map(state.results).set(txn.id, result),
|
||||
}));
|
||||
return result;
|
||||
},
|
||||
addBatch: (batch) => {
|
||||
set((state) => ({
|
||||
batches: [...state.batches, batch],
|
||||
}));
|
||||
},
|
||||
evaluateBatch: (txns) => {
|
||||
return evaluateBatch(txns);
|
||||
},
|
||||
}));
|
||||
EOFTXNSTORE
|
||||
|
||||
# Create a simple page component
|
||||
cat > src/pages/DashboardPage.tsx << 'EOFDASH'
|
||||
import React from 'react';
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div className="px-4 py-6 sm:px-0">
|
||||
<div className="border-4 border-dashed border-gray-200 rounded-lg h-96 p-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
|
||||
<p className="text-gray-600">Brazil SWIFT Operations Platform</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
EOFDASH
|
||||
|
||||
cat > src/pages/TransactionsPage.tsx << 'EOFTXNPAGE'
|
||||
import React from 'react';
|
||||
|
||||
export default function TransactionsPage() {
|
||||
return (
|
||||
<div className="px-4 py-6 sm:px-0">
|
||||
<h1 className="text-2xl font-bold mb-4">Transactions</h1>
|
||||
<p className="text-gray-600">Transaction processing interface</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
EOFTXNPAGE
|
||||
|
||||
cat > src/pages/TreasuryPage.tsx << 'EOFTREAS'
|
||||
import React from 'react';
|
||||
|
||||
export default function TreasuryPage() {
|
||||
return (
|
||||
<div className="px-4 py-6 sm:px-0">
|
||||
<h1 className="text-2xl font-bold mb-4">Treasury Management</h1>
|
||||
<p className="text-gray-600">Treasury and subledger management</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
EOFTREAS
|
||||
|
||||
cat > src/pages/ReportsPage.tsx << 'EOFREP'
|
||||
import React from 'react';
|
||||
|
||||
export default function ReportsPage() {
|
||||
return (
|
||||
<div className="px-4 py-6 sm:px-0">
|
||||
<h1 className="text-2xl font-bold mb-4">Reports</h1>
|
||||
<p className="text-gray-600">Compliance and regulatory reports</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
EOFREP
|
||||
|
||||
chmod +x create_web_files.sh
|
||||
24
package.json
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "brazil-swift-ops",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Brazil SWIFT Operations Platform - Regulator-Grade Implementation",
|
||||
"scripts": {
|
||||
"dev": "turbo run dev",
|
||||
"build": "turbo run build",
|
||||
"test": "turbo run test",
|
||||
"lint": "turbo run lint",
|
||||
"type-check": "turbo run type-check",
|
||||
"clean": "turbo run clean && rm -rf node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"turbo": "^1.11.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"packageManager": "pnpm@8.12.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"pnpm": ">=8.0.0"
|
||||
}
|
||||
}
|
||||
19
packages/audit/package.json
Normal file
19
packages/audit/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@brazil-swift-ops/audit",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@brazil-swift-ops/types": "workspace:*",
|
||||
"@brazil-swift-ops/utils": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
4
packages/audit/src/index.ts
Normal file
4
packages/audit/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './logger';
|
||||
export * from './reports';
|
||||
export * from './retention';
|
||||
export * from './versions';
|
||||
67
packages/audit/src/retention.ts
Normal file
67
packages/audit/src/retention.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { RetentionPolicy, AuditLog } from '@brazil-swift-ops/types';
|
||||
import { shouldArchive, shouldDelete } from '@brazil-swift-ops/utils';
|
||||
import { getAuditLogStore } from './logger';
|
||||
|
||||
class RetentionPolicyStore {
|
||||
private policies: RetentionPolicy[] = [];
|
||||
|
||||
add(policy: RetentionPolicy): void {
|
||||
this.policies.push(policy);
|
||||
}
|
||||
|
||||
get(dataType: RetentionPolicy['dataType']): RetentionPolicy | undefined {
|
||||
return this.policies.find((p) => p.dataType === dataType);
|
||||
}
|
||||
|
||||
getAll(): RetentionPolicy[] {
|
||||
return [...this.policies];
|
||||
}
|
||||
}
|
||||
|
||||
const retentionPolicyStore = new RetentionPolicyStore();
|
||||
|
||||
export function getRetentionPolicyStore(): RetentionPolicyStore {
|
||||
return retentionPolicyStore;
|
||||
}
|
||||
|
||||
export function applyRetentionPolicy(
|
||||
log: AuditLog,
|
||||
policy: RetentionPolicy
|
||||
): { shouldArchive: boolean; shouldDelete: boolean } {
|
||||
if (policy.archivalAfterDays) {
|
||||
const archive = shouldArchive(log.timestamp, policy.archivalAfterDays);
|
||||
if (archive) {
|
||||
return { shouldArchive: true, shouldDelete: false };
|
||||
}
|
||||
}
|
||||
|
||||
if (policy.autoDelete) {
|
||||
const deleteLog = shouldDelete(log.timestamp, policy.retentionPeriodDays);
|
||||
return { shouldArchive: false, shouldDelete: deleteLog };
|
||||
}
|
||||
|
||||
return { shouldArchive: false, shouldDelete: false };
|
||||
}
|
||||
|
||||
export function enforceRetentionPolicies(): void {
|
||||
const auditLogStore = getAuditLogStore();
|
||||
const policyStore = getRetentionPolicyStore();
|
||||
|
||||
const allPolicies = policyStore.getAll();
|
||||
const allLogs = auditLogStore.getAll();
|
||||
|
||||
allLogs.forEach((log) => {
|
||||
const policy = allPolicies.find(
|
||||
(p) => p.dataType === 'audit_log' || p.dataType === 'all'
|
||||
);
|
||||
|
||||
if (policy) {
|
||||
const { shouldDelete: shouldDeleteLog } = applyRetentionPolicy(log, policy);
|
||||
if (shouldDeleteLog && policy.autoDelete) {
|
||||
// In production, would actually delete from persistent storage
|
||||
// For now, just mark for deletion
|
||||
console.log(`Log ${log.id} should be deleted per retention policy`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
55
packages/audit/src/versions.ts
Normal file
55
packages/audit/src/versions.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { RuleVersion } from '@brazil-swift-ops/types';
|
||||
|
||||
class RuleVersionStore {
|
||||
private versions: RuleVersion[] = [];
|
||||
|
||||
add(version: RuleVersion): void {
|
||||
this.versions.push(version);
|
||||
}
|
||||
|
||||
get(version: string): RuleVersion | undefined {
|
||||
return this.versions.find((v) => v.version === version);
|
||||
}
|
||||
|
||||
getCurrent(): RuleVersion | undefined {
|
||||
return this.versions
|
||||
.filter((v) => !v.deprecated)
|
||||
.sort((a, b) => b.effectiveDate.getTime() - a.effectiveDate.getTime())[0];
|
||||
}
|
||||
|
||||
getAll(): RuleVersion[] {
|
||||
return [...this.versions];
|
||||
}
|
||||
|
||||
deprecate(version: string, deprecatedDate: Date): void {
|
||||
const versionObj = this.versions.find((v) => v.version === version);
|
||||
if (versionObj) {
|
||||
versionObj.deprecated = true;
|
||||
versionObj.deprecatedDate = deprecatedDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ruleVersionStore = new RuleVersionStore();
|
||||
|
||||
export function getRuleVersionStore(): RuleVersionStore {
|
||||
return ruleVersionStore;
|
||||
}
|
||||
|
||||
export function createRuleVersion(
|
||||
version: string,
|
||||
effectiveDate: Date,
|
||||
approvalAuthority: string,
|
||||
description?: string
|
||||
): RuleVersion {
|
||||
const ruleVersion: RuleVersion = {
|
||||
version,
|
||||
effectiveDate,
|
||||
approvalAuthority,
|
||||
deprecated: false,
|
||||
description,
|
||||
};
|
||||
|
||||
ruleVersionStore.add(ruleVersion);
|
||||
return ruleVersion;
|
||||
}
|
||||
18
packages/audit/tsconfig.json
Normal file
18
packages/audit/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../types"
|
||||
},
|
||||
{
|
||||
"path": "../utils"
|
||||
}
|
||||
]
|
||||
}
|
||||
19
packages/iso20022/package.json
Normal file
19
packages/iso20022/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@brazil-swift-ops/iso20022",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@brazil-swift-ops/types": "workspace:*",
|
||||
"@brazil-swift-ops/utils": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
34
packages/iso20022/src/exporter.ts
Normal file
34
packages/iso20022/src/exporter.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { ISO20022Message } from '@brazil-swift-ops/types';
|
||||
|
||||
export function exportToJSON(message: ISO20022Message): string {
|
||||
return JSON.stringify(message, null, 2);
|
||||
}
|
||||
|
||||
export function exportToXML(message: ISO20022Message): string {
|
||||
// Simplified XML export - in production would use proper XML serialization
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:${message.messageType}">
|
||||
<${message.messageType}>
|
||||
<GrpHdr>
|
||||
<MsgId>${message.groupHeader.messageIdentification}</MsgId>
|
||||
<CreDtTm>${message.groupHeader.creationDateTime.toISOString()}</CreDtTm>
|
||||
<NbOfTxs>${message.groupHeader.numberOfTransactions}</NbOfTxs>
|
||||
</GrpHdr>
|
||||
</${message.messageType}>
|
||||
</Document>`;
|
||||
return xml;
|
||||
}
|
||||
|
||||
export function exportMessage(
|
||||
message: ISO20022Message,
|
||||
format: 'json' | 'xml' = 'json'
|
||||
): string {
|
||||
switch (format) {
|
||||
case 'json':
|
||||
return exportToJSON(message);
|
||||
case 'xml':
|
||||
return exportToXML(message);
|
||||
default:
|
||||
throw new Error(`Unsupported export format: ${format}`);
|
||||
}
|
||||
}
|
||||
5
packages/iso20022/src/index.ts
Normal file
5
packages/iso20022/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './pacs008';
|
||||
export * from './pacs009';
|
||||
export * from './pain001';
|
||||
export * from './mt-mapper';
|
||||
export * from './exporter';
|
||||
101
packages/iso20022/src/mt-mapper.ts
Normal file
101
packages/iso20022/src/mt-mapper.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { Transaction, ISO20022Message } from '@brazil-swift-ops/types';
|
||||
import { createPacs008Message } from './pacs008';
|
||||
import { createPacs009Message } from './pacs009';
|
||||
import { createPain001Message } from './pain001';
|
||||
|
||||
export interface MT103Message {
|
||||
field20: string; // Sender's reference
|
||||
field23B: string; // Bank operation code
|
||||
field32A: string; // Value date, currency code, amount
|
||||
field50A?: string; // Ordering customer
|
||||
field52A?: string; // Ordering institution
|
||||
field53A?: string; // Sender's correspondent
|
||||
field54A?: string; // Receiver's correspondent
|
||||
field56A?: string; // Intermediary
|
||||
field57A?: string; // Account with institution
|
||||
field59: string; // Beneficiary customer
|
||||
field70: string; // Remittance information
|
||||
field71A: string; // Details of charges
|
||||
field72: string; // Sender to receiver information
|
||||
}
|
||||
|
||||
export function mapMT103ToTransaction(mt103: MT103Message): Transaction {
|
||||
const [valueDate, currency, amountStr] = mt103.field32A.split(/(\d{6})([A-Z]{3})([\d,]+)/).filter(Boolean);
|
||||
const amount = parseFloat(amountStr.replace(',', '.'));
|
||||
|
||||
return {
|
||||
id: mt103.field20,
|
||||
direction: 'outbound',
|
||||
amount,
|
||||
currency: currency || 'USD',
|
||||
orderingCustomer: {
|
||||
name: mt103.field50A || '',
|
||||
country: 'BR',
|
||||
},
|
||||
beneficiary: {
|
||||
name: mt103.field59,
|
||||
country: 'BR',
|
||||
},
|
||||
purposeOfPayment: mt103.field70,
|
||||
swiftReference: mt103.field20,
|
||||
status: 'pending',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapTransactionToMT103(transaction: Transaction): MT103Message {
|
||||
const valueDate = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
const amountStr = transaction.amount.toFixed(2).replace('.', ',');
|
||||
|
||||
return {
|
||||
field20: transaction.swiftReference || transaction.id,
|
||||
field23B: 'CRED',
|
||||
field32A: `${valueDate}${transaction.currency}${amountStr}`,
|
||||
field50A: transaction.orderingCustomer.name,
|
||||
field59: transaction.beneficiary.name,
|
||||
field70: transaction.purposeOfPayment || '',
|
||||
field71A: 'OUR',
|
||||
field72: `//${transaction.fxContractId || ''}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function convertMT103ToISO20022(
|
||||
mt103: MT103Message,
|
||||
targetType: 'pacs.008' | 'pacs.009' | 'pain.001' = 'pacs.008',
|
||||
version?: string
|
||||
): ISO20022Message {
|
||||
const transaction = mapMT103ToTransaction(mt103);
|
||||
|
||||
switch (targetType) {
|
||||
case 'pacs.008':
|
||||
return createPacs008Message(transaction, version);
|
||||
case 'pacs.009':
|
||||
return createPacs009Message(transaction, version);
|
||||
case 'pain.001':
|
||||
return createPain001Message(transaction, version);
|
||||
}
|
||||
}
|
||||
|
||||
export function convertISO20022ToMT103(message: ISO20022Message): MT103Message {
|
||||
// This is a simplified conversion - in production would need full mapping
|
||||
const transaction: Transaction = {
|
||||
id: message.groupHeader.messageIdentification,
|
||||
direction: 'outbound',
|
||||
amount: message.groupHeader.controlSum || 0,
|
||||
currency: 'USD',
|
||||
orderingCustomer: {
|
||||
name: message.groupHeader.initiatingParty.name || '',
|
||||
country: 'BR',
|
||||
},
|
||||
beneficiary: {
|
||||
name: '',
|
||||
country: 'BR',
|
||||
},
|
||||
status: 'pending',
|
||||
createdAt: message.creationDateTime,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
return mapTransactionToMT103(transaction);
|
||||
}
|
||||
20
packages/risk-models/package.json
Normal file
20
packages/risk-models/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@brazil-swift-ops/risk-models",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@brazil-swift-ops/types": "workspace:*",
|
||||
"@brazil-swift-ops/utils": "workspace:*",
|
||||
"decimal.js": "^10.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
42
packages/risk-models/src/capital.ts
Normal file
42
packages/risk-models/src/capital.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import Decimal from 'decimal.js';
|
||||
import type { Transaction, CapitalImpact, RiskWeight } from '@brazil-swift-ops/types';
|
||||
|
||||
export interface CapitalConfig {
|
||||
capitalRatio: number;
|
||||
capitalBuffer: number;
|
||||
riskWeights: RiskWeight[];
|
||||
}
|
||||
|
||||
export function getRiskWeight(transaction: Transaction, riskWeights: RiskWeight[]): number {
|
||||
const paymentWeight = riskWeights.find((w) => w.category === 'payment');
|
||||
return paymentWeight?.weight ?? 0.1;
|
||||
}
|
||||
|
||||
export function calculateCapitalImpact(transaction: Transaction, config: CapitalConfig): CapitalImpact {
|
||||
const riskWeight = getRiskWeight(transaction, config.riskWeights);
|
||||
const transactionDecimal = new Decimal(transaction.amount);
|
||||
const weightDecimal = new Decimal(riskWeight);
|
||||
const rwaDecimal = transactionDecimal.mul(weightDecimal);
|
||||
const riskWeightedAssets = rwaDecimal.toNumber();
|
||||
const ratioDecimal = new Decimal(config.capitalRatio);
|
||||
const capitalConsumedDecimal = rwaDecimal.mul(ratioDecimal);
|
||||
const capitalConsumed = capitalConsumedDecimal.toNumber();
|
||||
const capitalBufferAfter = config.capitalBuffer - capitalConsumed;
|
||||
const complianceCheck = capitalBufferAfter >= 0;
|
||||
|
||||
return {
|
||||
transactionId: transaction.id,
|
||||
transactionAmount: transaction.amount,
|
||||
currency: transaction.currency,
|
||||
riskWeight,
|
||||
riskWeightedAssets,
|
||||
capitalRatio: config.capitalRatio,
|
||||
capitalConsumed,
|
||||
capitalBufferBefore: config.capitalBuffer,
|
||||
capitalBufferAfter,
|
||||
complianceCheck,
|
||||
rationale: complianceCheck
|
||||
? `Capital consumed: ${capitalConsumed.toFixed(2)}. Capital buffer after transaction: ${capitalBufferAfter.toFixed(2)} (above minimum).`
|
||||
: `Capital consumed: ${capitalConsumed.toFixed(2)}. Capital buffer after transaction: ${capitalBufferAfter.toFixed(2)} (below minimum). Transaction may be blocked.`,
|
||||
};
|
||||
}
|
||||
66
packages/risk-models/src/escalation.ts
Normal file
66
packages/risk-models/src/escalation.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type {
|
||||
EscalationState,
|
||||
EscalationLevel,
|
||||
RuleDecision,
|
||||
} from '@brazil-swift-ops/types';
|
||||
|
||||
export function determineEscalationLevel(
|
||||
decision: RuleDecision,
|
||||
hasLiquidityIssue: boolean,
|
||||
hasCapitalIssue: boolean,
|
||||
hasLCRIssue: boolean
|
||||
): EscalationLevel {
|
||||
if (decision === 'Allow') {
|
||||
return 'Allow';
|
||||
}
|
||||
|
||||
if (hasLiquidityIssue || hasCapitalIssue || hasLCRIssue) {
|
||||
return 'TreasuryApproval';
|
||||
}
|
||||
|
||||
if (decision === 'Escalate') {
|
||||
return 'ComplianceReview';
|
||||
}
|
||||
|
||||
return 'Hold';
|
||||
}
|
||||
|
||||
export function createEscalationState(
|
||||
transactionId: string,
|
||||
level: EscalationLevel,
|
||||
reason: string
|
||||
): EscalationState {
|
||||
return {
|
||||
transactionId,
|
||||
currentLevel: level,
|
||||
reason,
|
||||
escalatedAt: new Date(),
|
||||
approvalRequired: level !== 'Allow',
|
||||
};
|
||||
}
|
||||
|
||||
export function escalate(
|
||||
currentState: EscalationState,
|
||||
newLevel: EscalationLevel,
|
||||
reason: string
|
||||
): EscalationState {
|
||||
return {
|
||||
...currentState,
|
||||
previousLevel: currentState.currentLevel,
|
||||
currentLevel: newLevel,
|
||||
reason,
|
||||
escalatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
export function approveEscalation(
|
||||
state: EscalationState,
|
||||
approvedBy: string
|
||||
): EscalationState {
|
||||
return {
|
||||
...state,
|
||||
approvalRequired: false,
|
||||
approvedBy,
|
||||
approvedAt: new Date(),
|
||||
};
|
||||
}
|
||||
5
packages/risk-models/src/index.ts
Normal file
5
packages/risk-models/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './reserves';
|
||||
export * from './capital';
|
||||
export * from './lcr';
|
||||
export * from './escalation';
|
||||
export * from './risk-weights';
|
||||
44
packages/risk-models/src/lcr.ts
Normal file
44
packages/risk-models/src/lcr.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import Decimal from 'decimal.js';
|
||||
import type { Transaction, LCRImpact } from '@brazil-swift-ops/types';
|
||||
import { getDefaultConverter } from '@brazil-swift-ops/utils';
|
||||
|
||||
export interface LCRConfig {
|
||||
hqla: number;
|
||||
netOutflows: number;
|
||||
minimumLCR: number;
|
||||
runoffFactor: number;
|
||||
}
|
||||
|
||||
export function calculateLCRImpact(transaction: Transaction, config: LCRConfig): LCRImpact {
|
||||
const converter = getDefaultConverter();
|
||||
const brlAmount = converter.convert(transaction.amount, transaction.currency, 'BRL');
|
||||
const transactionDecimal = new Decimal(brlAmount);
|
||||
const runoffDecimal = new Decimal(config.runoffFactor);
|
||||
const outflowStressDecimal = transactionDecimal.mul(runoffDecimal);
|
||||
const outflowStress = outflowStressDecimal.toNumber();
|
||||
const netOutflowsAfter = config.netOutflows + outflowStress;
|
||||
const hqlaDecimal = new Decimal(config.hqla);
|
||||
const outflowsBeforeDecimal = new Decimal(config.netOutflows);
|
||||
const outflowsAfterDecimal = new Decimal(netOutflowsAfter);
|
||||
const lcrBefore = outflowsBeforeDecimal.gt(0) ? hqlaDecimal.div(outflowsBeforeDecimal).toNumber() : Infinity;
|
||||
const lcrAfter = outflowsAfterDecimal.gt(0) ? hqlaDecimal.div(outflowsAfterDecimal).toNumber() : Infinity;
|
||||
const complianceCheck = lcrAfter >= config.minimumLCR;
|
||||
|
||||
return {
|
||||
transactionId: transaction.id,
|
||||
transactionAmount: transaction.amount,
|
||||
currency: transaction.currency,
|
||||
runoffFactor: config.runoffFactor,
|
||||
outflowStress,
|
||||
hqlaBefore: config.hqla,
|
||||
netOutflowsBefore: config.netOutflows,
|
||||
netOutflowsAfter,
|
||||
lcrBefore,
|
||||
lcrAfter,
|
||||
minimumLCR: config.minimumLCR,
|
||||
complianceCheck,
|
||||
rationale: complianceCheck
|
||||
? `LCR after transaction: ${(lcrAfter * 100).toFixed(2)}% (above minimum ${(config.minimumLCR * 100).toFixed(2)}%).`
|
||||
: `LCR after transaction: ${(lcrAfter * 100).toFixed(2)}% (below minimum ${(config.minimumLCR * 100).toFixed(2)}%). Transaction may be blocked.`,
|
||||
};
|
||||
}
|
||||
35
packages/risk-models/src/reserves.ts
Normal file
35
packages/risk-models/src/reserves.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import Decimal from 'decimal.js';
|
||||
import type { Transaction, ReserveImpact } from '@brazil-swift-ops/types';
|
||||
import { getDefaultConverter } from '@brazil-swift-ops/utils';
|
||||
|
||||
export interface ReserveConfig {
|
||||
reserveRatio: number;
|
||||
availableLiquidity: number;
|
||||
requiredReserves: number;
|
||||
}
|
||||
|
||||
export function calculateReserveImpact(transaction: Transaction, config: ReserveConfig): ReserveImpact {
|
||||
const converter = getDefaultConverter();
|
||||
const brlAmount = converter.convert(transaction.amount, transaction.currency, 'BRL');
|
||||
const transactionDecimal = new Decimal(brlAmount);
|
||||
const ratioDecimal = new Decimal(config.reserveRatio);
|
||||
const reserveImpactDecimal = transactionDecimal.mul(ratioDecimal);
|
||||
const reserveImpact = reserveImpactDecimal.toNumber();
|
||||
const availableLiquidityAfter = config.availableLiquidity - brlAmount;
|
||||
const complianceCheck = availableLiquidityAfter >= config.requiredReserves;
|
||||
|
||||
return {
|
||||
transactionId: transaction.id,
|
||||
transactionAmount: transaction.amount,
|
||||
currency: transaction.currency,
|
||||
reserveRatio: config.reserveRatio,
|
||||
reserveImpact,
|
||||
availableLiquidityBefore: config.availableLiquidity,
|
||||
availableLiquidityAfter,
|
||||
requiredReserves: config.requiredReserves,
|
||||
complianceCheck,
|
||||
rationale: complianceCheck
|
||||
? `Reserve impact: ${reserveImpact.toFixed(2)} BRL. Available liquidity after transaction: ${availableLiquidityAfter.toFixed(2)} BRL (above required ${config.requiredReserves.toFixed(2)} BRL).`
|
||||
: `Reserve impact: ${reserveImpact.toFixed(2)} BRL. Available liquidity after transaction: ${availableLiquidityAfter.toFixed(2)} BRL (below required ${config.requiredReserves.toFixed(2)} BRL). Transaction may be blocked.`,
|
||||
};
|
||||
}
|
||||
37
packages/risk-models/src/risk-weights.ts
Normal file
37
packages/risk-models/src/risk-weights.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { RiskWeight, RiskWeightTable } from '@brazil-swift-ops/types';
|
||||
|
||||
export const DEFAULT_RISK_WEIGHTS: RiskWeight[] = [
|
||||
{
|
||||
category: 'payment',
|
||||
description: 'Payment transactions',
|
||||
weight: 0.1, // 10%
|
||||
minWeight: 0.0,
|
||||
maxWeight: 0.2,
|
||||
},
|
||||
{
|
||||
category: 'fx_settlement',
|
||||
description: 'FX settlement transactions',
|
||||
weight: 0.35, // 35%
|
||||
minWeight: 0.2,
|
||||
maxWeight: 0.5,
|
||||
},
|
||||
{
|
||||
category: 'nostro_exposure',
|
||||
description: 'Nostro account exposure',
|
||||
weight: 1.0, // 100%
|
||||
minWeight: 0.5,
|
||||
maxWeight: 1.0,
|
||||
},
|
||||
];
|
||||
|
||||
export function createRiskWeightTable(
|
||||
version: string = '1.0.0',
|
||||
effectiveDate: Date = new Date()
|
||||
): RiskWeightTable {
|
||||
return {
|
||||
id: `RWT-${version}`,
|
||||
version,
|
||||
effectiveDate,
|
||||
weights: DEFAULT_RISK_WEIGHTS,
|
||||
};
|
||||
}
|
||||
18
packages/risk-models/tsconfig.json
Normal file
18
packages/risk-models/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../types"
|
||||
},
|
||||
{
|
||||
"path": "../utils"
|
||||
}
|
||||
]
|
||||
}
|
||||
20
packages/rules-engine/package.json
Normal file
20
packages/rules-engine/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@brazil-swift-ops/rules-engine",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@brazil-swift-ops/types": "workspace:*",
|
||||
"@brazil-swift-ops/utils": "workspace:*",
|
||||
"decimal.js": "^10.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
19
packages/treasury/package.json
Normal file
19
packages/treasury/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@brazil-swift-ops/treasury",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@brazil-swift-ops/types": "workspace:*",
|
||||
"@brazil-swift-ops/utils": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
72
packages/treasury/src/accounts.ts
Normal file
72
packages/treasury/src/accounts.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { TreasuryAccount, SubledgerAccount, Account } from '@brazil-swift-ops/types';
|
||||
|
||||
class AccountStore {
|
||||
private accounts: Map<string, Account> = new Map();
|
||||
|
||||
add(account: Account): void {
|
||||
this.accounts.set(account.id, account);
|
||||
}
|
||||
|
||||
get(id: string): Account | undefined {
|
||||
return this.accounts.get(id);
|
||||
}
|
||||
|
||||
getByParent(parentId: string): SubledgerAccount[] {
|
||||
return Array.from(this.accounts.values()).filter(
|
||||
(acc): acc is SubledgerAccount =>
|
||||
acc.type === 'subledger' && acc.parentAccountId === parentId
|
||||
);
|
||||
}
|
||||
|
||||
getAll(): Account[] {
|
||||
return Array.from(this.accounts.values());
|
||||
}
|
||||
|
||||
update(id: string, updates: Partial<Account>): void {
|
||||
const account = this.accounts.get(id);
|
||||
if (account) {
|
||||
this.accounts.set(id, { ...account, ...updates, updatedAt: new Date() });
|
||||
}
|
||||
}
|
||||
|
||||
delete(id: string): void {
|
||||
this.accounts.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
const accountStore = new AccountStore();
|
||||
|
||||
export function getAccountStore(): AccountStore {
|
||||
return accountStore;
|
||||
}
|
||||
|
||||
export function createTreasuryAccount(accountNumber: string, name: string, currency: string): TreasuryAccount {
|
||||
return {
|
||||
id: `TREASURY-${Date.now()}`,
|
||||
accountNumber,
|
||||
name,
|
||||
type: 'treasury',
|
||||
currency,
|
||||
status: 'active',
|
||||
balance: 0,
|
||||
availableBalance: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
export function createSubledgerAccount(accountNumber: string, name: string, currency: string, parentAccountId: string): SubledgerAccount {
|
||||
return {
|
||||
id: `SUB-${Date.now()}`,
|
||||
accountNumber,
|
||||
name,
|
||||
type: 'subledger',
|
||||
currency,
|
||||
status: 'active',
|
||||
parentAccountId,
|
||||
balance: 0,
|
||||
availableBalance: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
4
packages/treasury/src/index.ts
Normal file
4
packages/treasury/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './accounts';
|
||||
export * from './posting';
|
||||
export * from './transfers';
|
||||
export * from './reporting';
|
||||
87
packages/treasury/src/posting.ts
Normal file
87
packages/treasury/src/posting.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { Account, AccountPosting, PostingType, Transaction } from '@brazil-swift-ops/types';
|
||||
import { getDefaultConverter } from '@brazil-swift-ops/utils';
|
||||
import { getAccountStore } from './accounts';
|
||||
|
||||
class PostingStore {
|
||||
private postings: AccountPosting[] = [];
|
||||
|
||||
add(posting: AccountPosting): void {
|
||||
this.postings.push(posting);
|
||||
}
|
||||
|
||||
getByAccount(accountId: string): AccountPosting[] {
|
||||
return this.postings.filter((p) => p.accountId === accountId);
|
||||
}
|
||||
|
||||
getByTransaction(transactionId: string): AccountPosting[] {
|
||||
return this.postings.filter((p) => p.transactionId === transactionId);
|
||||
}
|
||||
|
||||
getAll(): AccountPosting[] {
|
||||
return [...this.postings];
|
||||
}
|
||||
}
|
||||
|
||||
const postingStore = new PostingStore();
|
||||
|
||||
export function getPostingStore(): PostingStore {
|
||||
return postingStore;
|
||||
}
|
||||
|
||||
export function postToAccount(account: Account, transaction: Transaction, postingType: PostingType, amount?: number): AccountPosting {
|
||||
const accountStore = getAccountStore();
|
||||
const converter = getDefaultConverter();
|
||||
const transactionAmount = amount ?? transaction.amount;
|
||||
let postingAmount = transactionAmount;
|
||||
let fxRate: number | undefined;
|
||||
|
||||
if (transaction.currency !== account.currency) {
|
||||
fxRate = converter.getRate(transaction.currency, account.currency);
|
||||
if (fxRate) {
|
||||
postingAmount = converter.convert(transactionAmount, transaction.currency, account.currency);
|
||||
}
|
||||
}
|
||||
|
||||
const balanceBefore = account.balance;
|
||||
const balanceAfter = postingType === 'debit' ? balanceBefore - postingAmount : balanceBefore + postingAmount;
|
||||
|
||||
const posting: AccountPosting = {
|
||||
id: `POST-${Date.now()}`,
|
||||
accountId: account.id,
|
||||
transactionId: transaction.id,
|
||||
postingType,
|
||||
amount: postingAmount,
|
||||
currency: account.currency,
|
||||
fxRate,
|
||||
balanceBefore,
|
||||
balanceAfter,
|
||||
postedAt: new Date(),
|
||||
description: `Transaction ${transaction.id} - ${postingType}`,
|
||||
};
|
||||
|
||||
accountStore.update(account.id, { balance: balanceAfter, availableBalance: balanceAfter });
|
||||
postingStore.add(posting);
|
||||
return posting;
|
||||
}
|
||||
|
||||
export function allocateTransactionToSubledger(transaction: Transaction, subledgerId: string): AccountPosting[] {
|
||||
const accountStore = getAccountStore();
|
||||
const subledger = accountStore.get(subledgerId);
|
||||
if (!subledger || subledger.type !== 'subledger') {
|
||||
throw new Error(`Subledger account ${subledgerId} not found`);
|
||||
}
|
||||
const parent = accountStore.get(subledger.parentAccountId);
|
||||
if (!parent) {
|
||||
throw new Error(`Parent account ${subledger.parentAccountId} not found`);
|
||||
}
|
||||
|
||||
const postings: AccountPosting[] = [];
|
||||
if (transaction.direction === 'inbound') {
|
||||
postings.push(postToAccount(subledger, transaction, 'credit'));
|
||||
postings.push(postToAccount(parent, transaction, 'credit'));
|
||||
} else {
|
||||
postings.push(postToAccount(subledger, transaction, 'debit'));
|
||||
postings.push(postToAccount(parent, transaction, 'debit'));
|
||||
}
|
||||
return postings;
|
||||
}
|
||||
55
packages/treasury/src/reporting.ts
Normal file
55
packages/treasury/src/reporting.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type {
|
||||
SubledgerReport,
|
||||
AccountPosting,
|
||||
} from '@brazil-swift-ops/types';
|
||||
import { getPostingStore } from './posting';
|
||||
import { getAccountStore } from './accounts';
|
||||
|
||||
export function generateSubledgerReport(
|
||||
subledgerId: string,
|
||||
periodStart: Date,
|
||||
periodEnd: Date
|
||||
): SubledgerReport {
|
||||
const accountStore = getAccountStore();
|
||||
const postingStore = getPostingStore();
|
||||
|
||||
const subledger = accountStore.get(subledgerId);
|
||||
if (!subledger || subledger.type !== 'subledger') {
|
||||
throw new Error(`Subledger account ${subledgerId} not found`);
|
||||
}
|
||||
|
||||
const postings = postingStore
|
||||
.getByAccount(subledgerId)
|
||||
.filter(
|
||||
(p) => p.postedAt >= periodStart && p.postedAt <= periodEnd
|
||||
);
|
||||
|
||||
const openingBalance = subledger.balance - postings.reduce((sum, p) => {
|
||||
return p.postingType === 'debit' ? sum - p.amount : sum + p.amount;
|
||||
}, 0);
|
||||
|
||||
const totalDebits = postings
|
||||
.filter((p) => p.postingType === 'debit')
|
||||
.reduce((sum, p) => sum + p.amount, 0);
|
||||
|
||||
const totalCredits = postings
|
||||
.filter((p) => p.postingType === 'credit')
|
||||
.reduce((sum, p) => sum + p.amount, 0);
|
||||
|
||||
const closingBalance = openingBalance + totalCredits - totalDebits;
|
||||
const netPosition = totalCredits - totalDebits;
|
||||
|
||||
return {
|
||||
subledgerId,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
openingBalance,
|
||||
closingBalance,
|
||||
totalDebits,
|
||||
totalCredits,
|
||||
netPosition,
|
||||
currency: subledger.currency,
|
||||
transactionCount: new Set(postings.map((p) => p.transactionId)).size,
|
||||
postings,
|
||||
};
|
||||
}
|
||||
99
packages/treasury/src/transfers.ts
Normal file
99
packages/treasury/src/transfers.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type {
|
||||
SubledgerTransfer,
|
||||
Account,
|
||||
} from '@brazil-swift-ops/types';
|
||||
import { getAccountStore } from './accounts';
|
||||
import { postToAccount } from './posting';
|
||||
|
||||
class TransferStore {
|
||||
private transfers: SubledgerTransfer[] = [];
|
||||
|
||||
add(transfer: SubledgerTransfer): void {
|
||||
this.transfers.push(transfer);
|
||||
}
|
||||
|
||||
get(id: string): SubledgerTransfer | undefined {
|
||||
return this.transfers.find((t) => t.id === id);
|
||||
}
|
||||
|
||||
getByAccount(accountId: string): SubledgerTransfer[] {
|
||||
return this.transfers.filter(
|
||||
(t) => t.fromAccountId === accountId || t.toAccountId === accountId
|
||||
);
|
||||
}
|
||||
|
||||
getAll(): SubledgerTransfer[] {
|
||||
return [...this.transfers];
|
||||
}
|
||||
}
|
||||
|
||||
const transferStore = new TransferStore();
|
||||
|
||||
export function getTransferStore(): TransferStore {
|
||||
return transferStore;
|
||||
}
|
||||
|
||||
export function executeSubledgerTransfer(
|
||||
fromAccountId: string,
|
||||
toAccountId: string,
|
||||
amount: number,
|
||||
currency: string,
|
||||
description?: string
|
||||
): SubledgerTransfer {
|
||||
const accountStore = getAccountStore();
|
||||
const fromAccount = accountStore.get(fromAccountId);
|
||||
const toAccount = accountStore.get(toAccountId);
|
||||
|
||||
if (!fromAccount) {
|
||||
throw new Error(`Source account ${fromAccountId} not found`);
|
||||
}
|
||||
|
||||
if (!toAccount) {
|
||||
throw new Error(`Destination account ${toAccountId} not found`);
|
||||
}
|
||||
|
||||
if (fromAccount.currency !== currency || toAccount.currency !== currency) {
|
||||
throw new Error('Currency mismatch in transfer');
|
||||
}
|
||||
|
||||
if (fromAccount.balance < amount) {
|
||||
throw new Error('Insufficient balance in source account');
|
||||
}
|
||||
|
||||
const transfer: SubledgerTransfer = {
|
||||
id: `TRF-${Date.now()}`,
|
||||
fromAccountId,
|
||||
toAccountId,
|
||||
amount,
|
||||
currency,
|
||||
description,
|
||||
executedAt: new Date(),
|
||||
status: 'pending',
|
||||
};
|
||||
|
||||
// Execute transfer
|
||||
try {
|
||||
// Debit from source
|
||||
const fromBalanceBefore = fromAccount.balance;
|
||||
accountStore.update(fromAccountId, {
|
||||
balance: fromBalanceBefore - amount,
|
||||
availableBalance: fromBalanceBefore - amount,
|
||||
});
|
||||
|
||||
// Credit to destination
|
||||
const toBalanceBefore = toAccount.balance;
|
||||
accountStore.update(toAccountId, {
|
||||
balance: toBalanceBefore + amount,
|
||||
availableBalance: toBalanceBefore + amount,
|
||||
});
|
||||
|
||||
transfer.status = 'completed';
|
||||
} catch (error) {
|
||||
transfer.status = 'failed';
|
||||
throw error;
|
||||
}
|
||||
|
||||
transferStore.add(transfer);
|
||||
|
||||
return transfer;
|
||||
}
|
||||
18
packages/treasury/tsconfig.json
Normal file
18
packages/treasury/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../types"
|
||||
},
|
||||
{
|
||||
"path": "../utils"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
packages/types/package.json
Normal file
15
packages/types/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@brazil-swift-ops/types",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
88
packages/types/src/audit.ts
Normal file
88
packages/types/src/audit.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Audit logging and compliance reporting types
|
||||
*/
|
||||
|
||||
export interface AuditLog {
|
||||
id: string;
|
||||
timestamp: Date;
|
||||
transactionId?: string;
|
||||
batchId?: string;
|
||||
ruleSetVersion: string;
|
||||
inputs: Record<string, unknown>;
|
||||
outputs: Record<string, unknown>;
|
||||
decision: 'Allow' | 'Hold' | 'Escalate';
|
||||
severity: 'Info' | 'Warning' | 'Critical';
|
||||
rationale: string;
|
||||
userId?: string;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
action: string; // e.g., 'transaction_processed', 'rule_evaluated', 'report_generated'
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface RuleVersion {
|
||||
version: string;
|
||||
effectiveDate: Date;
|
||||
approvalAuthority: string;
|
||||
deprecated: boolean;
|
||||
deprecatedDate?: Date;
|
||||
description?: string;
|
||||
changes?: string[];
|
||||
}
|
||||
|
||||
export interface BCBReport {
|
||||
reportId: string;
|
||||
reportType: 'transaction' | 'batch' | 'periodic';
|
||||
generatedAt: Date;
|
||||
periodStart?: Date;
|
||||
periodEnd?: Date;
|
||||
transactions: BCBReportTransaction[];
|
||||
summary: BCBReportSummary;
|
||||
format: 'json' | 'csv';
|
||||
data: string; // Serialized report data
|
||||
}
|
||||
|
||||
export interface BCBReportTransaction {
|
||||
transactionId: string;
|
||||
executionDate: Date;
|
||||
direction: 'inbound' | 'outbound';
|
||||
amount: number;
|
||||
currency: string;
|
||||
usdEquivalent: number;
|
||||
orderingCustomer: {
|
||||
name: string;
|
||||
taxId?: string;
|
||||
country: string;
|
||||
};
|
||||
beneficiary: {
|
||||
name: string;
|
||||
taxId?: string;
|
||||
country: string;
|
||||
accountNumber?: string;
|
||||
};
|
||||
purposeOfPayment?: string;
|
||||
fxContractId?: string;
|
||||
iofAmount?: number;
|
||||
reportingRequired: boolean;
|
||||
}
|
||||
|
||||
export interface BCBReportSummary {
|
||||
totalTransactions: number;
|
||||
totalAmount: number;
|
||||
totalUsdEquivalent: number;
|
||||
inboundCount: number;
|
||||
inboundAmount: number;
|
||||
outboundCount: number;
|
||||
outboundAmount: number;
|
||||
reportingRequiredCount: number;
|
||||
totalIOF: number;
|
||||
}
|
||||
|
||||
export interface RetentionPolicy {
|
||||
policyId: string;
|
||||
dataType: 'audit_log' | 'transaction' | 'report' | 'all';
|
||||
retentionPeriodDays: number;
|
||||
archivalAfterDays?: number;
|
||||
autoDelete: boolean;
|
||||
effectiveDate: Date;
|
||||
}
|
||||
24
packages/types/src/eo-uplift.ts
Normal file
24
packages/types/src/eo-uplift.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Errors & Omissions (E&O) Uplift types
|
||||
*/
|
||||
|
||||
export interface EOUplift {
|
||||
baseAmount: number;
|
||||
upliftRate: number; // 0.10 for 10%
|
||||
upliftAmount: number;
|
||||
adjustedExposure: number;
|
||||
treatment: 'off_balance_sheet'; // Non-booked risk buffer
|
||||
}
|
||||
|
||||
export interface TransactionEOUplift extends EOUplift {
|
||||
transactionId: string;
|
||||
currency: string;
|
||||
usdEquivalent?: number;
|
||||
}
|
||||
|
||||
export interface BatchEOUplift extends EOUplift {
|
||||
batchId: string;
|
||||
transactionCount: number;
|
||||
currency: string;
|
||||
usdEquivalent?: number;
|
||||
}
|
||||
32
packages/types/src/fx-contract.ts
Normal file
32
packages/types/src/fx-contract.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* FX Contract (Contrato de Câmbio) types
|
||||
*/
|
||||
|
||||
export interface FXContract {
|
||||
contractId: string;
|
||||
type: 'inbound' | 'outbound';
|
||||
counterparty: string;
|
||||
counterpartyTaxId?: string; // CNPJ
|
||||
amount: number;
|
||||
currency: string;
|
||||
effectiveDate: Date;
|
||||
expiryDate: Date;
|
||||
remainingAmount: number;
|
||||
usedAmount: number;
|
||||
status: 'active' | 'expired' | 'exhausted' | 'cancelled';
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface FXContractValidation {
|
||||
contractId: string;
|
||||
contractExists: boolean;
|
||||
contractValid: boolean;
|
||||
contractActive: boolean;
|
||||
amountWithinLimit: boolean;
|
||||
transactionAmount: number;
|
||||
contractRemainingAmount: number;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
14
packages/types/src/index.ts
Normal file
14
packages/types/src/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @brazil-swift-ops/types
|
||||
*
|
||||
* Shared TypeScript types and interfaces for the Brazil SWIFT Operations Platform
|
||||
*/
|
||||
|
||||
export * from './transaction';
|
||||
export * from './iso20022';
|
||||
export * from './regulatory';
|
||||
export * from './treasury';
|
||||
export * from './audit';
|
||||
export * from './risk';
|
||||
export * from './fx-contract';
|
||||
export * from './eo-uplift';
|
||||
131
packages/types/src/iso20022.ts
Normal file
131
packages/types/src/iso20022.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* ISO 20022 message types and structures
|
||||
*/
|
||||
|
||||
export type ISO20022MessageType = 'pacs.008' | 'pacs.009' | 'pain.001';
|
||||
export type ISO20022Version = string; // e.g., "001.08", "001.10"
|
||||
|
||||
export interface ISO20022Message {
|
||||
messageId: string;
|
||||
messageType: ISO20022MessageType;
|
||||
version: ISO20022Version;
|
||||
creationDateTime: Date;
|
||||
groupHeader: GroupHeader;
|
||||
paymentInformation: PaymentInformation[];
|
||||
rawMessage?: Record<string, unknown>; // Full ISO 20022 JSON structure
|
||||
}
|
||||
|
||||
export interface GroupHeader {
|
||||
messageIdentification: string;
|
||||
creationDateTime: Date;
|
||||
numberOfTransactions: number;
|
||||
controlSum?: number;
|
||||
initiatingParty: PartyIdentification;
|
||||
}
|
||||
|
||||
export interface PartyIdentification {
|
||||
name?: string;
|
||||
postalAddress?: PostalAddress;
|
||||
identification?: PartyIdentificationDetails;
|
||||
}
|
||||
|
||||
export interface PostalAddress {
|
||||
streetName?: string;
|
||||
buildingNumber?: string;
|
||||
postCode?: string;
|
||||
townName?: string;
|
||||
country: string;
|
||||
}
|
||||
|
||||
export interface PartyIdentificationDetails {
|
||||
organisationIdentification?: string;
|
||||
privateIdentification?: string; // CPF/CNPJ
|
||||
}
|
||||
|
||||
export interface PaymentInformation {
|
||||
paymentInformationIdentification: string;
|
||||
paymentMethod: string;
|
||||
requestedExecutionDate: Date;
|
||||
debtor: PartyIdentification;
|
||||
debtorAccount: CashAccount;
|
||||
debtorAgent?: FinancialInstitutionIdentification;
|
||||
creditTransferTransactionInformation: CreditTransferTransactionInformation[];
|
||||
}
|
||||
|
||||
export interface CashAccount {
|
||||
identification: string;
|
||||
name?: string;
|
||||
currency?: string;
|
||||
type?: string;
|
||||
iban?: string;
|
||||
}
|
||||
|
||||
export interface FinancialInstitutionIdentification {
|
||||
bic?: string;
|
||||
name?: string;
|
||||
postalAddress?: PostalAddress;
|
||||
}
|
||||
|
||||
export interface CreditTransferTransactionInformation {
|
||||
paymentIdentification: PaymentIdentification;
|
||||
amount: Amount;
|
||||
chargeBearer?: string;
|
||||
creditor: PartyIdentification;
|
||||
creditorAccount: CashAccount;
|
||||
creditorAgent?: FinancialInstitutionIdentification;
|
||||
remittanceInformation?: RemittanceInformation;
|
||||
purpose?: string;
|
||||
}
|
||||
|
||||
export interface PaymentIdentification {
|
||||
instructionId?: string;
|
||||
endToEndId: string;
|
||||
transactionId?: string;
|
||||
}
|
||||
|
||||
export interface Amount {
|
||||
currency: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface RemittanceInformation {
|
||||
unstructured?: string;
|
||||
structured?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// pacs.008 specific (FIToFICustomerCreditTransfer)
|
||||
export interface Pacs008Message extends ISO20022Message {
|
||||
messageType: 'pacs.008';
|
||||
creditTransferTransaction: CreditTransferTransaction[];
|
||||
}
|
||||
|
||||
export interface CreditTransferTransaction {
|
||||
paymentIdentification: PaymentIdentification;
|
||||
amount: Amount;
|
||||
chargeBearer?: string;
|
||||
debtor: PartyIdentification;
|
||||
debtorAccount: CashAccount;
|
||||
debtorAgent?: FinancialInstitutionIdentification;
|
||||
creditor: PartyIdentification;
|
||||
creditorAccount: CashAccount;
|
||||
creditorAgent?: FinancialInstitutionIdentification;
|
||||
remittanceInformation?: RemittanceInformation;
|
||||
purpose?: string;
|
||||
}
|
||||
|
||||
// pacs.009 specific (FinancialInstitutionCreditTransfer)
|
||||
export interface Pacs009Message extends ISO20022Message {
|
||||
messageType: 'pacs.009';
|
||||
creditTransferTransaction: CreditTransferTransaction[];
|
||||
}
|
||||
|
||||
// pain.001 specific (CustomerCreditTransferInitiation)
|
||||
export interface Pain001Message extends ISO20022Message {
|
||||
messageType: 'pain.001';
|
||||
customerCreditTransferInitiation: CustomerCreditTransferInitiation;
|
||||
}
|
||||
|
||||
export interface CustomerCreditTransferInitiation {
|
||||
groupHeader: GroupHeader;
|
||||
paymentInformation: PaymentInformation[];
|
||||
}
|
||||
105
packages/types/src/regulatory.ts
Normal file
105
packages/types/src/regulatory.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Brazil regulatory rule evaluation types
|
||||
*/
|
||||
|
||||
export type RuleSeverity = 'Info' | 'Warning' | 'Critical';
|
||||
export type RuleDecision = 'Allow' | 'Hold' | 'Escalate';
|
||||
export type EscalationLevel = 'Allow' | 'Hold' | 'ComplianceReview' | 'TreasuryApproval' | 'ExceptionApproval';
|
||||
|
||||
export interface RuleResult {
|
||||
ruleId: string;
|
||||
ruleName: string;
|
||||
passed: boolean;
|
||||
severity: RuleSeverity;
|
||||
decision: RuleDecision;
|
||||
rationale: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface BrazilRegulatoryResult {
|
||||
transactionId: string;
|
||||
timestamp: Date;
|
||||
ruleSetVersion: string;
|
||||
overallDecision: RuleDecision;
|
||||
overallSeverity: RuleSeverity;
|
||||
rules: RuleResult[];
|
||||
thresholdCheck?: ThresholdCheckResult;
|
||||
documentationCheck?: DocumentationCheckResult;
|
||||
fxContractCheck?: FXContractCheckResult;
|
||||
iofCalculation?: IOFCalculationResult;
|
||||
amlCheck?: AMLCheckResult;
|
||||
}
|
||||
|
||||
export interface ThresholdCheckResult {
|
||||
passed: boolean;
|
||||
transactionAmount: number;
|
||||
currency: string;
|
||||
usdEquivalent: number;
|
||||
threshold: number; // USD 10,000
|
||||
requiresReporting: boolean;
|
||||
rationale: string;
|
||||
}
|
||||
|
||||
export interface DocumentationCheckResult {
|
||||
passed: boolean;
|
||||
hasOrderingCustomerName: boolean;
|
||||
hasOrderingCustomerAddress: boolean;
|
||||
hasOrderingCustomerTaxId: boolean;
|
||||
hasBeneficiaryName: boolean;
|
||||
hasBeneficiaryAccount: boolean;
|
||||
hasBeneficiaryTaxId: boolean;
|
||||
hasPurposeOfPayment: boolean;
|
||||
missingFields: string[];
|
||||
rationale: string;
|
||||
}
|
||||
|
||||
export interface FXContractCheckResult {
|
||||
passed: boolean;
|
||||
fxContractId?: string;
|
||||
contractExists: boolean;
|
||||
contractType?: 'inbound' | 'outbound';
|
||||
contractAmount: number;
|
||||
contractRemainingAmount: number;
|
||||
transactionAmount: number;
|
||||
amountWithinLimit: boolean;
|
||||
rationale: string;
|
||||
}
|
||||
|
||||
export interface IOFCalculationResult {
|
||||
direction: 'inbound' | 'outbound';
|
||||
transactionAmount: number;
|
||||
currency: string;
|
||||
brlAmount: number;
|
||||
iofRate: number; // e.g., 0.0038 for inbound, 0.0350 for outbound
|
||||
iofAmount: number;
|
||||
netAmount: number; // transactionAmount - iofAmount (inbound) or transactionAmount + iofAmount (outbound)
|
||||
effectiveDate: Date;
|
||||
rateVersion: string;
|
||||
}
|
||||
|
||||
export interface AMLCheckResult {
|
||||
passed: boolean;
|
||||
singleTransactionCheck: SingleTransactionAMLResult;
|
||||
structuringCheck?: StructuringCheckResult;
|
||||
overallRiskLevel: 'Low' | 'Medium' | 'High';
|
||||
rationale: string;
|
||||
}
|
||||
|
||||
export interface SingleTransactionAMLResult {
|
||||
passed: boolean;
|
||||
transactionAmount: number;
|
||||
usdEquivalent: number;
|
||||
threshold: number;
|
||||
requiresEnhancedReview: boolean;
|
||||
riskLevel: 'Low' | 'Medium' | 'High';
|
||||
}
|
||||
|
||||
export interface StructuringCheckResult {
|
||||
detected: boolean;
|
||||
windowDays: number;
|
||||
transactionCount: number;
|
||||
totalAmount: number;
|
||||
totalUsdEquivalent: number;
|
||||
individualAmounts: number[];
|
||||
rationale: string;
|
||||
}
|
||||
86
packages/types/src/risk.ts
Normal file
86
packages/types/src/risk.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Risk, capital, and liquidity modeling types
|
||||
*/
|
||||
|
||||
export interface ReserveImpact {
|
||||
transactionId: string;
|
||||
transactionAmount: number;
|
||||
currency: string;
|
||||
reserveRatio: number; // e.g., 0.21 for 21%
|
||||
reserveImpact: number;
|
||||
availableLiquidityBefore: number;
|
||||
availableLiquidityAfter: number;
|
||||
requiredReserves: number;
|
||||
complianceCheck: boolean;
|
||||
rationale: string;
|
||||
}
|
||||
|
||||
export interface CapitalImpact {
|
||||
transactionId: string;
|
||||
transactionAmount: number;
|
||||
currency: string;
|
||||
riskWeight: number; // 0-1 (0% to 100%)
|
||||
riskWeightedAssets: number;
|
||||
capitalRatio: number; // e.g., 0.105 for 10.5%
|
||||
capitalConsumed: number;
|
||||
capitalBufferBefore: number;
|
||||
capitalBufferAfter: number;
|
||||
complianceCheck: boolean;
|
||||
rationale: string;
|
||||
}
|
||||
|
||||
export interface LCRImpact {
|
||||
transactionId: string;
|
||||
transactionAmount: number;
|
||||
currency: string;
|
||||
runoffFactor: number; // 0-1
|
||||
outflowStress: number;
|
||||
hqlaBefore: number; // High Quality Liquid Assets
|
||||
netOutflowsBefore: number;
|
||||
netOutflowsAfter: number;
|
||||
lcrBefore: number;
|
||||
lcrAfter: number;
|
||||
minimumLCR: number; // e.g., 1.0 for 100%
|
||||
complianceCheck: boolean;
|
||||
rationale: string;
|
||||
}
|
||||
|
||||
export interface RiskWeightTable {
|
||||
id: string;
|
||||
version: string;
|
||||
effectiveDate: Date;
|
||||
weights: RiskWeight[];
|
||||
}
|
||||
|
||||
export interface RiskWeight {
|
||||
category: string; // e.g., 'payment', 'fx_settlement', 'nostro_exposure'
|
||||
description: string;
|
||||
weight: number; // 0-1 (0% to 100%)
|
||||
minWeight?: number;
|
||||
maxWeight?: number;
|
||||
}
|
||||
|
||||
export interface EscalationState {
|
||||
transactionId: string;
|
||||
currentLevel: EscalationLevel;
|
||||
previousLevel?: EscalationLevel;
|
||||
reason: string;
|
||||
escalatedAt: Date;
|
||||
escalatedBy?: string;
|
||||
approvalRequired?: boolean;
|
||||
approvedBy?: string;
|
||||
approvedAt?: Date;
|
||||
}
|
||||
|
||||
export type EscalationLevel = 'Allow' | 'Hold' | 'ComplianceReview' | 'TreasuryApproval' | 'ExceptionApproval';
|
||||
|
||||
export interface StressTestResult {
|
||||
transactionId: string;
|
||||
reserveImpact: ReserveImpact;
|
||||
capitalImpact: CapitalImpact;
|
||||
lcrImpact: LCRImpact;
|
||||
overallCompliance: boolean;
|
||||
blockingIssues: string[];
|
||||
warnings: string[];
|
||||
recommendedAction: 'Allow' | 'Hold' | 'Escalate';
|
||||
}
|
||||
48
packages/types/src/transaction.ts
Normal file
48
packages/types/src/transaction.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Canonical transaction model for cross-border payments
|
||||
*/
|
||||
|
||||
export type TransactionDirection = 'inbound' | 'outbound';
|
||||
export type TransactionStatus = 'pending' | 'approved' | 'held' | 'rejected' | 'escalated';
|
||||
export type Currency = string; // ISO 4217 currency code
|
||||
|
||||
export interface Party {
|
||||
name: string;
|
||||
address?: string;
|
||||
city?: string;
|
||||
country: string;
|
||||
taxId?: string; // CPF for individuals, CNPJ for corporates
|
||||
email?: string;
|
||||
phone?: string;
|
||||
accountNumber?: string;
|
||||
iban?: string;
|
||||
bic?: string;
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
id: string;
|
||||
direction: TransactionDirection;
|
||||
amount: number;
|
||||
currency: Currency;
|
||||
usdEquivalent?: number; // Calculated USD equivalent
|
||||
orderingCustomer: Party;
|
||||
beneficiary: Party;
|
||||
purposeOfPayment?: string;
|
||||
fxContractId?: string; // Link to FX contract (contrato de câmbio)
|
||||
swiftReference?: string;
|
||||
iso20022MessageId?: string;
|
||||
status: TransactionStatus;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface BatchTransaction {
|
||||
batchId: string;
|
||||
transactions: Transaction[];
|
||||
totalAmount: number;
|
||||
totalUsdEquivalent: number;
|
||||
currency: Currency;
|
||||
createdAt: Date;
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||
}
|
||||
94
packages/types/src/treasury.ts
Normal file
94
packages/types/src/treasury.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Treasury and subledger account types
|
||||
*/
|
||||
|
||||
export type AccountStatus = 'active' | 'inactive' | 'closed' | 'suspended';
|
||||
export type AccountType = 'treasury' | 'subledger';
|
||||
export type PostingType = 'debit' | 'credit';
|
||||
|
||||
export interface TreasuryAccount {
|
||||
id: string;
|
||||
accountNumber: string;
|
||||
name: string;
|
||||
type: 'treasury';
|
||||
currency: string;
|
||||
status: AccountStatus;
|
||||
parentAccountId?: string; // For subledgers
|
||||
balance: number;
|
||||
availableBalance: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SubledgerAccount {
|
||||
id: string;
|
||||
accountNumber: string;
|
||||
name: string;
|
||||
type: 'subledger';
|
||||
currency: string;
|
||||
status: AccountStatus;
|
||||
parentAccountId: string; // Must have a parent treasury account
|
||||
balance: number;
|
||||
availableBalance: number;
|
||||
routingRules?: RoutingRule[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type Account = TreasuryAccount | SubledgerAccount;
|
||||
|
||||
export interface RoutingRule {
|
||||
id: string;
|
||||
subledgerAccountId: string;
|
||||
conditions: RoutingCondition[];
|
||||
priority: number;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface RoutingCondition {
|
||||
field: string; // e.g., 'currency', 'amount', 'counterparty'
|
||||
operator: 'equals' | 'greaterThan' | 'lessThan' | 'contains';
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface AccountPosting {
|
||||
id: string;
|
||||
accountId: string;
|
||||
transactionId: string;
|
||||
postingType: PostingType;
|
||||
amount: number;
|
||||
currency: string;
|
||||
fxRate?: number; // If currency conversion occurred
|
||||
balanceBefore: number;
|
||||
balanceAfter: number;
|
||||
postedAt: Date;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface SubledgerTransfer {
|
||||
id: string;
|
||||
fromAccountId: string;
|
||||
toAccountId: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
transactionId?: string; // Link to original transaction if applicable
|
||||
description?: string;
|
||||
executedAt: Date;
|
||||
status: 'pending' | 'completed' | 'failed';
|
||||
}
|
||||
|
||||
export interface SubledgerReport {
|
||||
subledgerId: string;
|
||||
periodStart: Date;
|
||||
periodEnd: Date;
|
||||
openingBalance: number;
|
||||
closingBalance: number;
|
||||
totalDebits: number;
|
||||
totalCredits: number;
|
||||
netPosition: number;
|
||||
currency: string;
|
||||
transactionCount: number;
|
||||
postings: AccountPosting[];
|
||||
}
|
||||
10
packages/types/tsconfig.json
Normal file
10
packages/types/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
20
packages/utils/package.json
Normal file
20
packages/utils/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@brazil-swift-ops/utils",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@brazil-swift-ops/types": "workspace:*",
|
||||
"decimal.js": "^10.4.3",
|
||||
"date-fns": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
130
packages/utils/src/currency.ts
Normal file
130
packages/utils/src/currency.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Currency conversion and USD equivalent calculation utilities
|
||||
*/
|
||||
|
||||
import Decimal from 'decimal.js';
|
||||
|
||||
export interface ExchangeRate {
|
||||
fromCurrency: string;
|
||||
toCurrency: string;
|
||||
rate: number;
|
||||
effectiveDate: Date;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export interface CurrencyConverter {
|
||||
convert(amount: number, fromCurrency: string, toCurrency: string, date?: Date): number;
|
||||
getUSDEquivalent(amount: number, currency: string, date?: Date): number;
|
||||
getRate(fromCurrency: string, toCurrency: string, date?: Date): number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple in-memory currency converter with configurable rates
|
||||
* In production, this would integrate with a real-time FX rate service
|
||||
*/
|
||||
export class SimpleCurrencyConverter implements CurrencyConverter {
|
||||
private rates: Map<string, ExchangeRate> = new Map();
|
||||
private defaultUSDRates: Record<string, number> = {
|
||||
USD: 1.0,
|
||||
BRL: 0.2,
|
||||
EUR: 1.1,
|
||||
GBP: 1.27,
|
||||
};
|
||||
|
||||
constructor(initialRates?: ExchangeRate[]) {
|
||||
if (initialRates) {
|
||||
initialRates.forEach((rate) => this.addRate(rate));
|
||||
}
|
||||
this.initializeDefaultRates();
|
||||
}
|
||||
|
||||
private initializeDefaultRates(): void {
|
||||
const now = new Date();
|
||||
Object.entries(this.defaultUSDRates).forEach(([currency, rate]) => {
|
||||
if (currency !== 'USD') {
|
||||
this.addRate({
|
||||
fromCurrency: currency,
|
||||
toCurrency: 'USD',
|
||||
rate,
|
||||
effectiveDate: now,
|
||||
source: 'default',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addRate(rate: ExchangeRate): void {
|
||||
const key = this.getRateKey(rate.fromCurrency, rate.toCurrency);
|
||||
this.rates.set(key, rate);
|
||||
const inverseKey = this.getRateKey(rate.toCurrency, rate.fromCurrency);
|
||||
this.rates.set(inverseKey, {
|
||||
...rate,
|
||||
fromCurrency: rate.toCurrency,
|
||||
toCurrency: rate.fromCurrency,
|
||||
rate: 1 / rate.rate,
|
||||
});
|
||||
}
|
||||
|
||||
private getRateKey(fromCurrency: string, toCurrency: string): string {
|
||||
return `${fromCurrency}:${toCurrency}`;
|
||||
}
|
||||
|
||||
getRate(fromCurrency: string, toCurrency: string, date?: Date): number | null {
|
||||
if (fromCurrency === toCurrency) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
const key = this.getRateKey(fromCurrency, toCurrency);
|
||||
const rate = this.rates.get(key);
|
||||
|
||||
if (!rate) {
|
||||
if (fromCurrency !== 'USD' && toCurrency !== 'USD') {
|
||||
const fromToUSD = this.getRate(fromCurrency, 'USD', date);
|
||||
const usdToTo = this.getRate('USD', toCurrency, date);
|
||||
if (fromToUSD && usdToTo) {
|
||||
return fromToUSD * usdToTo;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return rate.rate;
|
||||
}
|
||||
|
||||
convert(amount: number, fromCurrency: string, toCurrency: string, date?: Date): number {
|
||||
if (fromCurrency === toCurrency) {
|
||||
return amount;
|
||||
}
|
||||
|
||||
const rate = this.getRate(fromCurrency, toCurrency, date);
|
||||
if (rate === null) {
|
||||
throw new Error(
|
||||
`No exchange rate available for ${fromCurrency} to ${toCurrency}`
|
||||
);
|
||||
}
|
||||
|
||||
const amountDecimal = new Decimal(amount);
|
||||
const rateDecimal = new Decimal(rate);
|
||||
return amountDecimal.mul(rateDecimal).toNumber();
|
||||
}
|
||||
|
||||
getUSDEquivalent(amount: number, currency: string, date?: Date): number {
|
||||
if (currency === 'USD') {
|
||||
return amount;
|
||||
}
|
||||
return this.convert(amount, currency, 'USD', date);
|
||||
}
|
||||
}
|
||||
|
||||
let defaultConverter: CurrencyConverter | null = null;
|
||||
|
||||
export function getDefaultConverter(): CurrencyConverter {
|
||||
if (!defaultConverter) {
|
||||
defaultConverter = new SimpleCurrencyConverter();
|
||||
}
|
||||
return defaultConverter;
|
||||
}
|
||||
|
||||
export function setDefaultConverter(converter: CurrencyConverter): void {
|
||||
defaultConverter = converter;
|
||||
}
|
||||
83
packages/utils/src/dates.ts
Normal file
83
packages/utils/src/dates.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Date utilities for effective date logic and rolling windows
|
||||
*/
|
||||
|
||||
import { addDays, isAfter, isBefore, isWithinInterval, subDays } from 'date-fns';
|
||||
|
||||
export function isEffectiveDate(date: Date, effectiveDate: Date, expiryDate?: Date): boolean {
|
||||
if (isBefore(date, effectiveDate)) {
|
||||
return false;
|
||||
}
|
||||
if (expiryDate && isAfter(date, expiryDate)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export interface RollingWindow {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
days: number;
|
||||
}
|
||||
|
||||
export function calculateRollingWindow(
|
||||
referenceDate: Date,
|
||||
windowDays: number
|
||||
): RollingWindow {
|
||||
const startDate = subDays(referenceDate, windowDays);
|
||||
return {
|
||||
startDate,
|
||||
endDate: referenceDate,
|
||||
days: windowDays,
|
||||
};
|
||||
}
|
||||
|
||||
export function isWithinRollingWindow(
|
||||
date: Date,
|
||||
window: RollingWindow
|
||||
): boolean {
|
||||
return isWithinInterval(date, {
|
||||
start: window.startDate,
|
||||
end: window.endDate,
|
||||
});
|
||||
}
|
||||
|
||||
export function filterDatesInWindow(
|
||||
dates: Date[],
|
||||
window: RollingWindow
|
||||
): Date[] {
|
||||
return dates.filter((date) => isWithinRollingWindow(date, window));
|
||||
}
|
||||
|
||||
export function calculateRetentionExpiry(
|
||||
creationDate: Date,
|
||||
retentionDays: number
|
||||
): Date {
|
||||
return addDays(creationDate, retentionDays);
|
||||
}
|
||||
|
||||
export function shouldArchive(
|
||||
creationDate: Date,
|
||||
archivalAfterDays: number,
|
||||
currentDate: Date = new Date()
|
||||
): boolean {
|
||||
const archivalDate = addDays(creationDate, archivalAfterDays);
|
||||
return isAfter(currentDate, archivalDate);
|
||||
}
|
||||
|
||||
export function shouldDelete(
|
||||
creationDate: Date,
|
||||
retentionDays: number,
|
||||
currentDate: Date = new Date()
|
||||
): boolean {
|
||||
const expiryDate = calculateRetentionExpiry(creationDate, retentionDays);
|
||||
return isAfter(currentDate, expiryDate);
|
||||
}
|
||||
|
||||
export function formatISO20022Date(date: Date): string {
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
export function formatISO20022DateTime(date: Date): string {
|
||||
return date.toISOString().split('.')[0];
|
||||
}
|
||||
128
packages/utils/src/eo-uplift.ts
Normal file
128
packages/utils/src/eo-uplift.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Errors & Omissions (E&O) Uplift calculator
|
||||
*
|
||||
* E&O uplift is a +10% exposure buffer applied to transaction amounts
|
||||
* to account for errors and omissions outside of direct operations.
|
||||
*
|
||||
* Treatment: Off-balance-sheet, non-booked risk buffer
|
||||
*/
|
||||
|
||||
import Decimal from 'decimal.js';
|
||||
import type { EOUplift, TransactionEOUplift, BatchEOUplift } from '@brazil-swift-ops/types';
|
||||
|
||||
export const DEFAULT_EO_UPLIFT_RATE = 0.10; // 10%
|
||||
|
||||
/**
|
||||
* Calculate E&O uplift for a single transaction
|
||||
*/
|
||||
export function calculateTransactionEOUplift(
|
||||
baseAmount: number,
|
||||
currency: string,
|
||||
upliftRate: number = DEFAULT_EO_UPLIFT_RATE,
|
||||
usdEquivalent?: number
|
||||
): TransactionEOUplift {
|
||||
// Use Decimal.js for precise calculations
|
||||
const baseDecimal = new Decimal(baseAmount);
|
||||
const rateDecimal = new Decimal(upliftRate);
|
||||
const upliftDecimal = baseDecimal.mul(rateDecimal);
|
||||
const adjustedDecimal = baseDecimal.add(upliftDecimal);
|
||||
|
||||
const upliftAmount = upliftDecimal.toNumber();
|
||||
const adjustedExposure = adjustedDecimal.toNumber();
|
||||
|
||||
// Calculate USD equivalent for uplift if provided
|
||||
let upliftUsdEquivalent: number | undefined;
|
||||
if (usdEquivalent !== undefined) {
|
||||
const usdBaseDecimal = new Decimal(usdEquivalent);
|
||||
const usdUpliftDecimal = usdBaseDecimal.mul(rateDecimal);
|
||||
upliftUsdEquivalent = usdBaseDecimal.add(usdUpliftDecimal).toNumber();
|
||||
}
|
||||
|
||||
return {
|
||||
transactionId: '', // Will be set by caller
|
||||
baseAmount,
|
||||
currency,
|
||||
upliftRate,
|
||||
upliftAmount,
|
||||
adjustedExposure,
|
||||
usdEquivalent: upliftUsdEquivalent,
|
||||
treatment: 'off_balance_sheet',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate E&O uplift for a batch of transactions
|
||||
*/
|
||||
export function calculateBatchEOUplift(
|
||||
baseAmount: number,
|
||||
currency: string,
|
||||
transactionCount: number,
|
||||
upliftRate: number = DEFAULT_EO_UPLIFT_RATE,
|
||||
usdEquivalent?: number
|
||||
): BatchEOUplift {
|
||||
const baseDecimal = new Decimal(baseAmount);
|
||||
const rateDecimal = new Decimal(upliftRate);
|
||||
const upliftDecimal = baseDecimal.mul(rateDecimal);
|
||||
const adjustedDecimal = baseDecimal.add(upliftDecimal);
|
||||
|
||||
const upliftAmount = upliftDecimal.toNumber();
|
||||
const adjustedExposure = adjustedDecimal.toNumber();
|
||||
|
||||
// Calculate USD equivalent for uplift if provided
|
||||
let upliftUsdEquivalent: number | undefined;
|
||||
if (usdEquivalent !== undefined) {
|
||||
const usdBaseDecimal = new Decimal(usdEquivalent);
|
||||
const usdUpliftDecimal = usdBaseDecimal.mul(rateDecimal);
|
||||
upliftUsdEquivalent = usdBaseDecimal.add(usdUpliftDecimal).toNumber();
|
||||
}
|
||||
|
||||
return {
|
||||
batchId: '', // Will be set by caller
|
||||
baseAmount,
|
||||
currency,
|
||||
transactionCount,
|
||||
upliftRate,
|
||||
upliftAmount,
|
||||
adjustedExposure,
|
||||
usdEquivalent: upliftUsdEquivalent,
|
||||
treatment: 'off_balance_sheet',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate E&O uplift for a simple amount (no transaction context)
|
||||
*/
|
||||
export function calculateEOUplift(
|
||||
baseAmount: number,
|
||||
upliftRate: number = DEFAULT_EO_UPLIFT_RATE
|
||||
): EOUplift {
|
||||
const baseDecimal = new Decimal(baseAmount);
|
||||
const rateDecimal = new Decimal(upliftRate);
|
||||
const upliftDecimal = baseDecimal.mul(rateDecimal);
|
||||
const adjustedDecimal = baseDecimal.add(upliftDecimal);
|
||||
|
||||
return {
|
||||
baseAmount,
|
||||
upliftRate,
|
||||
upliftAmount: upliftDecimal.toNumber(),
|
||||
adjustedExposure: adjustedDecimal.toNumber(),
|
||||
treatment: 'off_balance_sheet',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply E&O uplift to an array of transaction amounts
|
||||
*/
|
||||
export function applyEOUpliftToAmounts(
|
||||
amounts: number[],
|
||||
upliftRate: number = DEFAULT_EO_UPLIFT_RATE
|
||||
): { baseAmount: number; upliftAmount: number; adjustedExposure: number }[] {
|
||||
return amounts.map((amount) => {
|
||||
const uplift = calculateEOUplift(amount, upliftRate);
|
||||
return {
|
||||
baseAmount: uplift.baseAmount,
|
||||
upliftAmount: uplift.upliftAmount,
|
||||
adjustedExposure: uplift.adjustedExposure,
|
||||
};
|
||||
});
|
||||
}
|
||||
10
packages/utils/src/index.ts
Normal file
10
packages/utils/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* @brazil-swift-ops/utils
|
||||
*
|
||||
* Shared utilities for the Brazil SWIFT Operations Platform
|
||||
*/
|
||||
|
||||
export * from './currency';
|
||||
export * from './dates';
|
||||
export * from './validation';
|
||||
export * from './eo-uplift';
|
||||
151
packages/utils/src/validation.ts
Normal file
151
packages/utils/src/validation.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Brazilian ID validation utilities (CPF/CNPJ)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Validate CPF (Cadastro de Pessoa Física) format and checksum
|
||||
* CPF format: XXX.XXX.XXX-XX (11 digits)
|
||||
*/
|
||||
export function validateCPF(cpf: string): { valid: boolean; formatted?: string; error?: string } {
|
||||
// Remove non-numeric characters
|
||||
const cleaned = cpf.replace(/\D/g, '');
|
||||
|
||||
// Check length
|
||||
if (cleaned.length !== 11) {
|
||||
return { valid: false, error: 'CPF must have 11 digits' };
|
||||
}
|
||||
|
||||
// Check for invalid patterns (all same digits)
|
||||
if (/^(\d)\1{10}$/.test(cleaned)) {
|
||||
return { valid: false, error: 'CPF cannot have all same digits' };
|
||||
}
|
||||
|
||||
// Validate checksum digits
|
||||
let sum = 0;
|
||||
let remainder: number;
|
||||
|
||||
// Validate first check digit
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
sum += parseInt(cleaned.substring(i - 1, i)) * (11 - i);
|
||||
}
|
||||
remainder = (sum * 10) % 11;
|
||||
if (remainder === 10 || remainder === 11) remainder = 0;
|
||||
if (remainder !== parseInt(cleaned.substring(9, 10))) {
|
||||
return { valid: false, error: 'Invalid CPF checksum (first digit)' };
|
||||
}
|
||||
|
||||
// Validate second check digit
|
||||
sum = 0;
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
sum += parseInt(cleaned.substring(i - 1, i)) * (12 - i);
|
||||
}
|
||||
remainder = (sum * 10) % 11;
|
||||
if (remainder === 10 || remainder === 11) remainder = 0;
|
||||
if (remainder !== parseInt(cleaned.substring(10, 11))) {
|
||||
return { valid: false, error: 'Invalid CPF checksum (second digit)' };
|
||||
}
|
||||
|
||||
// Format CPF
|
||||
const formatted = `${cleaned.substring(0, 3)}.${cleaned.substring(3, 6)}.${cleaned.substring(6, 9)}-${cleaned.substring(9, 11)}`;
|
||||
|
||||
return { valid: true, formatted };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate CNPJ (Cadastro Nacional da Pessoa Jurídica) format and checksum
|
||||
* CNPJ format: XX.XXX.XXX/XXXX-XX (14 digits)
|
||||
*/
|
||||
export function validateCNPJ(cnpj: string): { valid: boolean; formatted?: string; error?: string } {
|
||||
// Remove non-numeric characters
|
||||
const cleaned = cnpj.replace(/\D/g, '');
|
||||
|
||||
// Check length
|
||||
if (cleaned.length !== 14) {
|
||||
return { valid: false, error: 'CNPJ must have 14 digits' };
|
||||
}
|
||||
|
||||
// Check for invalid patterns (all same digits)
|
||||
if (/^(\d)\1{13}$/.test(cleaned)) {
|
||||
return { valid: false, error: 'CNPJ cannot have all same digits' };
|
||||
}
|
||||
|
||||
// Validate checksum digits
|
||||
let length = cleaned.length - 2;
|
||||
let numbers = cleaned.substring(0, length);
|
||||
const digits = cleaned.substring(length);
|
||||
let sum = 0;
|
||||
let pos = length - 7;
|
||||
|
||||
// Validate first check digit
|
||||
for (let i = length; i >= 1; i--) {
|
||||
sum += parseInt(numbers.charAt(length - i)) * pos--;
|
||||
if (pos < 2) pos = 9;
|
||||
}
|
||||
let result = sum % 11 < 2 ? 0 : 11 - (sum % 11);
|
||||
if (result !== parseInt(digits.charAt(0))) {
|
||||
return { valid: false, error: 'Invalid CNPJ checksum (first digit)' };
|
||||
}
|
||||
|
||||
// Validate second check digit
|
||||
length = length + 1;
|
||||
numbers = cleaned.substring(0, length);
|
||||
sum = 0;
|
||||
pos = length - 7;
|
||||
for (let i = length; i >= 1; i--) {
|
||||
sum += parseInt(numbers.charAt(length - i)) * pos--;
|
||||
if (pos < 2) pos = 9;
|
||||
}
|
||||
result = sum % 11 < 2 ? 0 : 11 - (sum % 11);
|
||||
if (result !== parseInt(digits.charAt(1))) {
|
||||
return { valid: false, error: 'Invalid CNPJ checksum (second digit)' };
|
||||
}
|
||||
|
||||
// Format CNPJ
|
||||
const formatted = `${cleaned.substring(0, 2)}.${cleaned.substring(2, 5)}.${cleaned.substring(5, 8)}/${cleaned.substring(8, 12)}-${cleaned.substring(12, 14)}`;
|
||||
|
||||
return { valid: true, formatted };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Brazilian tax ID (CPF or CNPJ)
|
||||
*/
|
||||
export function validateBrazilianTaxId(taxId: string): {
|
||||
valid: boolean;
|
||||
type?: 'CPF' | 'CNPJ';
|
||||
formatted?: string;
|
||||
error?: string;
|
||||
} {
|
||||
const cleaned = taxId.replace(/\D/g, '');
|
||||
|
||||
if (cleaned.length === 11) {
|
||||
const cpfResult = validateCPF(taxId);
|
||||
return {
|
||||
...cpfResult,
|
||||
type: cpfResult.valid ? 'CPF' : undefined,
|
||||
};
|
||||
} else if (cleaned.length === 14) {
|
||||
const cnpjResult = validateCNPJ(taxId);
|
||||
return {
|
||||
...cnpjResult,
|
||||
type: cnpjResult.valid ? 'CNPJ' : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Tax ID must be 11 digits (CPF) or 14 digits (CNPJ)',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format tax ID for display (CPF or CNPJ)
|
||||
*/
|
||||
export function formatTaxId(taxId: string): string {
|
||||
const cleaned = taxId.replace(/\D/g, '');
|
||||
if (cleaned.length === 11) {
|
||||
return `${cleaned.substring(0, 3)}.${cleaned.substring(3, 6)}.${cleaned.substring(6, 9)}-${cleaned.substring(9, 11)}`;
|
||||
} else if (cleaned.length === 14) {
|
||||
return `${cleaned.substring(0, 2)}.${cleaned.substring(2, 5)}.${cleaned.substring(5, 8)}/${cleaned.substring(8, 12)}-${cleaned.substring(12, 14)}`;
|
||||
}
|
||||
return taxId;
|
||||
}
|
||||
15
packages/utils/tsconfig.json
Normal file
15
packages/utils/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../types"
|
||||
}
|
||||
]
|
||||
}
|
||||
1744
pnpm-lock.yaml
generated
Normal file
1744
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- 'apps/*'
|
||||
- 'packages/*'
|
||||
31
tsconfig.json
Normal file
31
tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"composite": true,
|
||||
"incremental": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@brazil-swift-ops/types": ["./packages/types/src"],
|
||||
"@brazil-swift-ops/utils": ["./packages/utils/src"],
|
||||
"@brazil-swift-ops/rules-engine": ["./packages/rules-engine/src"],
|
||||
"@brazil-swift-ops/iso20022": ["./packages/iso20022/src"],
|
||||
"@brazil-swift-ops/treasury": ["./packages/treasury/src"],
|
||||
"@brazil-swift-ops/risk-models": ["./packages/risk-models/src"],
|
||||
"@brazil-swift-ops/audit": ["./packages/audit/src"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "build"]
|
||||
}
|
||||
26
turbo.json
Normal file
26
turbo.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"globalDependencies": ["**/.env.*local"],
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"type-check": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"clean": {
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user