Complete remaining todos: MT103 mapping, version management, logging, config, FX rates, tests, docs

- Enhanced MT103 mapping with all fields and validation
- Implemented version management system
- Added structured logging with correlation IDs
- Added configuration management from environment variables
- Implemented FX rate service with caching and provider abstraction
- Added comprehensive unit tests
- Created architecture and developer documentation
This commit is contained in:
defiQUG
2026-01-23 16:40:06 -08:00
parent 8322c8bf28
commit 13ee01e749
7 changed files with 1195 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { SimpleCurrencyConverter, getDefaultConverter } from '../currency';
describe('Currency Conversion', () => {
let converter: SimpleCurrencyConverter;
beforeEach(() => {
converter = getDefaultConverter();
});
it('should convert USD to USD (1:1)', () => {
const result = converter.convert(1000, 'USD', 'USD');
expect(result).toBe(1000);
});
it('should convert USD to BRL', () => {
const result = converter.convert(1000, 'USD', 'BRL');
expect(result).toBeGreaterThan(1000); // Assuming USD > BRL rate
});
it('should get USD equivalent', () => {
const result = converter.getUSDEquivalent(1000, 'USD');
expect(result).toBe(1000);
});
it('should get rate for currency pair', () => {
const rate = converter.getRate('USD', 'BRL');
expect(rate).toBeGreaterThan(0);
expect(typeof rate).toBe('number');
});
it('should return null for unsupported currency', () => {
const rate = converter.getRate('USD', 'XYZ');
expect(rate).toBeNull();
});
});

View File

@@ -0,0 +1,166 @@
/**
* Configuration management
* Externalizes configuration from environment variables and config files
*/
export interface AppConfig {
// Application
appName: string;
appVersion: string;
environment: 'development' | 'staging' | 'production';
port: number;
// Institution
institutionBIC: string;
institutionName: string;
institutionCountry: string;
// Regulatory
reportingThresholdUSD: number;
amlStructuringThresholdUSD: number;
amlStructuringWindowDays: number;
// IOF Rates
iofRateInbound: number;
iofRateOutbound: number;
// FX Rates
fxRateProvider: 'hardcoded' | 'central-bank' | 'bloomberg' | 'reuters' | 'xe';
fxRateCacheTTL: number; // seconds
// Database (when implemented)
databaseUrl?: string;
databasePoolSize?: number;
// Logging
logLevel: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
logFormat: 'json' | 'text';
// Security
enableAuth: boolean;
sessionSecret?: string;
jwtSecret?: string;
// BCB Reporting
bcbReportingEnabled: boolean;
bcbReportingApiUrl?: string;
bcbReportingApiKey?: string;
// Audit
auditRetentionDays: number;
auditAutoDelete: boolean;
}
/**
* Load configuration from environment variables
*/
export function loadConfig(): AppConfig {
return {
appName: process.env.APP_NAME || 'Brazil SWIFT Operations',
appVersion: process.env.APP_VERSION || '1.0.0',
environment: (process.env.NODE_ENV as any) || 'development',
port: parseInt(process.env.PORT || '3000', 10),
institutionBIC: process.env.INSTITUTION_BIC || 'ESTRBRRJ',
institutionName: process.env.INSTITUTION_NAME || 'Strategy Investimentos S/A CVC',
institutionCountry: process.env.INSTITUTION_COUNTRY || 'BR',
reportingThresholdUSD: parseFloat(process.env.REPORTING_THRESHOLD_USD || '10000'),
amlStructuringThresholdUSD: parseFloat(process.env.AML_STRUCTURING_THRESHOLD_USD || '10000'),
amlStructuringWindowDays: parseInt(process.env.AML_STRUCTURING_WINDOW_DAYS || '30', 10),
iofRateInbound: parseFloat(process.env.IOF_RATE_INBOUND || '0.0038'),
iofRateOutbound: parseFloat(process.env.IOF_RATE_OUTBOUND || '0.035'),
fxRateProvider: (process.env.FX_RATE_PROVIDER as any) || 'hardcoded',
fxRateCacheTTL: parseInt(process.env.FX_RATE_CACHE_TTL || '3600', 10),
databaseUrl: process.env.DATABASE_URL,
databasePoolSize: parseInt(process.env.DATABASE_POOL_SIZE || '10', 10),
logLevel: (process.env.LOG_LEVEL as any) || 'info',
logFormat: (process.env.LOG_FORMAT as any) || 'json',
enableAuth: process.env.ENABLE_AUTH === 'true',
sessionSecret: process.env.SESSION_SECRET,
jwtSecret: process.env.JWT_SECRET,
bcbReportingEnabled: process.env.BCB_REPORTING_ENABLED === 'true',
bcbReportingApiUrl: process.env.BCB_REPORTING_API_URL,
bcbReportingApiKey: process.env.BCB_REPORTING_API_KEY,
auditRetentionDays: parseInt(process.env.AUDIT_RETENTION_DAYS || '2555', 10), // 7 years
auditAutoDelete: process.env.AUDIT_AUTO_DELETE === 'true',
};
}
/**
* Validate configuration
*/
export function validateConfig(config: AppConfig): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (!config.institutionBIC || config.institutionBIC.length !== 8) {
errors.push('Institution BIC must be 8 characters');
}
if (config.reportingThresholdUSD <= 0) {
errors.push('Reporting threshold must be greater than 0');
}
if (config.iofRateInbound < 0 || config.iofRateInbound > 1) {
errors.push('IOF inbound rate must be between 0 and 1');
}
if (config.iofRateOutbound < 0 || config.iofRateOutbound > 1) {
errors.push('IOF outbound rate must be between 0 and 1');
}
if (config.auditRetentionDays < 0) {
errors.push('Audit retention days must be non-negative');
}
if (config.enableAuth && !config.sessionSecret && !config.jwtSecret) {
errors.push('Authentication enabled but no session or JWT secret provided');
}
if (config.bcbReportingEnabled && !config.bcbReportingApiUrl) {
errors.push('BCB reporting enabled but no API URL provided');
}
return {
valid: errors.length === 0,
errors,
};
}
/**
* Get default configuration (for development)
*/
export function getDefaultConfig(): AppConfig {
return loadConfig();
}
// Singleton instance
let configInstance: AppConfig | null = null;
/**
* Get configuration singleton
*/
export function getConfig(): AppConfig {
if (!configInstance) {
configInstance = loadConfig();
const validation = validateConfig(configInstance);
if (!validation.valid) {
console.warn('Configuration validation errors:', validation.errors);
}
}
return configInstance;
}
/**
* Reset configuration (useful for testing)
*/
export function resetConfig(): void {
configInstance = null;
}

