import { BalanceSheetItems, CompositionBreakdown, TrialBalanceResult, TrialBalanceLine, VarianceComment, ASSET_ITEMS_CONFIG, LIABILITY_ITEMS_CONFIG, ExchangeRateRecord, BRANCHES_LIST } from '../types'; // Default FX Rates table relative to USD export const DEFAULT_FX_RATES: Record = { USD: 1.0, PKR: 278.16, EUR: 0.92, GBP: 0.78, JPY: 155.40, AED: 3.67, SAR: 3.75, BDT: 117.50, CNY: 7.23, HKD: 7.82, CAD: 1.36, }; /** * Server-Side Accounting Engine * Centralizes all Balance Sheet, Trial Balance, FX, and Double-Entry Calculations */ export class AccountingEngine { /** * Compute Total Assets from BalanceSheetItems */ static computeTotalAssets(items: BalanceSheetItems): number { if (!items) return 0; const sum = (items.balancesWithBanks || 0) + (items.balanceCentralBank || 0) + (items.placementHO || 0) + (items.placementNetwork || 0) + (items.placementOutsideNetwork || 0) + (items.tradeAsset || 0) + (items.netAdvances || 0) + (items.investments || 0) + (items.otherAssets || 0); return Number(sum.toFixed(2)); } /** * Compute Total Liabilities from BalanceSheetItems */ static computeTotalLiabilities(items: BalanceSheetItems): number { if (!items) return 0; const sum = (items.fiDepositBanksOnly || 0) + (items.customerDeposit || 0) + (items.depositsNbpNetwork || 0) + (items.borrowingNetwork || 0) + (items.borrowingOutsideNetwork || 0) + (items.borrowingHO || 0) + (items.headOfficeSupportFund || 0) + (items.otherLiabilities || 0); return Number(sum.toFixed(2)); } /** * Compute Net Assets = Total Assets - Total Liabilities */ static computeNetAssets(items: BalanceSheetItems): number { const assets = this.computeTotalAssets(items); const liabilities = this.computeTotalLiabilities(items); return Number((assets - liabilities).toFixed(2)); } /** * Generate a formal Trial Balance with Double-Entry Validation (Debit = Credit) * Chart of Accounts (COA) mapping: * - Asset lines -> Debit balances * - Liability lines -> Credit balances * - Head Office Fund / Net Assets -> Balancing Equity (Credit balance) */ static generateTrialBalance( branchId: string, branchName: string, period: string, items: BalanceSheetItems ): TrialBalanceResult { const lines: TrialBalanceLine[] = []; // Chart of Accounts (COA) Codes const assetCoaMap: Record = { balancesWithBanks: { code: '1010', name: 'Balances with Banks (Onshore/Offshore)' }, balanceCentralBank: { code: '1020', name: 'Balance with Central Bank' }, placementHO: { code: '1110', name: 'Placements - Head Office' }, placementNetwork: { code: '1120', name: 'Placements - Overseas Network' }, placementOutsideNetwork: { code: '1130', name: 'Placements - Inter-Bank Outside Network' }, tradeAsset: { code: '1210', name: 'Trade Finance Assets' }, netAdvances: { code: '1310', name: 'Net Advances & Loans' }, investments: { code: '1410', name: 'Investment Portfolio (HTM/AFS/HFT)' }, otherAssets: { code: '1510', name: 'Other Assets & Sundry Receivables' }, // dummy mappings for liability keys to avoid TS error fiDepositBanksOnly: { code: '', name: '' }, customerDeposit: { code: '', name: '' }, depositsNbpNetwork: { code: '', name: '' }, borrowingNetwork: { code: '', name: '' }, borrowingOutsideNetwork: { code: '', name: '' }, borrowingHO: { code: '', name: '' }, headOfficeSupportFund: { code: '', name: '' }, otherLiabilities: { code: '', name: '' }, fullPledged: { code: '', name: '' }, partialPledged: { code: '', name: '' }, }; const liabilityCoaMap: Record = { fiDepositBanksOnly: { code: '2010', name: 'FI Deposits (Banks Only)' }, customerDeposit: { code: '2020', name: 'Customer Deposits' }, depositsNbpNetwork: { code: '2110', name: 'Deposits from NBP Overseas Network' }, borrowingNetwork: { code: '2210', name: 'Borrowings - NBP Network' }, borrowingOutsideNetwork: { code: '2220', name: 'Borrowings - Inter-Bank Outside Network' }, borrowingHO: { code: '2230', name: 'Borrowings - Head Office' }, headOfficeSupportFund: { code: '2310', name: 'Head Office Capital Support Fund' }, otherLiabilities: { code: '2410', name: 'Other Liabilities & Accrued Provisions' }, // dummy mappings balancesWithBanks: { code: '', name: '' }, balanceCentralBank: { code: '', name: '' }, placementHO: { code: '', name: '' }, placementNetwork: { code: '', name: '' }, placementOutsideNetwork: { code: '', name: '' }, tradeAsset: { code: '', name: '' }, netAdvances: { code: '', name: '' }, investments: { code: '', name: '' }, otherAssets: { code: '', name: '' }, fullPledged: { code: '', name: '' }, partialPledged: { code: '', name: '' }, }; let totalDebit = 0; let totalCredit = 0; // Process Assets (Debit Balance) ASSET_ITEMS_CONFIG.forEach((cfg) => { const val = items[cfg.key] || 0; if (val > 0) { const coa = assetCoaMap[cfg.key]; lines.push({ accountCode: coa.code, accountName: coa.name, type: 'Asset', debitUsdMn: val, creditUsdMn: 0, netBalanceUsdMn: val, }); totalDebit += val; } }); // Process Liabilities (Credit Balance) LIABILITY_ITEMS_CONFIG.forEach((cfg) => { const val = items[cfg.key] || 0; if (val > 0) { const coa = liabilityCoaMap[cfg.key]; lines.push({ accountCode: coa.code, accountName: coa.name, type: 'Liability', debitUsdMn: 0, creditUsdMn: val, netBalanceUsdMn: -val, }); totalCredit += val; } }); // Equity / Retained Earnings Balancing Line (Double Entry Principle) const netAssets = totalDebit - totalCredit; if (Math.abs(netAssets) > 0.0001) { lines.push({ accountCode: '3010', accountName: 'Branch Retained Capital & Reserves Equity', type: 'Equity', debitUsdMn: netAssets < 0 ? Math.abs(netAssets) : 0, creditUsdMn: netAssets > 0 ? netAssets : 0, netBalanceUsdMn: netAssets, }); if (netAssets > 0) { totalCredit += netAssets; } else { totalDebit += Math.abs(netAssets); } } totalDebit = Number(totalDebit.toFixed(2)); totalCredit = Number(totalCredit.toFixed(2)); const diff = Number((totalDebit - totalCredit).toFixed(2)); return { period, branchId, branchName, lines, totalDebitUsdMn: totalDebit, totalCreditUsdMn: totalCredit, differenceUsdMn: diff, isDoubleEntryBalanced: Math.abs(diff) < 0.01, computedAt: new Date().toISOString(), }; } /** * Calculate Variances with Materiality Threshold * Standard Banking Threshold: Default $1.00 Mn or $2.00 Mn */ static calculateVariancesWithMateriality( prevItems: BalanceSheetItems, newItems: BalanceSheetItems, existingComments: VarianceComment[] = [], materialityThresholdUsdMn: number = 1.0 ): { commentsRequired: { lineItemKey: keyof BalanceSheetItems; lineItemLabel: string; previousValue: number; newValue: number; variance: number }[]; autoGeneratedComments: VarianceComment[]; } { const commentsRequired: { lineItemKey: keyof BalanceSheetItems; lineItemLabel: string; previousValue: number; newValue: number; variance: number }[] = []; const autoGeneratedComments: VarianceComment[] = []; const allConfigs = [...ASSET_ITEMS_CONFIG, ...LIABILITY_ITEMS_CONFIG]; allConfigs.forEach((cfg) => { const pVal = prevItems ? prevItems[cfg.key] || 0 : 0; const nVal = newItems ? newItems[cfg.key] || 0 : 0; const variance = nVal - pVal; if (Math.abs(variance) >= materialityThresholdUsdMn) { commentsRequired.push({ lineItemKey: cfg.key, lineItemLabel: cfg.label, previousValue: pVal, newValue: nVal, variance: Number(variance.toFixed(2)), }); const existing = existingComments.find((c) => c.lineItemKey === cfg.key); autoGeneratedComments.push({ lineItemKey: cfg.key, lineItemLabel: cfg.label, previousValue: pVal, newValue: nVal, variance: Number(variance.toFixed(2)), comment: existing?.comment || `Material variance of ${variance > 0 ? '+' : ''}${variance.toFixed(2)} USD Mn observed vs prior period.`, }); } }); return { commentsRequired, autoGeneratedComments }; } /** * FX Currency Conversion * Convert amount from fromCurrency to toCurrency using current rates */ static convertCurrency( amount: number, fromCurrency: string = 'USD', toCurrency: string = 'USD', customRates?: Record ): { amountConverted: number; rateUsed: number } { const rates = { ...DEFAULT_FX_RATES, ...(customRates || {}) }; const fromRateToUsd = rates[fromCurrency.toUpperCase()] || 1.0; const toRateToUsd = rates[toCurrency.toUpperCase()] || 1.0; // First convert to USD const usdAmount = amount / fromRateToUsd; // Then convert to target currency const targetAmount = usdAmount * toRateToUsd; const effectiveCrossRate = toRateToUsd / fromRateToUsd; return { amountConverted: Number(targetAmount.toFixed(4)), rateUsed: Number(effectiveCrossRate.toFixed(4)), }; } /** * Server-Side Financial Input Validator */ static validateSubmissionInputs( items: BalanceSheetItems, compositions?: CompositionBreakdown ): { valid: boolean; errors: string[] } { const errors: string[] = []; if (!items) { return { valid: false, errors: ['Missing balance sheet items object.'] }; } const allConfigs = [...ASSET_ITEMS_CONFIG, ...LIABILITY_ITEMS_CONFIG]; allConfigs.forEach((cfg) => { const val = items[cfg.key]; if (val === undefined || val === null || isNaN(val)) { errors.push(`Field '${cfg.label}' (${cfg.key}) must be a valid number.`); } else if (val < 0) { errors.push(`Field '${cfg.label}' (${cfg.key}) cannot be negative (${val}).`); } else if (val > 100000) { errors.push(`Field '${cfg.label}' (${cfg.key}) exceeds maximum allowable threshold ($100,000 Mn).`); } }); // Validate Compositions sum matching line items if (compositions) { if (compositions.otherAssets && compositions.otherAssets.length > 0) { const compSum = compositions.otherAssets.reduce((s, c) => s + (c.amount || 0), 0); const lineVal = items.otherAssets || 0; if (Math.abs(compSum - lineVal) > 0.05) { errors.push( `Other Assets breakdown total ($${compSum.toFixed(2)} Mn) does not match main line item ($${lineVal.toFixed(2)} Mn).` ); } } if (compositions.otherLiabilities && compositions.otherLiabilities.length > 0) { const compSum = compositions.otherLiabilities.reduce((s, c) => s + (c.amount || 0), 0); const lineVal = items.otherLiabilities || 0; if (Math.abs(compSum - lineVal) > 0.05) { errors.push( `Other Liabilities breakdown total ($${compSum.toFixed(2)} Mn) does not match main line item ($${lineVal.toFixed(2)} Mn).` ); } } } return { valid: errors.length === 0, errors, }; } }