Dominion/src/services/accountingEngine.ts

342 lines
12 KiB
TypeScript
Raw Normal View History

2026-08-07 08:06:39 +00:00
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<string, number> = {
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<keyof BalanceSheetItems, { code: string; name: string }> = {
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<keyof BalanceSheetItems, { code: string; name: string }> = {
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<string, number>
): { 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,
};
}
}