View File

@@ -0,0 +1,265 @@
/**
* FX Rate Service
* Provides abstraction for multiple FX rate providers
*/
import { getLogger } from './logging';
import type { AppConfig } from './config';
import { getConfig } from './config';
export interface FXRate {
fromCurrency: string;
toCurrency: string;
rate: number;
timestamp: Date;
source: string;
}
export interface FXRateProvider {
name: string;
getRate(fromCurrency: string, toCurrency: string): Promise<FXRate | null>;
getRates(baseCurrency: string, targetCurrencies: string[]): Promise<Map<string, FXRate>>;
}
/**
* Hardcoded FX Rate Provider (for development)
*/
class HardcodedFXRateProvider implements FXRateProvider {
name = 'hardcoded';
private rates: Map<string, number> = new Map([
['USD-BRL', 5.20],
['EUR-BRL', 5.70],
['GBP-BRL', 6.50],
['BRL-USD', 1 / 5.20],
['EUR-USD', 1.10],
['GBP-USD', 1.25],
]);
async getRate(fromCurrency: string, toCurrency: string): Promise<FXRate | null> {
if (fromCurrency === toCurrency) {
return {
fromCurrency,
toCurrency,
rate: 1.0,
timestamp: new Date(),
source: this.name,
};
}
const key = `${fromCurrency}-${toCurrency}`;
const rate = this.rates.get(key);
if (!rate) {
// Try reverse rate
const reverseKey = `${toCurrency}-${fromCurrency}`;
const reverseRate = this.rates.get(reverseKey);
if (reverseRate) {
return {
fromCurrency,
toCurrency,
rate: 1 / reverseRate,
timestamp: new Date(),
source: this.name,
};
}
return null;
}
return {
fromCurrency,
toCurrency,
rate,
timestamp: new Date(),
source: this.name,
};
}
async getRates(baseCurrency: string, targetCurrencies: string[]): Promise<Map<string, FXRate>> {
const rates = new Map<string, FXRate>();
for (const targetCurrency of targetCurrencies) {
const rate = await this.getRate(baseCurrency, targetCurrency);
if (rate) {
rates.set(targetCurrency, rate);
}
}
return rates;
}
}
/**
* Central Bank of Brazil FX Rate Provider (stub)
*/
class CentralBankFXRateProvider implements FXRateProvider {
name = 'central-bank';
async getRate(fromCurrency: string, toCurrency: string): Promise<FXRate | null> {
// TODO: Implement actual BCB API integration
// For now, fallback to hardcoded
const hardcoded = new HardcodedFXRateProvider();
return hardcoded.getRate(fromCurrency, toCurrency);
}
async getRates(baseCurrency: string, targetCurrencies: string[]): Promise<Map<string, FXRate>> {
const rates = new Map<string, FXRate>();
for (const targetCurrency of targetCurrencies) {
const rate = await this.getRate(baseCurrency, targetCurrency);
if (rate) {
rates.set(targetCurrency, rate);
}
}
return rates;
}
}
/**
* FX Rate Cache
*/
class FXRateCache {
private cache: Map<string, { rate: FXRate; expiresAt: Date }> = new Map();
private ttl: number; // seconds
constructor(ttl: number) {
this.ttl = ttl;
}
get(key: string): FXRate | null {
const cached = this.cache.get(key);
if (!cached) {
return null;
}
if (new Date() > cached.expiresAt) {
this.cache.delete(key);
return null;
}
return cached.rate;
}
set(key: string, rate: FXRate): void {
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + this.ttl);
this.cache.set(key, { rate, expiresAt });
}
clear(): void {
this.cache.clear();
}
getCacheKey(fromCurrency: string, toCurrency: string): string {
return `${fromCurrency}-${toCurrency}`;
}
}
/**
* FX Rate Service
*/
class FXRateService {
private provider: FXRateProvider;
private cache: FXRateCache;
private config: AppConfig;
private logger = getLogger();
constructor(config?: AppConfig) {
this.config = config || getConfig();
this.cache = new FXRateCache(this.config.fxRateCacheTTL);
// Initialize provider based on config
switch (this.config.fxRateProvider) {
case 'central-bank':
this.provider = new CentralBankFXRateProvider();
break;
case 'hardcoded':
default:
this.provider = new HardcodedFXRateProvider();
break;
}
}
async getRate(fromCurrency: string, toCurrency: string, useCache: boolean = true): Promise<FXRate | null> {
const cacheKey = this.cache.getCacheKey(fromCurrency, toCurrency);
// Check cache first
if (useCache) {
const cached = this.cache.get(cacheKey);
if (cached) {
this.logger.debug('FX rate retrieved from cache', { fromCurrency, toCurrency, rate: cached.rate });
return cached;
}
}
try {
// Fetch from provider
const rate = await this.provider.getRate(fromCurrency, toCurrency);
if (rate) {
// Cache the rate
if (useCache) {
this.cache.set(cacheKey, rate);
}
this.logger.info('FX rate retrieved from provider', {
fromCurrency,
toCurrency,
rate: rate.rate,
source: rate.source,
});
return rate;
}
this.logger.warn('FX rate not available', { fromCurrency, toCurrency });
return null;
} catch (error) {
this.logger.error('Error fetching FX rate', error as Error, { fromCurrency, toCurrency });
return null;
}
}
async getRates(baseCurrency: string, targetCurrencies: string[]): Promise<Map<string, FXRate>> {
const rates = new Map<string, FXRate>();
for (const targetCurrency of targetCurrencies) {
const rate = await this.getRate(baseCurrency, targetCurrency);
if (rate) {
rates.set(targetCurrency, rate);
}
}
return rates;
}
clearCache(): void {
this.cache.clear();
this.logger.info('FX rate cache cleared');
}
getProviderName(): string {
return this.provider.name;
}
}
// Singleton instance
let fxRateServiceInstance: FXRateService | null = null;
/**
* Get FX Rate Service singleton
*/
export function getFXRateService(config?: AppConfig): FXRateService {
if (!fxRateServiceInstance) {
fxRateServiceInstance = new FXRateService(config);
}
return fxRateServiceInstance;
}
/**
* Reset FX Rate Service (useful for testing)
*/
export function resetFXRateService(): void {
fxRateServiceInstance = null;
}