Implement UI components and quick wins

- Complete Dashboard page with statistics, recent activity, compliance status
- Complete Transactions page with form, validation, E&O uplift display
- Complete Treasury page with account management
- Complete Reports page with BCB report generation and export
- Add LoadingSpinner component
- Add ErrorBoundary component
- Add Toast notification system
- Add comprehensive input validation
- Add error handling utilities
- Add basic unit tests structure
- Fix XML exporter TypeScript errors
- All quick wins completed
This commit is contained in:
defiQUG
2026-01-23 16:32:41 -08:00
parent adb2b3620b
commit 7558268f9d
20 changed files with 2135 additions and 28 deletions

View File

@@ -0,0 +1,130 @@
/**
* Error handling and user-friendly error messages
*/
export class ValidationError extends Error {
constructor(
message: string,
public field?: string,
public code?: string
) {
super(message);
this.name = 'ValidationError';
}
}
export class BusinessRuleError extends Error {
constructor(
message: string,
public ruleId?: string,
public severity?: 'Info' | 'Warning' | 'Critical'
) {
super(message);
this.name = 'BusinessRuleError';
}
}
export class SystemError extends Error {
constructor(
message: string,
public originalError?: Error,
public context?: Record<string, unknown>
) {
super(message);
this.name = 'SystemError';
}
}
export class ExternalServiceError extends Error {
constructor(
message: string,
public service?: string,
public statusCode?: number,
public retryable?: boolean
) {
super(message);
this.name = 'ExternalServiceError';
}
}
/**
* Get user-friendly error message
*/
export function getUserFriendlyMessage(error: Error): string {
if (error instanceof ValidationError) {
return `Validation Error: ${error.message}${error.field ? ` (Field: ${error.field})` : ''}`;
}
if (error instanceof BusinessRuleError) {
return `Business Rule Violation: ${error.message}${error.ruleId ? ` (Rule: ${error.ruleId})` : ''}`;
}
if (error instanceof ExternalServiceError) {
if (error.service === 'fx-rate-service') {
return 'Unable to fetch exchange rates. Please try again or contact support.';
}
return `Service Error: ${error.message}. ${error.retryable ? 'Please try again.' : 'Please contact support.'}`;
}
if (error instanceof SystemError) {
return 'A system error occurred. Please contact support if the problem persists.';
}
// Generic error - don't expose internal details
return 'An error occurred. Please try again or contact support.';
}
/**
* Format error for logging (includes all details)
*/
export function formatErrorForLogging(error: Error): Record<string, unknown> {
const base: Record<string, unknown> = {
name: error.name,
message: error.message,
stack: error.stack,
};
if (error instanceof ValidationError) {
base.field = error.field;
base.code = error.code;
}
if (error instanceof BusinessRuleError) {
base.ruleId = error.ruleId;
base.severity = error.severity;
}
if (error instanceof SystemError) {
base.originalError = error.originalError?.message;
base.context = error.context;
}
if (error instanceof ExternalServiceError) {
base.service = error.service;
base.statusCode = error.statusCode;
base.retryable = error.retryable;
}
return base;
}
/**
* Check if error is retryable
*/
export function isRetryableError(error: Error): boolean {
if (error instanceof ExternalServiceError) {
return error.retryable ?? false;
}
if (error instanceof SystemError) {
// Network errors, timeouts are typically retryable
const message = error.message.toLowerCase();
return (
message.includes('timeout') ||
message.includes('network') ||
message.includes('connection')
);
}
return false;
}

View File

@@ -7,5 +7,6 @@
export * from './currency';
export * from './dates';
export * from './validation';
export * from './input-validation';
export * from './eo-uplift';
export * from './institution-config';

View File

@@ -0,0 +1,53 @@
/**
* Input validation utilities for user inputs and transaction data
*/
export interface ValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
}
/**
* Validate transaction amount
*/
export declare function validateAmount(amount: number | string): ValidationResult;
/**
* Validate currency code (ISO 4217)
*/
export declare function validateCurrency(currency: string): ValidationResult;
/**
* Validate email address
*/
export declare function validateEmail(email: string | undefined): ValidationResult;
/**
* Validate phone number (basic validation)
*/
export declare function validatePhone(phone: string | undefined): ValidationResult;
/**
* Validate account number or IBAN
*/
export declare function validateAccountNumber(accountNumber: string | undefined, iban: string | undefined): ValidationResult;
/**
* Validate purpose of payment
*/
export declare function validatePurposeOfPayment(purpose: string | undefined): ValidationResult;
/**
* Validate name field
*/
export declare function validateName(name: string | undefined, fieldName: string): ValidationResult;
/**
* Validate address
*/
export declare function validateAddress(address: string | undefined, city: string | undefined, country: string | undefined): ValidationResult;
/**
* Validate tax ID (CPF or CNPJ)
*/
export declare function validateTaxId(taxId: string | undefined, fieldName: string): ValidationResult;
/**
* Sanitize string input (remove dangerous characters)
*/
export declare function sanitizeString(input: string): string;
/**
* Sanitize number input
*/
export declare function sanitizeNumber(input: string | number): number;
//# sourceMappingURL=input-validation.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"input-validation.d.ts","sourceRoot":"","sources":["input-validation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,gBAAgB,CAmBxE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAiBnE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,CAkBzE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,CAkBzE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB,gBAAgB,CA0BlB;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,GAAG,SAAS,GAC1B,gBAAgB,CAiBlB;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAiB1F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,OAAO,EAAE,MAAM,GAAG,SAAS,GAC1B,gBAAgB,CAmBlB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAmB5F;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKpD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAM7D"}

