diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..7dce4e9 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,259 @@ +# Architecture Documentation + +## Overview + +The Brazil SWIFT Operations Platform is a regulator-grade software system for processing SWIFT international payments and foreign exchange transactions in compliance with Brazilian regulations. + +## System Architecture + +### Monorepo Structure + +The project uses a monorepo architecture with pnpm workspaces and Turborepo for build orchestration: + +``` +brazil-swift-ops/ +├── apps/ +│ └── web/ # React web application +├── packages/ +│ ├── types/ # Shared TypeScript types +│ ├── utils/ # Shared utilities +│ ├── rules-engine/ # Brazil regulatory rules engine +│ ├── iso20022/ # ISO 20022 message handling +│ ├── treasury/ # Treasury management +│ └── audit/ # Audit logging and reporting +└── docs/ # Documentation +``` + +### Package Dependencies + +``` +web +├── types +├── utils +├── rules-engine +│ ├── types +│ └── utils +├── iso20022 +│ ├── types +│ └── utils +├── treasury +│ ├── types +│ └── utils +└── audit + ├── types + └── utils +``` + +## Core Components + +### 1. Rules Engine (`@brazil-swift-ops/rules-engine`) + +The rules engine evaluates transactions against Brazilian regulatory requirements: + +- **Threshold Check**: USD 10,000 reporting requirement +- **Documentation Check**: CPF/CNPJ validation, purpose of payment +- **FX Contract Check**: Validates FX contract linkage +- **IOF Calculation**: Calculates IOF tax based on transaction direction +- **AML Check**: Detects structuring patterns (rolling window analysis) + +**Decision Flow:** +1. Evaluate all rules +2. Aggregate results +3. Determine overall decision (Allow/Hold/Escalate) +4. Assign severity level + +### 2. ISO 20022 Message Handling (`@brazil-swift-ops/iso20022`) + +Supports three ISO 20022 message types: + +- **pacs.008**: FIToFICustomerCreditTransfer +- **pacs.009**: FinancialInstitutionCreditTransfer +- **pain.001**: CustomerCreditTransferInitiation + +**Features:** +- Message creation from transactions +- Message validation +- XML/JSON export +- MT103 mapping (SWIFT legacy format) + +### 3. Treasury Management (`@brazil-swift-ops/treasury`) + +Manages treasury accounts and subledgers: + +- **Account Store**: Parent treasury accounts +- **Subledger Accounts**: Child accounts linked to parent +- **Posting Engine**: Deterministic balance updates +- **Transfer System**: Inter-subledger transfers +- **Reporting**: Subledger-level reporting + +### 4. Audit System (`@brazil-swift-ops/audit`) + +Immutable audit logging: + +- **Audit Logger**: Creates audit logs for all transactions +- **BCB Reports**: Generates Banco Central do Brasil reports +- **Retention Policies**: Configurable data retention +- **Version Tracking**: Rule set version governance + +### 5. Utilities (`@brazil-swift-ops/utils`) + +Shared utilities: + +- **Currency Conversion**: FX rate handling +- **Validation**: CPF/CNPJ validation +- **Date Utilities**: Rolling windows, retention calculations +- **E&O Uplift**: Errors & Omissions calculation (10%) +- **Error Handling**: User-friendly error messages +- **Logging**: Structured JSON logging +- **Configuration**: Environment-based config management +- **Version Management**: Centralized version tracking + +## Data Flow + +### Transaction Processing Flow + +``` +1. User submits transaction + ↓ +2. Input validation + ↓ +3. Rules engine evaluation + ├── Threshold check + ├── Documentation check + ├── FX contract check + ├── IOF calculation + └── AML check + ↓ +4. Decision (Allow/Hold/Escalate) + ↓ +5. ISO 20022 message creation (if allowed) + ↓ +6. Treasury posting (if allowed) + ↓ +7. Audit logging + ↓ +8. BCB reporting (if required) +``` + +### Batch Processing Flow + +``` +1. User submits batch of transactions + ↓ +2. Validate all transactions + ↓ +3. Evaluate rules for each transaction + ↓ +4. Aggregate batch-level E&O uplift + ↓ +5. Generate batch-level report + ↓ +6. Create ISO 20022 batch message (pain.001) + ↓ +7. Audit logging for batch +``` + +## Technology Stack + +### Frontend +- **React 18**: UI framework +- **TypeScript**: Type safety +- **Tailwind CSS**: Styling +- **Vite**: Build tool +- **React Router**: Routing +- **Zustand**: State management + +### Backend (Packages) +- **TypeScript**: Type safety +- **Turborepo**: Monorepo build orchestration +- **pnpm**: Package manager + +### Testing +- **Vitest**: Unit and integration tests +- **Playwright**: E2E tests (planned) + +## Regulatory Compliance + +### Brazilian Regulations + +1. **USD 10,000 Reporting Threshold**: Per-transaction reporting to BCB +2. **CPF/CNPJ Validation**: Required for all parties +3. **Purpose of Payment**: Mandatory field +4. **IOF Tax**: Calculated based on transaction direction +5. **FX Contract Linkage**: Required for FX transactions +6. **AML Structuring Detection**: 30-day rolling window + +### Audit Requirements + +- Immutable audit logs +- 7-year retention (configurable) +- BCB-compliant report generation +- Rule version tracking + +## Security Considerations + +### Current Implementation +- Input validation and sanitization +- Error handling without exposing internals +- Type-safe data structures + +### Planned Enhancements +- Authentication and authorization +- Data encryption at rest and in transit +- API security (rate limiting, authentication) +- XSS protection + +## Performance Considerations + +### Current Implementation +- In-memory stores (for development) +- Efficient rule evaluation +- Batch processing support + +### Planned Enhancements +- Database persistence +- Caching layer (FX rates, rule results) +- Parallel batch processing +- Frontend optimization (code splitting, lazy loading) + +## Deployment Architecture + +### Development +- Local development with Vite dev server +- In-memory data stores +- Hot module replacement + +### Production (Planned) +- Containerized deployment (Docker) +- Database persistence (PostgreSQL) +- Log aggregation (ELK stack or similar) +- Monitoring and alerting (Prometheus, Grafana) + +## Extension Points + +### Adding New Rules +1. Create rule function in `packages/rules-engine/src/` +2. Add rule result type in `packages/types/src/regulatory.ts` +3. Register in orchestrator +4. Update UI to display rule results + +### Adding New Message Types +1. Define message type in `packages/types/src/iso20022.ts` +2. Create message builder in `packages/iso20022/src/` +3. Add validation function +4. Update exporter if needed + +### Adding New Treasury Features +1. Extend account types in `packages/types/src/treasury.ts` +2. Add business logic in `packages/treasury/src/` +3. Update UI components + +## Future Enhancements + +See `RECOMMENDATIONS.md` for detailed roadmap including: +- Database persistence +- Real-time FX rates +- Comprehensive testing +- Security features +- Performance optimizations +- Documentation diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md new file mode 100644 index 0000000..06fe897 --- /dev/null +++ b/docs/DEVELOPER_GUIDE.md @@ -0,0 +1,320 @@ +# Developer Guide + +## Getting Started + +### Prerequisites + +- Node.js >= 18.0.0 +- pnpm >= 8.0.0 + +### Installation + +```bash +# Install dependencies +pnpm install + +# Build all packages +pnpm build + +# Run type checking +pnpm type-check + +# Run tests +pnpm test +``` + +### Development + +```bash +# Start development server +pnpm dev + +# Build for production +pnpm build +``` + +## Project Structure + +### Monorepo Workspaces + +- `apps/web`: React web application +- `packages/types`: Shared TypeScript types +- `packages/utils`: Shared utilities +- `packages/rules-engine`: Regulatory rules engine +- `packages/iso20022`: ISO 20022 message handling +- `packages/treasury`: Treasury management +- `packages/audit`: Audit logging + +### Adding a New Package + +1. Create directory in `packages/` +2. Add `package.json` with workspace dependencies +3. Add `tsconfig.json` extending root config +4. Update root `tsconfig.json` references +5. Add to `pnpm-workspace.yaml` + +### Adding a New App + +1. Create directory in `apps/` +2. Add `package.json` with workspace dependencies +3. Add `tsconfig.json` extending root config +4. Update `turbo.json` build pipeline + +## Code Style + +### TypeScript + +- Use strict TypeScript configuration +- Prefer interfaces over types for public APIs +- Use explicit return types for exported functions +- Avoid `any` - use `unknown` if needed + +### Naming Conventions + +- **Files**: kebab-case (`transaction-store.ts`) +- **Types/Interfaces**: PascalCase (`Transaction`, `BrazilRegulatoryResult`) +- **Functions**: camelCase (`evaluateTransaction`) +- **Constants**: UPPER_SNAKE_CASE (`DEFAULT_CONFIG`) +- **Classes**: PascalCase (`AuditLogStore`) + +### Code Organization + +``` +package/ +├── src/ +│ ├── index.ts # Public API exports +│ ├── types.ts # Type definitions (if not in @types package) +│ ├── core.ts # Core functionality +│ ├── utils.ts # Internal utilities +│ └── __tests__/ # Test files +├── package.json +└── tsconfig.json +``` + +## Testing + +### Unit Tests + +```bash +# Run all tests +pnpm test + +# Run tests in watch mode +pnpm test:watch + +# Run tests for specific package +cd packages/utils && pnpm test +``` + +### Writing Tests + +```typescript +import { describe, it, expect } from 'vitest'; +import { myFunction } from '../my-module'; + +describe('myFunction', () => { + it('should do something', () => { + const result = myFunction(input); + expect(result).toBe(expected); + }); +}); +``` + +### Test Coverage + +Target: 80%+ coverage for critical functions + +## Building + +### Build Process + +1. TypeScript compilation (`tsc`) +2. Package bundling (if needed) +3. Type declaration generation + +### Build Order + +Turborepo handles build order automatically based on dependencies: +1. `types` (no dependencies) +2. `utils` (depends on `types`) +3. Other packages (depend on `types` and `utils`) +4. `web` (depends on all packages) + +## Type Safety + +### Internal Package Dependencies + +Use workspace protocol: +```json +{ + "dependencies": { + "@brazil-swift-ops/types": "workspace:*" + } +} +``` + +### Type Exports + +Export types from `index.ts`: +```typescript +export type { Transaction, BrazilRegulatoryResult } from './types'; +``` + +## Error Handling + +### Error Types + +Use custom error classes from `@brazil-swift-ops/utils`: +- `ValidationError`: Input validation failures +- `BusinessRuleError`: Business rule violations +- `SystemError`: System-level errors +- `ExternalServiceError`: External API failures + +### Error Logging + +```typescript +import { getLogger } from '@brazil-swift-ops/utils'; + +const logger = getLogger(); +logger.error('Operation failed', error, { context }); +``` + +## Configuration + +### Environment Variables + +Configuration is loaded from environment variables via `@brazil-swift-ops/utils/config`: + +```typescript +import { getConfig } from '@brazil-swift-ops/utils'; + +const config = getConfig(); +``` + +### Configuration Files + +- `.env`: Local development (not committed) +- `.env.example`: Example configuration (committed) + +## Logging + +### Structured Logging + +```typescript +import { getLogger, generateCorrelationId } from '@brazil-swift-ops/utils'; + +const logger = getLogger(); +const correlationId = generateCorrelationId(); +logger.setCorrelationId(correlationId); + +logger.info('Transaction processed', { transactionId: 'TXN-123' }); +``` + +### Log Levels + +- `debug`: Detailed debugging information +- `info`: General informational messages +- `warn`: Warning messages +- `error`: Error messages +- `fatal`: Critical errors + +## Version Management + +### Version Information + +```typescript +import { getVersion, getVersionString } from '@brazil-swift-ops/utils'; + +const version = getVersion(); +console.log(getVersionString()); // "brazil-swift-ops v1.0.0" +``` + +### Versioning Strategy + +- Semantic versioning (MAJOR.MINOR.PATCH) +- Version in root `package.json` is source of truth +- Rule set versions tracked separately in audit logs + +## Contributing + +### Workflow + +1. Create feature branch +2. Make changes +3. Write/update tests +4. Run tests and type checking +5. Commit with descriptive message +6. Push and create PR + +### Commit Messages + +Format: `type(scope): description` + +Types: +- `feat`: New feature +- `fix`: Bug fix +- `docs`: Documentation +- `test`: Tests +- `refactor`: Code refactoring +- `chore`: Maintenance + +Example: `feat(rules-engine): add AML structuring detection` + +## Common Tasks + +### Adding a New Rule + +1. Create rule function in `packages/rules-engine/src/` +2. Add result type in `packages/types/src/regulatory.ts` +3. Register in `packages/rules-engine/src/orchestrator.ts` +4. Add tests in `packages/rules-engine/src/__tests__/` + +### Adding a New ISO 20022 Message Type + +1. Define type in `packages/types/src/iso20022.ts` +2. Create builder in `packages/iso20022/src/` +3. Add validation function +4. Update exporter +5. Add tests + +### Adding a New UI Page + +1. Create component in `apps/web/src/pages/` +2. Add route in `apps/web/src/App.tsx` +3. Add navigation link +4. Create store if needed + +## Troubleshooting + +### Build Errors + +```bash +# Clean and rebuild +pnpm clean +pnpm install +pnpm build +``` + +### Type Errors + +```bash +# Check types +pnpm type-check + +# Rebuild types package +cd packages/types && pnpm build +``` + +### Test Failures + +```bash +# Run tests with verbose output +pnpm test -- --reporter=verbose +``` + +## Resources + +- [TypeScript Handbook](https://www.typescriptlang.org/docs/) +- [React Documentation](https://react.dev/) +- [Vitest Documentation](https://vitest.dev/) +- [Turborepo Documentation](https://turbo.build/repo/docs) diff --git a/packages/rules-engine/src/__tests__/documentation.test.ts b/packages/rules-engine/src/__tests__/documentation.test.ts new file mode 100644 index 0000000..bffdc5b --- /dev/null +++ b/packages/rules-engine/src/__tests__/documentation.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect } from 'vitest'; +import { validateDocumentation } from '../documentation'; +import type { Transaction } from '@brazil-swift-ops/types'; + +describe('Documentation Validation', () => { + it('should pass validation for complete transaction', () => { + const transaction: Transaction = { + id: 'TXN-1', + direction: 'outbound', + amount: 5000, + currency: 'USD', + orderingCustomer: { + name: 'John Doe', + taxId: '12345678909', + country: 'BR', + }, + beneficiary: { + name: 'Jane Smith', + taxId: '98765432100', + country: 'BR', + }, + purposeOfPayment: 'Payment for services rendered', + status: 'pending', + createdAt: new Date(), + updatedAt: new Date(), + }; + + const result = validateDocumentation(transaction); + expect(result.passed).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it('should fail validation for missing tax ID', () => { + const transaction: Transaction = { + id: 'TXN-2', + direction: 'outbound', + amount: 5000, + currency: 'USD', + orderingCustomer: { + name: 'John Doe', + country: 'BR', + }, + beneficiary: { + name: 'Jane Smith', + country: 'BR', + }, + purposeOfPayment: 'Payment', + status: 'pending', + createdAt: new Date(), + updatedAt: new Date(), + }; + + const result = validateDocumentation(transaction); + expect(result.passed).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + }); + + it('should fail validation for missing purpose of payment', () => { + const transaction: Transaction = { + id: 'TXN-3', + direction: 'outbound', + amount: 5000, + currency: 'USD', + orderingCustomer: { + name: 'John Doe', + taxId: '12345678909', + country: 'BR', + }, + beneficiary: { + name: 'Jane Smith', + taxId: '98765432100', + country: 'BR', + }, + status: 'pending', + createdAt: new Date(), + updatedAt: new Date(), + }; + + const result = validateDocumentation(transaction); + expect(result.passed).toBe(false); + expect(result.errors.some((e) => e.includes('purpose'))).toBe(true); + }); +}); diff --git a/packages/rules-engine/src/__tests__/iof.test.ts b/packages/rules-engine/src/__tests__/iof.test.ts new file mode 100644 index 0000000..17b875a --- /dev/null +++ b/packages/rules-engine/src/__tests__/iof.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { calculateIOF } from '../iof'; +import { setConfig, DEFAULT_CONFIG } from '../config'; +import type { Transaction } from '@brazil-swift-ops/types'; + +describe('IOF Calculation', () => { + beforeEach(() => { + setConfig(DEFAULT_CONFIG); + }); + + it('should calculate IOF for inbound transaction', () => { + const transaction: Transaction = { + id: 'TXN-1', + direction: 'inbound', + amount: 10000, + currency: 'USD', + orderingCustomer: { name: 'Test', country: 'BR' }, + beneficiary: { name: 'Test', country: 'BR' }, + status: 'pending', + createdAt: new Date(), + updatedAt: new Date(), + }; + + const result = calculateIOF(transaction); + expect(result.rate).toBe(DEFAULT_CONFIG.iofRateInbound); + expect(result.amount).toBe(10000 * DEFAULT_CONFIG.iofRateInbound); + expect(result.currency).toBe('BRL'); + }); + + it('should calculate IOF for outbound transaction', () => { + const transaction: Transaction = { + id: 'TXN-2', + direction: 'outbound', + amount: 10000, + currency: 'USD', + orderingCustomer: { name: 'Test', country: 'BR' }, + beneficiary: { name: 'Test', country: 'BR' }, + status: 'pending', + createdAt: new Date(), + updatedAt: new Date(), + }; + + const result = calculateIOF(transaction); + expect(result.rate).toBe(DEFAULT_CONFIG.iofRateOutbound); + expect(result.amount).toBe(10000 * DEFAULT_CONFIG.iofRateOutbound); + expect(result.currency).toBe('BRL'); + }); + + it('should return zero IOF for non-BRL transactions without conversion', () => { + const transaction: Transaction = { + id: 'TXN-3', + direction: 'inbound', + amount: 10000, + currency: 'EUR', + orderingCustomer: { name: 'Test', country: 'BR' }, + beneficiary: { name: 'Test', country: 'BR' }, + status: 'pending', + createdAt: new Date(), + updatedAt: new Date(), + }; + + const result = calculateIOF(transaction); + // IOF is calculated on BRL equivalent, so if no conversion provided, should handle gracefully + expect(result.rate).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/packages/utils/src/__tests__/currency.test.ts b/packages/utils/src/__tests__/currency.test.ts new file mode 100644 index 0000000..719b4e3 --- /dev/null +++ b/packages/utils/src/__tests__/currency.test.ts @@ -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(); + }); +}); diff --git a/packages/utils/src/config.ts b/packages/utils/src/config.ts new file mode 100644 index 0000000..c79458f --- /dev/null +++ b/packages/utils/src/config.ts @@ -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; +} diff --git a/packages/utils/src/fx-rates.ts b/packages/utils/src/fx-rates.ts new file mode 100644 index 0000000..51f8582 --- /dev/null +++ b/packages/utils/src/fx-rates.ts @@ -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; + getRates(baseCurrency: string, targetCurrencies: string[]): Promise>; +} + +/** + * Hardcoded FX Rate Provider (for development) + */ +class HardcodedFXRateProvider implements FXRateProvider { + name = 'hardcoded'; + + private rates: Map = 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 { + 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> { + const rates = new Map(); + + 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 { + // 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> { + const rates = new Map(); + + 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 = 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 { + 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> { + const rates = new Map(); + + 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; +}