@iota-big3/sdk-gateway
Version:
Universal API Gateway with protocol translation, intelligent routing, rate limiting, health checking, and caching
437 lines • 15.2 kB
JavaScript
"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