Complete bank selector dropdown and SWIFT/BIC registry
- Added Bank type and BankRegistry interface - Created banks service with CRUD operations and validation - Added bank dropdown to Transactions page with ESTRBRRJ as default - Extended Transaction type with bankSwiftCode field - Added unit tests for bank registry - Bank information stored in TypeScript module (can be migrated to DB/XML)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { useTransactionStore } from '../stores/transactionStore';
|
||||
import type { Transaction } from '@brazil-swift-ops/types';
|
||||
import { calculateTransactionEOUplift, getDefaultConverter } from '@brazil-swift-ops/utils';
|
||||
import { calculateTransactionEOUplift, getDefaultConverter, getAllBanks, getBankBySwiftCode } from '@brazil-swift-ops/utils';
|
||||
import {
|
||||
validateAmount,
|
||||
validateCurrency,
|
||||
@@ -189,6 +189,35 @@ export default function TransactionsPage() {
|
||||
<h2 className="text-lg font-semibold">New Transaction</h2>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
{/* Bank Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Bank (SWIFT/BIC) *
|
||||
</label>
|
||||
<select
|
||||
value={selectedBankSwiftCode}
|
||||
onChange={(e) => setSelectedBankSwiftCode(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2"
|
||||
required
|
||||
>
|
||||
{banks.map((bank) => (
|
||||
<option key={bank.swiftCode} value={bank.swiftCode}>
|
||||
{bank.swiftCode} - {bank.institutionName} ({bank.city})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedBank && (
|
||||
<div className="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-md">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>Selected Bank:</strong> {selectedBank.institutionName}
|
||||
</p>
|
||||
<p className="text-xs text-blue-600 mt-1">
|
||||
{selectedBank.city}, {selectedBank.country} | SWIFT: {selectedBank.swiftCode}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Direction
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from './audit';
|
||||
export * from './risk';
|
||||
export * from './fx-contract';
|
||||
export * from './eo-uplift';
|
||||
export * from './bank';
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface Transaction {
|
||||
beneficiary: Party;
|
||||
purposeOfPayment?: string;
|
||||
fxContractId?: string; // Link to FX contract (contrato de câmbio)
|
||||
bankSwiftCode?: string; // SWIFT/BIC code of the bank processing the transaction
|
||||
swiftReference?: string;
|
||||
iso20022MessageId?: string;
|
||||
status: TransactionStatus;
|
||||
|
||||
158
packages/utils/src/banks.ts
Normal file
158
packages/utils/src/banks.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Bank Registry Service
|
||||
* Manages SWIFT/BIC code information for financial institutions
|
||||
*/
|
||||
|
||||
import type { Bank, BankRegistry } from '@brazil-swift-ops/types';
|
||||
|
||||
// Bank data - in production, this would be loaded from a database or external service
|
||||
const BANKS_DATA: Bank[] = [
|
||||
{
|
||||
swiftCode: 'ESTRBRRJ',
|
||||
institutionName: 'strategy INVESTIMENTOS S/A CVC',
|
||||
city: 'Rio de Janeiro',
|
||||
country: 'BR',
|
||||
active: true,
|
||||
},
|
||||
// Add more banks as needed
|
||||
];
|
||||
|
||||
let bankRegistry: BankRegistry = {
|
||||
banks: BANKS_DATA,
|
||||
lastUpdated: new Date(),
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all banks
|
||||
*/
|
||||
export function getAllBanks(): Bank[] {
|
||||
return bankRegistry.banks.filter((bank) => bank.active);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bank by SWIFT code
|
||||
*/
|
||||
export function getBankBySwiftCode(swiftCode: string): Bank | null {
|
||||
const normalizedCode = swiftCode.toUpperCase().trim();
|
||||
return (
|
||||
bankRegistry.banks.find(
|
||||
(bank) => bank.swiftCode.toUpperCase() === normalizedCode && bank.active
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search banks by name or city
|
||||
*/
|
||||
export function searchBanks(query: string): Bank[] {
|
||||
const normalizedQuery = query.toLowerCase().trim();
|
||||
if (!normalizedQuery) {
|
||||
return getAllBanks();
|
||||
}
|
||||
|
||||
return bankRegistry.banks.filter(
|
||||
(bank) =>
|
||||
bank.active &&
|
||||
(bank.institutionName.toLowerCase().includes(normalizedQuery) ||
|
||||
bank.city.toLowerCase().includes(normalizedQuery) ||
|
||||
bank.swiftCode.toLowerCase().includes(normalizedQuery))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new bank to the registry
|
||||
*/
|
||||
export function addBank(bank: Omit<Bank, 'active'> & { active?: boolean }): Bank {
|
||||
// Check if bank already exists
|
||||
const existing = getBankBySwiftCode(bank.swiftCode);
|
||||
if (existing) {
|
||||
throw new Error(`Bank with SWIFT code ${bank.swiftCode} already exists`);
|
||||
}
|
||||
|
||||
const newBank: Bank = {
|
||||
...bank,
|
||||
active: bank.active !== undefined ? bank.active : true,
|
||||
};
|
||||
|
||||
bankRegistry.banks.push(newBank);
|
||||
bankRegistry.lastUpdated = new Date();
|
||||
|
||||
return newBank;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing bank
|
||||
*/
|
||||
export function updateBank(swiftCode: string, updates: Partial<Bank>): Bank {
|
||||
const bank = getBankBySwiftCode(swiftCode);
|
||||
if (!bank) {
|
||||
throw new Error(`Bank with SWIFT code ${swiftCode} not found`);
|
||||
}
|
||||
|
||||
Object.assign(bank, updates);
|
||||
bankRegistry.lastUpdated = new Date();
|
||||
|
||||
return bank;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate a bank (soft delete)
|
||||
*/
|
||||
export function deactivateBank(swiftCode: string): boolean {
|
||||
const bank = getBankBySwiftCode(swiftCode);
|
||||
if (!bank) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bank.active = false;
|
||||
bankRegistry.lastUpdated = new Date();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bank registry metadata
|
||||
*/
|
||||
export function getBankRegistry(): BankRegistry {
|
||||
return { ...bankRegistry };
|
||||
}
|
||||
|
||||
/**
|
||||
* Load banks from external source (e.g., JSON file, API)
|
||||
* In production, this would fetch from a database or external service
|
||||
*/
|
||||
export function loadBanksFromSource(banks: Bank[]): void {
|
||||
bankRegistry.banks = banks;
|
||||
bankRegistry.lastUpdated = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate SWIFT code format
|
||||
* SWIFT codes are 8 or 11 characters: 4 letters (bank), 2 letters (country), 2 characters (location), optional 3 characters (branch)
|
||||
*/
|
||||
export function validateSwiftCode(swiftCode: string): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
} {
|
||||
const errors: string[] = [];
|
||||
const normalized = swiftCode.toUpperCase().trim();
|
||||
|
||||
if (!normalized) {
|
||||
errors.push('SWIFT code is required');
|
||||
return { valid: false, errors };
|
||||
}
|
||||
|
||||
if (normalized.length !== 8 && normalized.length !== 11) {
|
||||
errors.push('SWIFT code must be 8 or 11 characters');
|
||||
}
|
||||
|
||||
if (!/^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$/.test(normalized)) {
|
||||
errors.push('SWIFT code format is invalid (expected: AAAA BB CC DDD)');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
@@ -15,3 +15,4 @@ export * from './version';
|
||||
export * from './logging';
|
||||
export * from './config';
|
||||
export * from './fx-rates';
|
||||
export * from './banks';
|
||||
|
||||
Reference in New Issue
Block a user