UNPKG

@accounter/server

Version:
329 lines • 18.7 kB
import { AdminContextProvider } from '../../../admin-context/providers/admin-context.provider.js'; import { BankDepositChargesProvider } from '../../../bank-deposits/providers/bank-deposit-charges.provider.js'; import { getChargeBusinesses } from '../../../charges/helpers/common.helper.js'; import { ExchangeProvider } from '../../../exchange-rates/providers/exchange.provider.js'; import { TaxCategoriesProvider } from '../../../financial-entities/providers/tax-categories.provider.js'; import { TransactionsProvider } from '../../../transactions/providers/transactions.provider.js'; import { identifyInterestTransactionIds } from '../../helpers/bank-deposit-ledger-generation.helper.js'; import { ledgerEntryFromMainTransaction } from '../../helpers/common-charge-ledger.helper.js'; import { calculateExchangeRate } from '../../helpers/exchange-ledger.helper.js'; import { convertLedgerRecordToProto, storeInitialGeneratedRecords, } from '../../helpers/ledgrer-storage.helper.js'; import { generateMiscExpensesLedger } from '../../helpers/misc-expenses-ledger.helper.js'; import { getFinancialAccountTaxCategoryId, getLedgerBalanceInfo, LedgerError, ledgerProtoToRecordsConverter, updateLedgerBalanceByEntry, validateTransactionBasicVariables, } from '../../helpers/utils.helper.js'; import { LedgerProvider } from '../../providers/ledger.provider.js'; export const generateLedgerRecordsForBankDeposit = async (charge, { insertLedgerRecordsIfNotExists }, { injector }) => { const chargeId = charge.id; const { ownerId, defaultLocalCurrency, general: { taxCategories: { exchangeRateTaxCategoryId }, }, } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); const errors = new Set(); try { // validate ledger records are balanced const ledgerBalance = new Map(); const transactionsPromise = injector .get(TransactionsProvider) .transactionsByChargeIDLoader.load(chargeId); const bankDepositTransactionsPromise = injector .get(BankDepositChargesProvider) .getDepositTransactionsByChargeId(chargeId); const [transactions, bankDepositTransactions, { mainBusinessId }] = await Promise.all([ transactionsPromise, bankDepositTransactionsPromise, getChargeBusinesses(chargeId, injector), ]); const entriesPromises = []; const financialAccountLedgerEntries = []; const interestLedgerEntries = []; const feeFinancialAccountLedgerEntries = []; let isWithdrawal = false; let isBreathingDeposit = false; // generate ledger from transactions const [mainTransaction, interestTransactions] = transactions.reduce(([mainTransaction, interestTransactions], current) => { if (mainTransaction.id === current.id) { return [mainTransaction, interestTransactions]; } return Number(current.amount) > Number(mainTransaction.amount) ? [current, [mainTransaction, ...interestTransactions]] : [mainTransaction, [current, ...interestTransactions]]; }, [transactions[0], []]); if (Number(mainTransaction.amount) > 0) { isWithdrawal = true; } const mainTransactionPromise = async () => ledgerEntryFromMainTransaction(mainTransaction, injector, chargeId, charge.owner_id, mainBusinessId ?? undefined) .then(ledgerEntry => { financialAccountLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency); }) .catch(e => { if (e instanceof LedgerError) { errors.add(e.message); } else { throw e; } }); // create a ledger record for fee transactions const interestTransactionsPromises = interestTransactions.map(async (transaction) => { // for each transaction, create a ledger record const { currency, valueDate, transactionBusinessId } = validateTransactionBasicVariables(transaction); const businessTaxCategory = await injector .get(TaxCategoriesProvider) .taxCategoryByBusinessIDsLoader.load(transactionBusinessId); if (!businessTaxCategory) { errors.add(`Business ID="${transactionBusinessId}" is missing tax category`); return; } let amount = Number(transaction.amount); let foreignAmount = undefined; if (currency !== defaultLocalCurrency) { // get exchange rate for currency const exchangeRate = await injector .get(ExchangeProvider) .getExchangeRates(currency, defaultLocalCurrency, valueDate); foreignAmount = amount; // calculate amounts in ILS: amount = exchangeRate * amount; } const accountTaxCategoryId = await getFinancialAccountTaxCategoryId(injector, transaction); const isCreditorCounterparty = amount > 0; const ledgerEntry = { id: transaction.id, invoiceDate: transaction.event_date, valueDate, currency, creditAccountID1: isCreditorCounterparty ? businessTaxCategory.id : accountTaxCategoryId, creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, localCurrencyCreditAmount1: Math.abs(amount), debitAccountID1: isCreditorCounterparty ? accountTaxCategoryId : businessTaxCategory.id, debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined, localCurrencyDebitAmount1: Math.abs(amount), description: transaction.source_description ?? undefined, reference: transaction.origin_key, isCreditorCounterparty, ownerId: charge.owner_id, currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined, chargeId, }; interestLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency); }); // generate ledger from misc expenses const expensesLedgerPromise = generateMiscExpensesLedger(charge, injector).then(entries => { entries.map(entry => { entry.ownerId = charge.owner_id; feeFinancialAccountLedgerEntries.push(entry); updateLedgerBalanceByEntry(entry, ledgerBalance, defaultLocalCurrency); }); }); entriesPromises.push(mainTransactionPromise(), ...interestTransactionsPromises, expensesLedgerPromise); await Promise.all(entriesPromises); const miscLedgerEntries = []; const miscLedgerEntriesPromise = async () => { const mainLedgerEntry = financialAccountLedgerEntries[0]; if (!mainLedgerEntry) { throw new LedgerError('Main ledger entry not found'); } const depositTransactions = bankDepositTransactions.filter(t => Number(t.amount) < 0); if (depositTransactions.length === 0 && isWithdrawal) { throw new LedgerError('Deposit transaction not found'); } if (depositTransactions.length > 1) { isBreathingDeposit = true; // Validate consistent currency const depositCurrency = depositTransactions[0].currency; for (const tx of depositTransactions) { if (tx.currency !== depositCurrency) { throw new LedgerError(`Deposit has inconsistent currencies: found ${tx.currency} and ${depositCurrency}`); } } // If currency is local, no revaluation needed if (depositCurrency === defaultLocalCurrency) { return; } // Filter out interest transactions via shared helper const interestTransactionIds = identifyInterestTransactionIds(depositTransactions, { getId: tx => tx.id, getChargeId: tx => tx.charge_id, getAmount: tx => Number(tx.amount), }); const nonInterestTransactions = depositTransactions.filter(tx => !interestTransactionIds.has(tx.id)); // Find latest deposit/withdrawal transaction prior to current const mainTxDate = mainTransaction.debit_date ?? mainTransaction.event_date; const prevTransactions = nonInterestTransactions.filter(tx => { const txDate = tx.debit_date ?? tx.event_date; if (txDate < mainTxDate) { return true; } if (txDate.getTime() === mainTxDate.getTime()) { // Same date: use ID ordering return tx.id < mainTransaction.id; } return false; }); if (prevTransactions.length === 0) { throw new LedgerError('No previous deposit transaction found for revaluation'); } // Sort by date desc, then by id desc to get the latest prevTransactions.sort((a, b) => { const dateA = a.debit_date ?? a.event_date; const dateB = b.debit_date ?? b.event_date; if (dateB.getTime() !== dateA.getTime()) { return dateB.getTime() - dateA.getTime(); } return b.id.localeCompare(a.id); }); const prevAction = prevTransactions[0]; const prevActionDate = prevAction.debit_date ?? prevAction.event_date; // Calculate origin balance at prev action (sum of all non-interest up to and including prev action) let originBalance = 0; for (const tx of nonInterestTransactions) { const txDate = tx.debit_date ?? tx.event_date; if (txDate < prevActionDate || (txDate.getTime() === prevActionDate.getTime() && tx.id <= prevAction.id)) { originBalance += Number(tx.amount); } } // Get exchange rate for prev action and calculate prev action local balance const prevExchangeRate = await injector .get(ExchangeProvider) .getExchangeRates(depositCurrency, defaultLocalCurrency, prevActionDate); const prevLocalBalance = originBalance * prevExchangeRate; // Get current exchange rate const currentExchangeRate = await injector .get(ExchangeProvider) .getExchangeRates(depositCurrency, defaultLocalCurrency, mainLedgerEntry.valueDate); // Calculate revaluation: (origin balance * current rate) - prev local balance const rawRevaluationAmount = originBalance * currentExchangeRate - prevLocalBalance; const revaluationAmount = Math.abs(rawRevaluationAmount); if (revaluationAmount > 0) { const isCreditorCounterparty = rawRevaluationAmount > 0; const mainAccountId = mainLedgerEntry.creditAccountID1; const revaluationLedgerEntry = { id: mainTransaction.id + '|revaluation', creditAccountID1: isCreditorCounterparty ? mainAccountId : exchangeRateTaxCategoryId, localCurrencyCreditAmount1: revaluationAmount, debitAccountID1: isCreditorCounterparty ? exchangeRateTaxCategoryId : mainAccountId, localCurrencyDebitAmount1: revaluationAmount, description: 'Revaluation ledger record', isCreditorCounterparty, invoiceDate: mainLedgerEntry.valueDate, valueDate: mainLedgerEntry.valueDate, currency: defaultLocalCurrency, ownerId: mainLedgerEntry.ownerId, chargeId, }; miscLedgerEntries.push(revaluationLedgerEntry); updateLedgerBalanceByEntry(revaluationLedgerEntry, ledgerBalance, defaultLocalCurrency); } } else if (isWithdrawal) { const mainBusinessBalance = mainTransaction.business_id ? ledgerBalance.get(mainTransaction.business_id) : undefined; const mainBusinessBalanceByCurrency = mainTransaction.currency === defaultLocalCurrency ? mainBusinessBalance?.amount : mainBusinessBalance?.foreignAmounts?.[mainTransaction.currency]?.foreign; const mainBusinessAbsBalance = mainBusinessBalanceByCurrency ? Math.abs(mainBusinessBalanceByCurrency) : undefined; const initialDepositTransaction = depositTransactions[0]; const depositTransactionAmount = Math.abs(Number(initialDepositTransaction.amount)); if (mainBusinessAbsBalance !== depositTransactionAmount || mainTransaction.currency !== initialDepositTransaction.currency) { throw new LedgerError('Deposit transaction does not match the withdrawal transaction'); } const depositLedgerRecords = await injector .get(LedgerProvider) .getLedgerRecordsByChargesIdLoader.load(initialDepositTransaction.charge_id); if (depositLedgerRecords.length !== 1) { if (depositLedgerRecords.length === 0) { throw new LedgerError('Deposit ledger record not found'); } throw new LedgerError('Multiple deposit ledger records found'); } const depositLedgerRecord = depositLedgerRecords[0]; if (Number.isNaN(depositLedgerRecord.credit_local_amount1)) { throw new LedgerError('Deposit ledger record has invalid local amount'); } if (depositLedgerRecord.debit_entity1) { updateLedgerBalanceByEntry(convertLedgerRecordToProto(depositLedgerRecord, ownerId), ledgerBalance, defaultLocalCurrency); } if (mainTransaction.currency !== defaultLocalCurrency) { const rawAmount = Number(depositLedgerRecord.credit_local_amount1) - mainLedgerEntry.localCurrencyCreditAmount1; const amount = Math.abs(rawAmount); const isCreditorCounterparty = rawAmount > 0; const mainAccountId = mainLedgerEntry.creditAccountID1; // validate exchange rate try { const exchangeAmount = calculateExchangeRate(mainLedgerEntry.creditAccountID1, [ ...financialAccountLedgerEntries, convertLedgerRecordToProto(depositLedgerRecord, ownerId), ], defaultLocalCurrency); if ((!exchangeAmount && !!amount) || (exchangeAmount && Math.abs(exchangeAmount) !== amount)) { throw new LedgerError('Exchange rate error'); } } catch (e) { errors.add(e.message); } const exchangeLedgerEntry = { id: mainTransaction.id + '|fee', // NOTE: this field is dummy creditAccountID1: isCreditorCounterparty ? mainAccountId : exchangeRateTaxCategoryId, localCurrencyCreditAmount1: amount, debitAccountID1: isCreditorCounterparty ? exchangeRateTaxCategoryId : mainAccountId, localCurrencyDebitAmount1: amount, description: 'Exchange ledger record', isCreditorCounterparty, invoiceDate: mainLedgerEntry.valueDate, valueDate: mainLedgerEntry.valueDate, currency: defaultLocalCurrency, ownerId: mainLedgerEntry.ownerId, chargeId, }; miscLedgerEntries.push(exchangeLedgerEntry); updateLedgerBalanceByEntry(exchangeLedgerEntry, ledgerBalance, defaultLocalCurrency); } } return; }; await miscLedgerEntriesPromise().catch(e => { if (e instanceof LedgerError) { errors.add(e.message); } else { throw e; } }); const records = [ ...financialAccountLedgerEntries, ...interestLedgerEntries, ...feeFinancialAccountLedgerEntries, ...miscLedgerEntries, ]; if (insertLedgerRecordsIfNotExists) { await storeInitialGeneratedRecords(charge.id, records, injector); } const allowedUnbalancedBusinesses = new Set(); const mainLedgerEntry = financialAccountLedgerEntries[0]; if (!mainLedgerEntry.isCreditorCounterparty) { allowedUnbalancedBusinesses.add(mainLedgerEntry.debitAccountID1); } if (mainBusinessId && isBreathingDeposit) { allowedUnbalancedBusinesses.add(mainBusinessId); } const ledgerBalanceInfo = await getLedgerBalanceInfo(injector, ledgerBalance, errors, allowedUnbalancedBusinesses); return { records: ledgerProtoToRecordsConverter(records), charge, balance: ledgerBalanceInfo, errors: Array.from(errors), }; } catch (e) { return { __typename: 'CommonError', message: `Failed to generate ledger records for charge ID="${chargeId}"\n${e}`, }; } }; //# sourceMappingURL=bank-deposit-ledger-generation.resolver.js.map