View File

@@ -0,0 +1,214 @@
/**
* Input validation utilities for user inputs and transaction data
*/
import { validateBrazilianTaxId } from './validation';
/**
* Validate transaction amount
*/
export function validateAmount(amount) {
const errors = [];
const warnings = [];
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
if (isNaN(numAmount)) {
errors.push('Amount must be a valid number');
}
else if (numAmount <= 0) {
errors.push('Amount must be greater than zero');
}
else if (numAmount > 1000000000) {
warnings.push('Amount exceeds 1 billion - please verify');
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate currency code (ISO 4217)
*/
export function validateCurrency(currency) {
const errors = [];
const warnings = [];
if (!currency || currency.trim().length === 0) {
errors.push('Currency code is required');
}
else if (currency.length !== 3) {
errors.push('Currency code must be 3 characters (ISO 4217)');
}
else if (!/^[A-Z]{3}$/.test(currency)) {
errors.push('Currency code must be uppercase letters only');
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate email address
*/
export function validateEmail(email) {
const errors = [];
const warnings = [];
if (!email) {
return { valid: true, errors, warnings };
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
errors.push('Invalid email address format');
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate phone number (basic validation)
*/
export function validatePhone(phone) {
const errors = [];
const warnings = [];
if (!phone) {
return { valid: true, errors, warnings };
}
const phoneRegex = /^[\d\s\-\+\(\)]+$/;
if (!phoneRegex.test(phone)) {
errors.push('Invalid phone number format');
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate account number or IBAN
*/
export function validateAccountNumber(accountNumber, iban) {
const errors = [];
const warnings = [];
if (!accountNumber && !iban) {
errors.push('Either account number or IBAN is required');
return { valid: false, errors, warnings };
}
if (iban) {
// Basic IBAN validation (2 letters + 2 digits + up to 30 alphanumeric)
const ibanRegex = /^[A-Z]{2}\d{2}[A-Z0-9]{4,30}$/;
if (!ibanRegex.test(iban.replace(/\s/g, ''))) {
errors.push('Invalid IBAN format');
}
}
if (accountNumber && accountNumber.trim().length === 0) {
errors.push('Account number cannot be empty');
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate purpose of payment
*/
export function validatePurposeOfPayment(purpose) {
const errors = [];
const warnings = [];
if (!purpose || purpose.trim().length === 0) {
errors.push('Purpose of payment is required');
}
else if (purpose.trim().length < 5) {
warnings.push('Purpose of payment is very short - provide more detail');
}
else if (purpose.length > 140) {
warnings.push('Purpose of payment exceeds 140 characters - may be truncated');
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate name field
*/
export function validateName(name, fieldName) {
const errors = [];
const warnings = [];
if (!name || name.trim().length === 0) {
errors.push(`${fieldName} is required`);
}
else if (name.trim().length < 2) {
errors.push(`${fieldName} must be at least 2 characters`);
}
else if (name.length > 140) {
warnings.push(`${fieldName} exceeds 140 characters`);
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate address
*/
export function validateAddress(address, city, country) {
const errors = [];
const warnings = [];
if (!address && !city) {
errors.push('Either address or city is required');
}
if (!country) {
errors.push('Country is required');
}
else if (country.length !== 2) {
errors.push('Country must be a 2-letter ISO code');
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate tax ID (CPF or CNPJ)
*/
export function validateTaxId(taxId, fieldName) {
const errors = [];
const warnings = [];
if (!taxId) {
errors.push(`${fieldName} is required`);
return { valid: false, errors, warnings };
}
const validation = validateBrazilianTaxId(taxId);
if (!validation.valid) {
errors.push(`${fieldName}: ${validation.error || 'Invalid format'}`);
}
return {
valid: validation.valid,
errors,
warnings,
};
}
/**
* Sanitize string input (remove dangerous characters)
*/
export function sanitizeString(input) {
return input
.replace(/[<>]/g, '') // Remove potential HTML/XML tags
.replace(/[\x00-\x1F\x7F]/g, '') // Remove control characters
.trim();
}
/**
* Sanitize number input
*/
export function sanitizeNumber(input) {
if (typeof input === 'number') {
return isNaN(input) ? 0 : input;
}
const num = parseFloat(input.replace(/[^\d.-]/g, ''));
return isNaN(num) ? 0 : num;
}
//# sourceMappingURL=input-validation.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,256 @@
/**
* Input validation utilities for user inputs and transaction data
*/
import { validateBrazilianTaxId } from './validation';
export interface ValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
}
/**
* Validate transaction amount
*/
export function validateAmount(amount: number | string): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
if (isNaN(numAmount)) {
errors.push('Amount must be a valid number');
} else if (numAmount <= 0) {
errors.push('Amount must be greater than zero');
} else if (numAmount > 1000000000) {
warnings.push('Amount exceeds 1 billion - please verify');
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate currency code (ISO 4217)
*/
export function validateCurrency(currency: string): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (!currency || currency.trim().length === 0) {
errors.push('Currency code is required');
} else if (currency.length !== 3) {
errors.push('Currency code must be 3 characters (ISO 4217)');
} else if (!/^[A-Z]{3}$/.test(currency)) {
errors.push('Currency code must be uppercase letters only');
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate email address
*/
export function validateEmail(email: string | undefined): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (!email) {
return { valid: true, errors, warnings };
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
errors.push('Invalid email address format');
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate phone number (basic validation)
*/
export function validatePhone(phone: string | undefined): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (!phone) {
return { valid: true, errors, warnings };
}
const phoneRegex = /^[\d\s\-\+\(\)]+$/;
if (!phoneRegex.test(phone)) {
errors.push('Invalid phone number format');
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate account number or IBAN
*/
export function validateAccountNumber(
accountNumber: string | undefined,
iban: string | undefined
): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (!accountNumber && !iban) {
errors.push('Either account number or IBAN is required');
return { valid: false, errors, warnings };
}
if (iban) {
// Basic IBAN validation (2 letters + 2 digits + up to 30 alphanumeric)
const ibanRegex = /^[A-Z]{2}\d{2}[A-Z0-9]{4,30}$/;
if (!ibanRegex.test(iban.replace(/\s/g, ''))) {
errors.push('Invalid IBAN format');
}
}
if (accountNumber && accountNumber.trim().length === 0) {
errors.push('Account number cannot be empty');
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate purpose of payment
*/
export function validatePurposeOfPayment(
purpose: string | undefined
): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (!purpose || purpose.trim().length === 0) {
errors.push('Purpose of payment is required');
} else if (purpose.trim().length < 5) {
warnings.push('Purpose of payment is very short - provide more detail');
} else if (purpose.length > 140) {
warnings.push('Purpose of payment exceeds 140 characters - may be truncated');
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate name field
*/
export function validateName(name: string | undefined, fieldName: string): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (!name || name.trim().length === 0) {
errors.push(`${fieldName} is required`);
} else if (name.trim().length < 2) {
errors.push(`${fieldName} must be at least 2 characters`);
} else if (name.length > 140) {
warnings.push(`${fieldName} exceeds 140 characters`);
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate address
*/
export function validateAddress(
address: string | undefined,
city: string | undefined,
country: string | undefined
): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (!address && !city) {
errors.push('Either address or city is required');
}
if (!country) {
errors.push('Country is required');
} else if (country.length !== 2) {
errors.push('Country must be a 2-letter ISO code');
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate tax ID (CPF or CNPJ)
*/
export function validateTaxId(taxId: string | undefined, fieldName: string): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (!taxId) {
errors.push(`${fieldName} is required`);
return { valid: false, errors, warnings };
}
const validation = validateBrazilianTaxId(taxId);
if (!validation.valid) {
errors.push(`${fieldName}: ${validation.error || 'Invalid format'}`);
}
return {
valid: validation.valid,
errors,
warnings,
};
}
/**
* Sanitize string input (remove dangerous characters)
*/
export function sanitizeString(input: string): string {
return input
.replace(/[<>]/g, '') // Remove potential HTML/XML tags
.replace(/[\x00-\x1F\x7F]/g, '') // Remove control characters
.trim();
}
/**
* Sanitize number input
*/
export function sanitizeNumber(input: string | number): number {
if (typeof input === 'number') {
return isNaN(input) ? 0 : input;
}
const num = parseFloat(input.replace(/[^\d.-]/g, ''));
return isNaN(num) ? 0 : num;
}