ecfr-navigator
Version:
A lightweight, reusable Vue 3 component with Pinia integration for navigating hierarchical eCFR-style content in existing Vue applications.
687 lines (552 loc) • 21.3 kB
JavaScript
import { ref, reactive, computed } from 'vue';
/**
* Advanced validation patterns for financial services and complex forms
*/
export function useAdvancedValidation() {
// Validation result cache for performance
const validationCache = new Map();
const asyncValidationPromises = new Map();
/**
* Financial validation patterns
*/
const financialValidators = {
// Credit card number validation with Luhn algorithm
creditCard: (value, message = 'Invalid credit card number') => {
if (!value) return true;
const cleanValue = value.replace(/\D/g, '');
if (cleanValue.length < 13 || cleanValue.length > 19) {
return message;
}
// Luhn algorithm
let sum = 0;
let isEven = false;
for (let i = cleanValue.length - 1; i >= 0; i--) {
let digit = parseInt(cleanValue[i]);
if (isEven) {
digit *= 2;
if (digit > 9) {
digit -= 9;
}
}
sum += digit;
isEven = !isEven;
}
return sum % 10 === 0 ? true : message;
},
// Bank routing number validation
routingNumber: (value, message = 'Invalid routing number') => {
if (!value) return true;
const cleanValue = value.replace(/\D/g, '');
if (cleanValue.length !== 9) {
return message;
}
// ABA routing number checksum
const digits = cleanValue.split('').map(Number);
const checksum = (
3 * (digits[0] + digits[3] + digits[6]) +
7 * (digits[1] + digits[4] + digits[7]) +
1 * (digits[2] + digits[5] + digits[8])
) % 10;
return checksum === 0 ? true : message;
},
// Bank account number validation (basic format check)
accountNumber: (value, message = 'Invalid account number format') => {
if (!value) return true;
const cleanValue = value.replace(/\D/g, '');
if (cleanValue.length < 4 || cleanValue.length > 17) {
return message;
}
return true;
},
// IBAN validation
iban: (value, message = 'Invalid IBAN') => {
if (!value) return true;
const cleanValue = value.replace(/\s/g, '').toUpperCase();
if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/.test(cleanValue)) {
return message;
}
// Move first 4 characters to end
const rearranged = cleanValue.slice(4) + cleanValue.slice(0, 4);
// Replace letters with numbers (A=10, B=11, etc.)
const numericString = rearranged.replace(/[A-Z]/g, (char) =>
(char.charCodeAt(0) - 55).toString()
);
// Mod 97 check
let remainder = '';
for (let i = 0; i < numericString.length; i += 7) {
remainder = (remainder + numericString.slice(i, i + 7)) % 97;
}
return remainder === 1 ? true : message;
},
// Tax ID validation (EIN format)
taxId: (value, message = 'Invalid Tax ID format') => {
if (!value) return true;
const cleanValue = value.replace(/\D/g, '');
if (cleanValue.length !== 9) {
return message;
}
// Basic EIN format validation
const firstTwo = cleanValue.slice(0, 2);
const validPrefixes = [
'01', '02', '03', '04', '05', '06', '10', '11', '12', '13', '14', '15', '16',
'20', '21', '22', '23', '24', '25', '26', '27', '30', '31', '32', '33', '34',
'35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47',
'48', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '60', '61',
'62', '63', '64', '65', '66', '67', '68', '71', '72', '73', '74', '75', '76',
'77', '80', '81', '82', '83', '84', '85', '86', '87', '88', '90', '91', '92',
'93', '94', '95', '98', '99'
];
return validPrefixes.includes(firstTwo) ? true : message;
},
// Income range validation
incomeRange: (minIncome, maxIncome, message) => (value) => {
const numValue = Number(value);
if (isNaN(numValue)) {
return 'Income must be a valid number';
}
if (numValue < minIncome || numValue > maxIncome) {
return message || `Income must be between $${minIncome.toLocaleString()} and $${maxIncome.toLocaleString()}`;
}
return true;
},
// Debt-to-income ratio validation
debtToIncomeRatio: (maxRatio, message) => (value, allData) => {
const monthlyIncome = Number(allData.employment?.grossMonthlyIncome) || 0;
const monthlyDebt = Number(allData.financial?.monthlyDebtPayments) || 0;
if (monthlyIncome === 0) {
return 'Monthly income is required for debt-to-income calculation';
}
const ratio = (monthlyDebt / monthlyIncome) * 100;
if (ratio > maxRatio) {
return message || `Debt-to-income ratio (${ratio.toFixed(1)}%) exceeds maximum allowed (${maxRatio}%)`;
}
return true;
}
};
/**
* Identity validation patterns
*/
const identityValidators = {
// Enhanced SSN validation with area number checks
ssnAdvanced: (value, message = 'Invalid Social Security Number') => {
if (!value) return true;
const cleanValue = value.replace(/\D/g, '');
if (cleanValue.length !== 9) {
return message;
}
const area = cleanValue.slice(0, 3);
const group = cleanValue.slice(3, 5);
const serial = cleanValue.slice(5, 9);
// Invalid patterns
if (area === '000' || area === '666' || area.startsWith('9')) {
return message;
}
if (group === '00' || serial === '0000') {
return message;
}
// Known invalid SSNs
const invalidSSNs = [
'123456789', '111111111', '222222222', '333333333',
'444444444', '555555555', '777777777', '888888888'
];
if (invalidSSNs.includes(cleanValue)) {
return message;
}
return true;
},
// Driver's license validation (format varies by state)
driversLicense: (state, message = 'Invalid driver\'s license format') => (value) => {
if (!value) return true;
const patterns = {
'CA': /^[A-Z]\d{7}$/,
'NY': /^\d{3}\s?\d{3}\s?\d{3}$/,
'TX': /^\d{8}$/,
'FL': /^[A-Z]\d{12}$/,
'IL': /^[A-Z]\d{11}$/,
// Add more state patterns as needed
};
const pattern = patterns[state];
if (!pattern) {
return true; // Skip validation for unsupported states
}
const cleanValue = value.replace(/\s/g, '').toUpperCase();
return pattern.test(cleanValue) ? true : message;
},
// Passport number validation
passport: (country = 'US', message = 'Invalid passport number') => (value) => {
if (!value) return true;
const patterns = {
'US': /^\d{9}$/,
'CA': /^[A-Z]{2}\d{6}$/,
'UK': /^\d{9}$/,
'DE': /^[CFGHJKLMNPRTVWXYZ0-9]{9}$/
};
const pattern = patterns[country];
if (!pattern) {
return true; // Skip validation for unsupported countries
}
const cleanValue = value.replace(/\s/g, '').toUpperCase();
return pattern.test(cleanValue) ? true : message;
}
};
/**
* Business validation patterns
*/
const businessValidators = {
// DUNS number validation
duns: (value, message = 'Invalid DUNS number') => {
if (!value) return true;
const cleanValue = value.replace(/\D/g, '');
if (cleanValue.length !== 9) {
return message;
}
return true; // DUNS doesn't have a simple checksum algorithm
},
// NAICS code validation
naics: (value, message = 'Invalid NAICS code') => {
if (!value) return true;
const cleanValue = value.replace(/\D/g, '');
if (cleanValue.length !== 6) {
return message;
}
// Basic range check (NAICS codes are 11-92)
const sector = parseInt(cleanValue.slice(0, 2));
return (sector >= 11 && sector <= 92) ? true : message;
},
// Business license validation (basic format)
businessLicense: (value, message = 'Invalid business license format') => {
if (!value) return true;
// General format: alphanumeric, 6-20 characters
if (!/^[A-Z0-9]{6,20}$/i.test(value)) {
return message;
}
return true;
}
};
/**
* Address validation patterns
*/
const addressValidators = {
// ZIP+4 validation
zipPlusFour: (value, message = 'Invalid ZIP+4 format') => {
if (!value) return true;
const cleanValue = value.replace(/\D/g, '');
if (cleanValue.length === 5) {
return /^\d{5}$/.test(cleanValue) ? true : message;
} else if (cleanValue.length === 9) {
return /^\d{5}\d{4}$/.test(cleanValue) ? true : message;
}
return message;
},
// Canadian postal code validation
postalCodeCA: (value, message = 'Invalid Canadian postal code') => {
if (!value) return true;
const cleanValue = value.replace(/\s/g, '').toUpperCase();
return /^[A-Z]\d[A-Z]\d[A-Z]\d$/.test(cleanValue) ? true : message;
},
// UK postcode validation
postcodeUK: (value, message = 'Invalid UK postcode') => {
if (!value) return true;
const cleanValue = value.replace(/\s/g, '').toUpperCase();
return /^[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}$/.test(cleanValue) ? true : message;
}
};
/**
* Date and time validators
*/
const dateValidators = {
// Age validation
age: (minAge, maxAge, message) => (value) => {
if (!value) return true;
const birthDate = new Date(value);
const today = new Date();
if (isNaN(birthDate.getTime())) {
return 'Invalid date format';
}
if (birthDate > today) {
return 'Birth date cannot be in the future';
}
const age = Math.floor((today - birthDate) / (365.25 * 24 * 60 * 60 * 1000));
if (age < minAge || age > maxAge) {
return message || `Age must be between ${minAge} and ${maxAge} years`;
}
return true;
},
// Business hours validation
businessHours: (startHour, endHour, message) => (value) => {
if (!value) return true;
const time = new Date(`1970-01-01T${value}:00`);
if (isNaN(time.getTime())) {
return 'Invalid time format';
}
const hour = time.getHours();
if (hour < startHour || hour >= endHour) {
return message || `Time must be between ${startHour}:00 and ${endHour}:00`;
}
return true;
},
// Date range validation
dateRange: (minDate, maxDate, message) => (value) => {
if (!value) return true;
const date = new Date(value);
const min = new Date(minDate);
const max = new Date(maxDate);
if (isNaN(date.getTime())) {
return 'Invalid date format';
}
if (date < min || date > max) {
return message || `Date must be between ${min.toLocaleDateString()} and ${max.toLocaleDateString()}`;
}
return true;
}
};
/**
* Async validation patterns
*/
const asyncValidators = {
// Email domain validation
emailDomain: (allowedDomains, message) => async (value) => {
if (!value) return true;
const cacheKey = `emailDomain_${value}`;
if (validationCache.has(cacheKey)) {
return validationCache.get(cacheKey);
}
const domain = value.split('@')[1];
if (!domain) {
return 'Invalid email format';
}
if (allowedDomains && !allowedDomains.includes(domain)) {
const result = message || `Email domain '${domain}' is not allowed`;
validationCache.set(cacheKey, result);
return result;
}
try {
// Simulate DNS lookup
await new Promise(resolve => setTimeout(resolve, 300));
// In real implementation, check MX records
const blockedDomains = ['tempmail.com', '10minutemail.com', 'guerrillamail.com'];
if (blockedDomains.some(blocked => domain.includes(blocked))) {
const result = 'Temporary email addresses are not allowed';
validationCache.set(cacheKey, result);
return result;
}
validationCache.set(cacheKey, true);
return true;
} catch (error) {
return 'Unable to verify email domain';
}
},
// SSN verification (simulated)
ssnVerification: (message = 'SSN verification failed') => async (value) => {
if (!value) return true;
const cacheKey = `ssnVerification_${value}`;
if (validationCache.has(cacheKey)) {
return validationCache.get(cacheKey);
}
try {
// Simulate SSN verification API call
await new Promise(resolve => setTimeout(resolve, 800));
const cleanValue = value.replace(/\D/g, '');
// Simulate blocked SSNs
const blockedSSNs = ['123456789', '987654321'];
if (blockedSSNs.includes(cleanValue)) {
const result = 'This SSN is flagged in our system';
validationCache.set(cacheKey, result);
return result;
}
validationCache.set(cacheKey, true);
return true;
} catch (error) {
return 'Unable to verify SSN at this time';
}
},
// Address verification
addressVerification: (message = 'Address verification failed') => async (value, allData) => {
if (!value) return true;
const fullAddress = `${value} ${allData.address?.city} ${allData.address?.state} ${allData.address?.zipCode}`;
const cacheKey = `addressVerification_${fullAddress}`;
if (validationCache.has(cacheKey)) {
return validationCache.get(cacheKey);
}
try {
// Simulate address verification API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Simulate unverifiable addresses
if (value.toLowerCase().includes('fake') || value.toLowerCase().includes('test')) {
const result = 'Address could not be verified';
validationCache.set(cacheKey, result);
return result;
}
validationCache.set(cacheKey, true);
return true;
} catch (error) {
return 'Unable to verify address at this time';
}
},
// Credit check simulation
creditCheck: (minScore, message) => async (value, allData) => {
const ssn = allData.personal?.ssn;
if (!ssn) {
return 'SSN required for credit check';
}
const cacheKey = `creditCheck_${ssn}`;
if (validationCache.has(cacheKey)) {
return validationCache.get(cacheKey);
}
try {
// Simulate credit bureau API call
await new Promise(resolve => setTimeout(resolve, 1500));
// Simulate credit score based on SSN
const cleanSSN = ssn.replace(/\D/g, '');
const lastDigit = parseInt(cleanSSN.slice(-1));
const simulatedScore = 500 + (lastDigit * 35); // 500-815 range
if (simulatedScore < minScore) {
const result = message || `Credit score (${simulatedScore}) below minimum requirement (${minScore})`;
validationCache.set(cacheKey, result);
return result;
}
validationCache.set(cacheKey, true);
return true;
} catch (error) {
return 'Unable to perform credit check at this time';
}
}
};
/**
* Conditional validation patterns
*/
const conditionalValidators = {
// Required if another field has a specific value
requiredIf: (dependentField, dependentValue, message) => (value, allData) => {
const dependentFieldValue = getNestedValue(allData, dependentField);
if (dependentFieldValue === dependentValue) {
if (!value || value === '') {
return message || `This field is required when ${dependentField} is ${dependentValue}`;
}
}
return true;
},
// Minimum value based on another field
minValueIf: (dependentField, multiplier, message) => (value, allData) => {
const dependentFieldValue = Number(getNestedValue(allData, dependentField)) || 0;
const minValue = dependentFieldValue * multiplier;
const currentValue = Number(value) || 0;
if (currentValue < minValue) {
return message || `Value must be at least ${minValue.toLocaleString()} (${multiplier}x ${dependentField})`;
}
return true;
},
// Cross-field comparison
greaterThan: (compareField, message) => (value, allData) => {
const compareValue = Number(getNestedValue(allData, compareField)) || 0;
const currentValue = Number(value) || 0;
if (currentValue <= compareValue) {
return message || `Value must be greater than ${compareField} (${compareValue})`;
}
return true;
},
// Date comparison
dateAfter: (compareField, message) => (value, allData) => {
if (!value) return true;
const compareValue = getNestedValue(allData, compareField);
if (!compareValue) return true;
const currentDate = new Date(value);
const compareDate = new Date(compareValue);
if (isNaN(currentDate.getTime()) || isNaN(compareDate.getTime())) {
return 'Invalid date format';
}
if (currentDate <= compareDate) {
return message || `Date must be after ${compareField}`;
}
return true;
}
};
/**
* Utility function to get nested object values
*/
const getNestedValue = (obj, path) => {
return path.split('.').reduce((current, key) => current?.[key], obj);
};
/**
* Create validation rule from pattern
*/
const createValidator = (pattern, ...args) => {
const [category, validatorName] = pattern.split('.');
const validatorMap = {
financial: financialValidators,
identity: identityValidators,
business: businessValidators,
address: addressValidators,
date: dateValidators,
async: asyncValidators,
conditional: conditionalValidators
};
const categoryValidators = validatorMap[category];
if (!categoryValidators || !categoryValidators[validatorName]) {
throw new Error(`Unknown validation pattern: ${pattern}`);
}
const validator = categoryValidators[validatorName];
return typeof validator === 'function' ? validator(...args) : validator;
};
/**
* Batch validation for multiple fields
*/
const validateBatch = async (fields, data) => {
const results = {};
const promises = [];
for (const [fieldPath, validators] of Object.entries(fields)) {
const value = getNestedValue(data, fieldPath);
for (const validator of validators) {
if (typeof validator === 'function') {
const result = validator(value, data);
if (result instanceof Promise) {
promises.push(
result.then(res => ({ fieldPath, result: res }))
.catch(err => ({ fieldPath, result: err.message }))
);
} else if (result !== true) {
results[fieldPath] = result;
break; // Stop on first error
}
}
}
}
// Wait for async validations
const asyncResults = await Promise.all(promises);
asyncResults.forEach(({ fieldPath, result }) => {
if (result !== true && !results[fieldPath]) {
results[fieldPath] = result;
}
});
return results;
};
/**
* Clear validation cache
*/
const clearCache = (pattern) => {
if (pattern) {
for (const key of validationCache.keys()) {
if (key.includes(pattern)) {
validationCache.delete(key);
}
}
} else {
validationCache.clear();
}
};
return {
// Validator categories
financialValidators,
identityValidators,
businessValidators,
addressValidators,
dateValidators,
asyncValidators,
conditionalValidators,
// Utilities
createValidator,
validateBatch,
clearCache,
getNestedValue
};
}