UNPKG

@iota-big3/sdk-gateway

Version:

Universal API Gateway with protocol translation, intelligent routing, rate limiting, health checking, and caching

437 lines 15.2 kB
"use strict"; /** * Financial Validation Utilities * Following Phase 2g: Strict validation for financial operations * * @module financial-validation * @description Enforces financial constraints: * - Double-entry bookkeeping rules * - Currency and precision validation * - Audit trail requirements * - SOX compliance checks */ Object.defineProperty(exports, "__esModule", { value: true }); exports.financialValidators = void 0; exports.validateMonetaryAmount = validateMonetaryAmount; exports.validateJournalEntry = validateJournalEntry; exports.validateFinancialUserPermission = validateFinancialUserPermission; exports.validateGLTransaction = validateGLTransaction; const financial_types_1 = require("./financial-types"); const financial_types_2 = require("./financial-types"); // ============================================ // Core Financial Validations // ============================================ /** * Validate monetary amount */ function validateMonetaryAmount(value) { const errors = []; if (!value || typeof value !== 'object') { errors.push({ path: 'root', message: 'Monetary amount must be an object', expected: 'object', actual: typeof value, code: 'INVALID_MONEY' }); return { valid: false, errors }; } const amount = value; // Validate value as bigint if (typeof amount.value !== 'bigint') { if (typeof amount.value === 'number' && Number.isInteger(amount.value)) { // Convert integer to bigint amount.value = BigInt(amount.value); } else { errors.push({ path: 'value', message: 'Value must be a bigint', expected: 'bigint', actual: typeof amount.value, code: 'INVALID_VALUE' }); } } // Validate precision if (typeof amount.precision !== 'number' || !Number.isInteger(amount.precision) || amount.precision < 0) { errors.push({ path: 'precision', message: 'Precision must be a non-negative integer', expected: 'integer >= 0', actual: String(amount.precision), code: 'INVALID_PRECISION' }); } // Validate currency if (typeof amount.currency !== 'string' || amount.currency.length !== 3) { errors.push({ path: 'currency', message: 'Currency must be a 3-letter ISO code', expected: 'string(3)', actual: String(amount.currency), code: 'INVALID_CURRENCY' }); } const warnings = []; // Warn about precision for specific currencies if (amount.currency === 'JPY' && amount.precision !== 0) { warnings.push('JPY typically has precision 0'); } const valid = errors.length === 0; const typedValue = valid ? amount : undefined; return { valid, value: typedValue, errors, warnings, metadata: { validatedAt: new Date().toISOString(), validatorVersion: '1.0.0', checksPerformed: 3 } }; } /** * Validate journal entry for double-entry bookkeeping */ function validateJournalEntry(entry) { const errors = []; const warnings = []; if (!entry || typeof entry !== 'object') { errors.push({ path: 'root', message: 'Journal entry must be an object', expected: 'object', actual: typeof entry, code: 'INVALID_ENTRY' }); return { valid: false, errors }; } const je = entry; // Validate required fields if (!je.id || typeof je.id !== 'string') { errors.push({ path: 'id', message: 'Journal entry ID is required', expected: 'string', actual: typeof je.id, code: 'MISSING_ID' }); } // Validate date if (!je.date || typeof je.date !== 'string' || !isValidISODate(je.date)) { errors.push({ path: 'date', message: 'Date must be valid ISO 8601', expected: 'ISO 8601 date', actual: String(je.date), code: 'INVALID_DATE' }); } // Validate lines if (!Array.isArray(je.lines) || je.lines.length < 2) { errors.push({ path: 'lines', message: 'Journal entry must have at least 2 lines', expected: 'array(length >= 2)', actual: Array.isArray(je.lines) ? `array(${je.lines.length})` : typeof je.lines, code: 'INSUFFICIENT_LINES' }); return { valid: false, errors }; } // Validate each line and calculate totals let totalDebits = BigInt(0); let totalCredits = BigInt(0); let currency; for (let i = 0; i < je.lines.length; i++) { const line = je.lines[i]; // Validate line structure if (!line.account || typeof line.account !== 'string') { errors.push({ path: `lines[${i}].account`, message: 'Account number is required', expected: 'string', actual: typeof line.account, code: 'MISSING_ACCOUNT' }); } // Validate amount const amountResult = validateMonetaryAmount(line.amount); if (!amountResult.valid) { errors.push(...amountResult.errors.map(e => ({ ...e, path: `lines[${i}].amount.${e.path}` }))); continue; } const amount = amountResult.value; // Check currency consistency if (!currency) { currency = amount.currency; } else if (currency !== amount.currency) { errors.push({ path: `lines[${i}].amount.currency`, message: 'All lines must use the same currency', expected: currency, actual: amount.currency, code: 'CURRENCY_MISMATCH' }); } // Validate entry type if (line.type !== financial_types_1.EntryType.Debit && line.type !== financial_types_1.EntryType.Credit) { errors.push({ path: `lines[${i}].type`, message: 'Entry type must be DEBIT or CREDIT', expected: 'DEBIT | CREDIT', actual: String(line.type), code: 'INVALID_ENTRY_TYPE' }); continue; } // Add to totals if (line.type === financial_types_1.EntryType.Debit) { totalDebits += amount.value; } else { totalCredits += amount.value; } } // Validate double-entry balance if (totalDebits !== totalCredits) { errors.push({ path: 'lines', message: 'Journal entry must balance (debits = credits)', expected: `debits: ${totalDebits}`, actual: `credits: ${totalCredits}`, code: 'UNBALANCED_ENTRY' }); } // Validate status const validStatuses = Object.values(financial_types_1.JournalEntryStatus); if (!validStatuses.includes(je.status)) { errors.push({ path: 'status', message: 'Invalid journal entry status', expected: validStatuses.join(' | '), actual: String(je.status), code: 'INVALID_STATUS' }); } // Additional validations based on status if (je.status === financial_types_1.JournalEntryStatus.Posted) { if (!je.metadata || !je.metadata.postedAt) { warnings.push('Posted entries should have postedAt timestamp'); } if (!je.metadata || !je.metadata.postedBy) { warnings.push('Posted entries should have postedBy user'); } } const valid = errors.length === 0; const typedValue = valid ? je : undefined; return { valid, value: typedValue, errors, warnings, metadata: { validatedAt: new Date().toISOString(), validatorVersion: '1.0.0', checksPerformed: je.lines.length + 5 } }; } /** * Validate financial user permissions */ function validateFinancialUserPermission(user, requiredPermission, context) { const errors = []; const warnings = []; // Check basic permission const hasPermission = user.permissions.includes(requiredPermission); if (!hasPermission) { errors.push({ path: 'permissions', message: `User lacks required permission: ${requiredPermission}`, expected: requiredPermission, actual: user.permissions.join(', '), code: 'INSUFFICIENT_PERMISSION' }); } // Additional context-based validations if (context) { // Check approval limits if (context.amount && user.approvalLimit && requiredPermission === financial_types_1.FinancialPermission.ApproveJournalEntry) { const limitResult = (0, financial_types_2.subtractMoney)(user.approvalLimit, context.amount); if (limitResult.success && limitResult.data.value < BigInt(0)) { errors.push({ path: 'approvalLimit', message: 'Amount exceeds user approval limit', expected: `<= ${formatMoneyForError(user.approvalLimit)}`, actual: formatMoneyForError(context.amount), code: 'EXCEEDS_APPROVAL_LIMIT' }); } } // Check period status if (context.period.status === 'CLOSED' && !isAdminPermission(requiredPermission)) { errors.push({ path: 'period', message: 'Cannot perform action in closed period', expected: 'OPEN period', actual: 'CLOSED period', code: 'PERIOD_CLOSED' }); } // Check cost center access if (context.accounts && user.costCenters && user.costCenters.length > 0) { warnings.push('Cost center validation not fully implemented'); } } const valid = errors.length === 0; return { valid, value: valid ? hasPermission : undefined, errors, warnings, metadata: { validatedAt: new Date().toISOString(), validatorVersion: '1.0.0', checksPerformed: 3 } }; } // ============================================ // Helper Functions // ============================================ /** * Check if date is valid ISO 8601 */ function isValidISODate(date) { const d = new Date(date); return d instanceof Date && !isNaN(d.getTime()) && d.toISOString().startsWith(date.substring(0, 10)); } /** * Format money for error messages */ function formatMoneyForError(amount) { const divisor = BigInt(10 ** amount.precision); const whole = amount.value / divisor; const fraction = amount.value % divisor; const fractionStr = fraction.toString().padStart(amount.precision, '0'); return `${amount.currency} ${whole}.${fractionStr}`; } /** * Check if permission is admin-level */ function isAdminPermission(permission) { return [ financial_types_1.FinancialPermission.ManageChartOfAccounts, financial_types_1.FinancialPermission.ManageUsers, financial_types_1.FinancialPermission.ClosePeriod, financial_types_1.FinancialPermission.ReopenPeriod ].includes(permission); } // ============================================ // Composite Validators // ============================================ /** * Validate a complete general ledger transaction */ function validateGLTransaction(entry, accounts, period) { const errors = []; const warnings = []; // First validate the journal entry structure const entryResult = validateJournalEntry(entry); if (!entryResult.valid) { return entryResult; } // Validate against chart of accounts for (let i = 0; i < entry.lines.length; i++) { const line = entry.lines[i]; if (!line) continue; const account = accounts.get(line.account); if (!account) { errors.push({ path: `lines[${i}].account`, message: 'Account not found in chart of accounts', expected: 'valid account', actual: line.account, code: 'INVALID_ACCOUNT' }); continue; } // Check account is active if (!account.isActive) { errors.push({ path: `lines[${i}].account`, message: 'Cannot post to inactive account', expected: 'active account', actual: 'inactive account', code: 'INACTIVE_ACCOUNT' }); } // Check currency matches if (account.currency !== line.amount.currency) { errors.push({ path: `lines[${i}].amount.currency`, message: 'Currency must match account currency', expected: account.currency, actual: line.amount.currency, code: 'CURRENCY_MISMATCH' }); } // Validate normal balance (warning only) const normalBalance = (0, financial_types_2.getNormalBalance)(account.type); if (normalBalance !== line.type) { warnings.push(`Line ${i}: ${account.name} normally has ${normalBalance} balance, but entry is ${line.type}`); } } // Validate period if (period.status !== 'OPEN') { errors.push({ path: 'period', message: 'Can only post to open periods', expected: 'OPEN', actual: period.status, code: 'PERIOD_NOT_OPEN' }); } // Validate date is within period const entryDate = new Date(entry.date); const periodStart = new Date(period.startDate); const periodEnd = new Date(period.endDate); if (entryDate < periodStart || entryDate > periodEnd) { errors.push({ path: 'date', message: 'Entry date must be within fiscal period', expected: `${period.startDate} to ${period.endDate}`, actual: entry.date, code: 'DATE_OUTSIDE_PERIOD' }); } const valid = errors.length === 0; return { valid, value: valid ? entry : undefined, errors, warnings, metadata: { validatedAt: new Date().toISOString(), validatorVersion: '1.0.0', checksPerformed: entry.lines.length * 3 + 2 } }; } // ============================================ // Export Financial Validators // ============================================ exports.financialValidators = { amount: validateMonetaryAmount, journalEntry: validateJournalEntry, permission: validateFinancialUserPermission, transaction: validateGLTransaction }; //# sourceMappingURL=financial-validation.js.map