UNPKG

@accounter/server

Version:
291 lines • 15.3 kB
import { DIVIDEND_WITHHOLDING_TAX_PERCENTAGE } from '../../../../shared/constants.js'; import { AdminContextProvider } from '../../../admin-context/providers/admin-context.provider.js'; import { DividendsProvider } from '../../../dividends/providers/dividends.provider.js'; import { ExchangeProvider } from '../../../exchange-rates/providers/exchange.provider.js'; import { TransactionsProvider } from '../../../transactions/providers/transactions.provider.js'; import { ledgerEntryFromBalanceCancellation } from '../../helpers/common-charge-ledger.helper.js'; import { splitDividendTransactions } from '../../helpers/dividend-ledger.helper.js'; import { getEntriesFromFeeTransaction } from '../../helpers/fee-transactions.js'; import { storeInitialGeneratedRecords } from '../../helpers/ledgrer-storage.helper.js'; import { generateMiscExpensesLedger } from '../../helpers/misc-expenses-ledger.helper.js'; import { generatePartialLedgerEntry, getFinancialAccountTaxCategoryId, getLedgerBalanceInfo, LedgerError, ledgerProtoToRecordsConverter, updateLedgerBalanceByEntry, validateTransactionRequiredVariables, } from '../../helpers/utils.helper.js'; import { BalanceCancellationProvider } from '../../providers/balance-cancellation.provider.js'; export const generateLedgerRecordsForDividend = async (charge, { insertLedgerRecordsIfNotExists }, { injector }) => { const { defaultLocalCurrency, dividends } = await injector .get(AdminContextProvider) .getVerifiedAdminContext(); const { dividendWithholdingTaxBusinessId, dividendTaxCategoryId } = dividends; if (!dividendWithholdingTaxBusinessId) { return { __typename: 'CommonError', message: `Dividend withholding tax business ID is not defined`, }; } if (!dividendTaxCategoryId) { return { __typename: 'CommonError', message: `Dividend tax category ID is not defined`, }; } const chargeId = charge.id; const errors = new Set(); try { // validate ledger records are balanced const ledgerBalance = new Map(); // generate ledger from transactions const coreLedgerEntries = []; const withholdingTaxLedgerEntries = []; const paymentLedgerEntries = []; const miscLedgerEntries = []; let mainAccountId = undefined; // Get all transactions const transactionsPromise = injector .get(TransactionsProvider) .transactionsByChargeIDLoader.load(chargeId); const chargeBallanceCancellationsPromise = injector .get(BalanceCancellationProvider) .getBalanceCancellationByChargesIdLoader.load(chargeId); const [transactions, balanceCancellations] = await Promise.all([ transactionsPromise, chargeBallanceCancellationsPromise, ]); const { withholdingTaxTransactions, paymentsTransactions, feeTransactions, errors: splitErrors, } = splitDividendTransactions(transactions, dividends); splitErrors.map(errors.add); // create a ledger record for tax deduction origin const withholdingTaxTransactionsPromises = withholdingTaxTransactions.map(async (preValidatedTransaction) => { try { const transaction = validateTransactionRequiredVariables(preValidatedTransaction); if (transaction.currency !== defaultLocalCurrency) { errors.add(`Withholding tax currency supposed to be local, got ${transaction.currency}`); } // get tax category const financialAccountTaxCategoryId = await getFinancialAccountTaxCategoryId(injector, transaction); // set main account for dividend mainAccountId ||= financialAccountTaxCategoryId; if (mainAccountId !== financialAccountTaxCategoryId) { errors.add(`Tax category is not consistent`); } const partialEntry = generatePartialLedgerEntry(transaction, charge.owner_id, undefined); const ledgerEntry = { ...partialEntry, creditAccountID1: partialEntry.isCreditorCounterparty ? transaction.business_id : financialAccountTaxCategoryId, debitAccountID1: partialEntry.isCreditorCounterparty ? financialAccountTaxCategoryId : transaction.business_id, }; withholdingTaxLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency); } catch (e) { if (e instanceof LedgerError) { errors.add(e.message); } else { throw e; } } }); const dividendRecordsPromise = injector .get(DividendsProvider) .getDividendsByChargeIdLoader.load(chargeId); const [dividendRecords] = await Promise.all([ dividendRecordsPromise, ...withholdingTaxTransactionsPromises, ]); const allowedUnbalancedBusinesses = new Set(); // create a ledger record for dividend payments const coreLedgerEntriesPromises = paymentsTransactions.map(async (preValidatedTransaction) => { try { const dividendRecord = dividendRecords.find(record => record.transaction_id === preValidatedTransaction.id); // run validations if (!dividendRecord) { throw new LedgerError(`Transaction reference "${preValidatedTransaction.source_reference}" is missing matching dividend record`); } const transaction = validateTransactionRequiredVariables(preValidatedTransaction); if (Number(transaction.amount) >= 0) { throw new LedgerError(`Dividend transaction amount cannot be positive (reference: ${transaction.source_reference})`); } if (Number(dividendRecord.amount) <= 0) { throw new LedgerError(`Dividend amount is not positive (ID: ${dividendRecord.id})`); } if (charge.owner_id !== dividendRecord.owner_id) { throw new LedgerError(`Transaction reference "${transaction.source_reference}" is not matching dividend record ID="${dividendRecord.id}"`); } const withholdingTaxPercentage = dividendRecord.withholding_tax_percentage_override ? Number(dividendRecord.withholding_tax_percentage_override) : DIVIDEND_WITHHOLDING_TAX_PERCENTAGE; const dividendRecordAbsAmount = Math.abs(Number(dividendRecord.amount)); // generate core ledger entries const coreLedgerEntry = { currency: defaultLocalCurrency, ownerId: dividendRecord.owner_id, id: dividendRecord.id, chargeId, invoiceDate: dividendRecord.date, valueDate: dividendRecord.date, isCreditorCounterparty: false, description: 'Main dividend record', debitAccountID1: dividendTaxCategoryId, localCurrencyDebitAmount1: dividendRecordAbsAmount, creditAccountID1: dividendRecord.business_id, localCurrencyCreditAmount1: dividendRecordAbsAmount * (1 - withholdingTaxPercentage), creditAccountID2: dividendWithholdingTaxBusinessId, localCurrencyCreditAmount2: dividendRecordAbsAmount * withholdingTaxPercentage, }; coreLedgerEntries.push(coreLedgerEntry); updateLedgerBalanceByEntry(coreLedgerEntry, ledgerBalance, defaultLocalCurrency); // preparations for payment ledger entries let exchangeRate = undefined; if (transaction.currency !== defaultLocalCurrency) { // get exchange rate for currency exchangeRate = await injector .get(ExchangeProvider) .getExchangeRates(transaction.currency, defaultLocalCurrency, dividendRecord.date); } const partialEntry = generatePartialLedgerEntry(transaction, charge.owner_id, exchangeRate); const isForeignCurrency = partialEntry.currency !== defaultLocalCurrency; const amountDiff = partialEntry.localCurrencyCreditAmount1 - Number(dividendRecord.amount) * (1 - withholdingTaxPercentage); if (Math.abs(amountDiff) > 0.005 && !isForeignCurrency) { throw new LedgerError(`Transaction reference "${transaction.source_reference}" and dividend record ID="${dividendRecord.id}" amounts mismatch`); } const foreignAccountTaxCategoryId = await getFinancialAccountTaxCategoryId(injector, transaction); // create payment ledger entries const paymentEntry = { ...partialEntry, isCreditorCounterparty: false, creditAccountID1: foreignAccountTaxCategoryId, debitAccountID1: dividendRecord.business_id, }; paymentLedgerEntries.push(paymentEntry); updateLedgerBalanceByEntry(paymentEntry, ledgerBalance, defaultLocalCurrency); } catch (e) { if (e instanceof LedgerError) { errors.add(e.message); } else { throw e; } } }); // create a ledger record for fee transactions const feeFinancialAccountLedgerEntries = []; const feeTransactionsPromises = feeTransactions.map(async (transaction) => { await getEntriesFromFeeTransaction(transaction, charge, injector) .then(ledgerEntries => { feeFinancialAccountLedgerEntries.push(...ledgerEntries); ledgerEntries.map(ledgerEntry => { updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency); }); }) .catch(e => { if (e instanceof LedgerError) { errors.add(e.message); } else { throw e; } }); }); const entriesPromises = [...feeTransactionsPromises, ...coreLedgerEntriesPromises]; // generate ledger from misc expenses const expensesLedgerPromise = generateMiscExpensesLedger(charge, injector) .then(entries => { entries.map(entry => { entry.ownerId = charge.owner_id; miscLedgerEntries.push(entry); updateLedgerBalanceByEntry(entry, ledgerBalance, defaultLocalCurrency); }); }) .catch(e => { if (e instanceof LedgerError) { errors.add(e.message); } else { errors.add(`Failed to generate misc expenses ledger: ${e.message}`); } }); entriesPromises.push(expensesLedgerPromise); await Promise.all(entriesPromises); // create foreign currency balance ledger for (const entry of paymentLedgerEntries) { if (entry.currency !== defaultLocalCurrency) { const currencyBalanceEntry1 = { id: entry.id + `|${entry.currency}-balance`, // NOTE: this field is dummy creditAccountID1: entry.debitAccountID1, localCurrencyCreditAmount1: entry.localCurrencyCreditAmount1, localCurrencyDebitAmount1: entry.localCurrencyCreditAmount1, creditAmount1: entry.creditAmount1, description: `Foreign currency balance (${entry.currency})`, isCreditorCounterparty: true, invoiceDate: entry.invoiceDate, valueDate: entry.valueDate, currency: entry.currency, ownerId: entry.ownerId, chargeId: charge.id, }; miscLedgerEntries.push(currencyBalanceEntry1); updateLedgerBalanceByEntry(currencyBalanceEntry1, ledgerBalance, defaultLocalCurrency); const currencyBalanceEntry2 = { id: entry.id + `|${defaultLocalCurrency}-balance`, // NOTE: this field is dummy debitAccountID1: entry.debitAccountID1, localCurrencyCreditAmount1: entry.localCurrencyCreditAmount1, localCurrencyDebitAmount1: entry.localCurrencyCreditAmount1, description: `Foreign currency balance (${defaultLocalCurrency})`, isCreditorCounterparty: false, invoiceDate: entry.invoiceDate, valueDate: entry.valueDate, currency: defaultLocalCurrency, ownerId: entry.ownerId, chargeId: charge.id, }; miscLedgerEntries.push(currencyBalanceEntry2); updateLedgerBalanceByEntry(currencyBalanceEntry2, ledgerBalance, defaultLocalCurrency); } } // generate ledger from balance cancellation for (const balanceCancellation of balanceCancellations) { try { const ledgerEntry = await ledgerEntryFromBalanceCancellation(balanceCancellation, ledgerBalance, coreLedgerEntries, chargeId, charge.owner_id, injector); miscLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency); } catch (e) { if (e instanceof LedgerError) { errors.add(e.message); continue; } else { throw e; } } } const ledgerBalanceInfo = await getLedgerBalanceInfo(injector, ledgerBalance, errors, allowedUnbalancedBusinesses); const records = [ ...withholdingTaxLedgerEntries, ...coreLedgerEntries, ...feeFinancialAccountLedgerEntries, ...paymentLedgerEntries, ...miscLedgerEntries, ]; if (insertLedgerRecordsIfNotExists) { await storeInitialGeneratedRecords(charge.id, records, injector); } 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=dividend-ledger-generation.resolver.js.map