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:
130
packages/utils/src/errors.ts
Normal file
130
packages/utils/src/errors.ts
Normal 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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
53
packages/utils/src/input-validation.d.ts
vendored
Normal file
53
packages/utils/src/input-validation.d.ts
vendored
Normal 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
|
||||
1
packages/utils/src/input-validation.d.ts.map
Normal file
1
packages/utils/src/input-validation.d.ts.map
Normal 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"}
|
||||
214
packages/utils/src/input-validation.js
Normal file
214
packages/utils/src/input-validation.js
Normal 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
|
||||
1
packages/utils/src/input-validation.js.map
Normal file
1
packages/utils/src/input-validation.js.map
Normal file
File diff suppressed because one or more lines are too long
256
packages/utils/src/input-validation.ts
Normal file
256
packages/utils/src/input-validation.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user