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:
259
docs/ARCHITECTURE.md
Normal file
259
docs/ARCHITECTURE.md
Normal file
@@ -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
|
||||
320
docs/DEVELOPER_GUIDE.md
Normal file
320
docs/DEVELOPER_GUIDE.md
Normal file
@@ -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)
|
||||
83
packages/rules-engine/src/__tests__/documentation.test.ts
Normal file
83
packages/rules-engine/src/__tests__/documentation.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
66
packages/rules-engine/src/__tests__/iof.test.ts
Normal file
66
packages/rules-engine/src/__tests__/iof.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
36
packages/utils/src/__tests__/currency.test.ts
Normal file
36
packages/utils/src/__tests__/currency.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
166
packages/utils/src/config.ts
Normal file
166
packages/utils/src/config.ts
Normal 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;
|
||||
}
|
||||
265
packages/utils/src/fx-rates.ts
Normal file
265
packages/utils/src/fx-rates.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user