UNPKG

@accounter/server

Version:
193 lines • 10.6 kB
import { format } from 'date-fns'; import { dateToTimelessDateString, getMonthFromDescription, } from '../../../../shared/helpers/index.js'; import { AdminContextProvider } from '../../../admin-context/providers/admin-context.provider.js'; import { getChargeTransactionsMeta } from '../../../charges/helpers/common.helper.js'; import { ExchangeProvider } from '../../../exchange-rates/providers/exchange.provider.js'; import { getVatRecords } from '../../../reports/resolvers/get-vat-records.resolver.js'; import { TransactionsProvider } from '../../../transactions/providers/transactions.provider.js'; import { storeInitialGeneratedRecords } from '../../helpers/ledgrer-storage.helper.js'; import { generateMiscExpensesLedger } from '../../helpers/misc-expenses-ledger.helper.js'; import { getVatDataFromVatReportRecords } from '../../helpers/monthly-vat-ledger-generation.helper.js'; import { generatePartialLedgerEntry, getFinancialAccountTaxCategoryId, getLedgerBalanceInfo, LedgerError, ledgerProtoToRecordsConverter, updateLedgerBalanceByEntry, validateTransactionRequiredVariables, } from '../../helpers/utils.helper.js'; import { UnbalancedBusinessesProvider } from '../../providers/unbalanced-businesses.provider.js'; export const generateLedgerRecordsForMonthlyVat = async (charge, { insertLedgerRecordsIfNotExists }, { injector }, __) => { const chargeId = charge.id; const errors = new Set(); try { const { defaultLocalCurrency, authorities: { vatBusinessId, inputVatTaxCategoryId, outputVatTaxCategoryId, propertyOutputVatTaxCategoryId, }, general: { taxCategories: { balanceCancellationTaxCategoryId }, }, } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); // figure out VAT month const { transactionsMinDebitDate, transactionsMinEventDate } = await getChargeTransactionsMeta(chargeId, injector); const transactionDate = transactionsMinDebitDate ?? transactionsMinEventDate ?? undefined; if (!charge.user_description) { errors.add(`Monthly VAT charge must have description that indicates it's month (ID="${chargeId}")`); } const vatDates = charge.user_description ? getMonthFromDescription(charge.user_description, transactionDate) : null; if (!vatDates?.length) { errors.add(`Cannot extract charge ID="${chargeId}" VAT month`); } // get VAT relevant records const vatRecordsPromises = (vatDates ?? []).map(async (vatDate) => { const [year, month] = (vatDate ?? format(new Date(), 'yyyy-MM')).split('-').map(Number); const ledgerDate = new Date(year, month, 0); const monthDate = dateToTimelessDateString(new Date(year, month - 1, 15)); const { income, expenses } = await getVatRecords({ filters: { financialEntityId: charge.owner_id, monthDate } }, injector); return { income, expenses, ledgerDate, vatDate, }; }); const transactionsPromise = injector .get(TransactionsProvider) .transactionsByChargeIDLoader.load(chargeId); const unbalancedBusinessesPromise = injector .get(UnbalancedBusinessesProvider) .getChargeUnbalancedBusinessesByChargeIds.load(chargeId); const [vatRecords, transactions, unbalancedBusinesses] = await Promise.all([ Promise.all(vatRecordsPromises), transactionsPromise, unbalancedBusinessesPromise, ]); const accountingLedgerEntries = []; const financialAccountLedgerEntries = []; const miscExpensesLedgerEntries = []; const ledgerBalance = new Map(); // for each transaction, create a ledger record const mainTransactionsPromises = transactions.map(async (preValidatedTransaction) => { try { const transaction = validateTransactionRequiredVariables(preValidatedTransaction); if (transaction.currency !== defaultLocalCurrency) { throw new LedgerError(`Monthly VAT currency supposed to be local, got ${transaction.currency}`); } // preparations for core ledger entries let exchangeRate = undefined; if (transaction.currency !== defaultLocalCurrency) { // get exchange rate for currency exchangeRate = await injector .get(ExchangeProvider) .getExchangeRates(transaction.currency, defaultLocalCurrency, transaction.debit_timestamp); } const partialEntry = generatePartialLedgerEntry(transaction, charge.owner_id, exchangeRate); const financialAccountTaxCategoryId = await getFinancialAccountTaxCategoryId(injector, transaction); const ledgerEntry = { ...partialEntry, ...(partialEntry.isCreditorCounterparty ? { creditAccountID1: vatBusinessId, debitAccountID1: financialAccountTaxCategoryId, } : { creditAccountID1: financialAccountTaxCategoryId, debitAccountID1: vatBusinessId, }), }; financialAccountLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency); } catch (e) { if (e instanceof LedgerError) { errors.add(e.message); } else { throw e; } } }); const miscExpensesLedgerPromise = generateMiscExpensesLedger(charge, injector).then(entries => { entries.map(entry => { entry.ownerId = charge.owner_id; miscExpensesLedgerEntries.push(entry); updateLedgerBalanceByEntry(entry, ledgerBalance, defaultLocalCurrency); }); }); const isProperty = charge.is_property; if (isProperty && !propertyOutputVatTaxCategoryId) { console.error(`Charge ID="${chargeId}" is marked as property but no Property Output VAT tax category is configured in admin context`); return { __typename: 'CommonError', message: `One of the charges is marked as property but no Property Output VAT tax category is configured in admin context`, }; } await Promise.all([...mainTransactionsPromises, miscExpensesLedgerPromise]); for (const { income, expenses, ledgerDate, vatDate } of vatRecords) { const [incomeVat, roundedIncomeVat] = getVatDataFromVatReportRecords(income); const [expensesVat, roundedExpensesVat] = getVatDataFromVatReportRecords(expenses); const accountingLedgerMaterials = []; accountingLedgerMaterials.push({ amount: roundedIncomeVat, taxCategoryId: inputVatTaxCategoryId, counterpartyId: vatBusinessId, }, { amount: roundedExpensesVat * -1, taxCategoryId: isProperty ? propertyOutputVatTaxCategoryId : outputVatTaxCategoryId, counterpartyId: vatBusinessId, }); const roundedIncomeVatDiff = Math.round((roundedIncomeVat - incomeVat) * 100) / 100; if (roundedIncomeVatDiff) { accountingLedgerMaterials.push({ amount: roundedIncomeVatDiff * -1, taxCategoryId: inputVatTaxCategoryId, counterpartyId: balanceCancellationTaxCategoryId, }); } const roundedExpensesVatDiff = Math.round((roundedExpensesVat - expensesVat) * 100) / 100; if (roundedExpensesVatDiff) { accountingLedgerMaterials.push({ amount: roundedExpensesVatDiff, taxCategoryId: isProperty ? propertyOutputVatTaxCategoryId : outputVatTaxCategoryId, counterpartyId: balanceCancellationTaxCategoryId, }); } accountingLedgerMaterials.map(({ amount, taxCategoryId, counterpartyId }) => { if (amount === 0) { return; } const isCreditorCounterparty = amount > 0; const ledgerProto = { id: chargeId, invoiceDate: ledgerDate, valueDate: ledgerDate, currency: defaultLocalCurrency, creditAccountID1: isCreditorCounterparty ? counterpartyId : taxCategoryId, localCurrencyCreditAmount1: Math.abs(amount), debitAccountID1: isCreditorCounterparty ? taxCategoryId : counterpartyId, localCurrencyDebitAmount1: Math.abs(amount), description: counterpartyId === balanceCancellationTaxCategoryId ? 'Balance cancellation' : `VAT command ${vatDate}`, isCreditorCounterparty, ownerId: charge.owner_id, chargeId, }; accountingLedgerEntries.push(ledgerProto); updateLedgerBalanceByEntry(ledgerProto, ledgerBalance, defaultLocalCurrency); }); } const allowedUnbalancedBusinesses = new Set(unbalancedBusinesses.map(({ business_id }) => business_id)); const ledgerBalanceInfo = await getLedgerBalanceInfo(injector, ledgerBalance, errors, allowedUnbalancedBusinesses); const records = [ ...financialAccountLedgerEntries, ...accountingLedgerEntries, ...miscExpensesLedgerEntries, ]; 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=monthly-vat-ledger-generation.resolver.js.map