UNPKG

@accounter/server

Version:
205 lines (179 loc) • 7.37 kB
import { endOfDay } from 'date-fns'; import { GraphQLError } from 'graphql'; import type { Maybe, ResolverFn, ResolversParentTypes, ResolversTypes, } from '../../../../../__generated__/types.js'; import { EMPTY_UUID } from '../../../../../shared/constants.js'; import type { Currency } from '../../../../../shared/enums.js'; import { dateToTimelessDateString, formatCurrency } from '../../../../../shared/helpers/index.js'; import type { LedgerProto, TimelessDateString } from '../../../../../shared/types/index.js'; import { AdminContextProvider } from '../../../../admin-context/providers/admin-context.provider.js'; import { ExchangeProvider } from '../../../../exchange-rates/providers/exchange.provider.js'; import { TaxCategoriesProvider } from '../../../../financial-entities/providers/tax-categories.provider.js'; import { businessTransactionsSumFromLedgerRecords } from '../../../../financial-entities/resolvers/business-transactions-sum-from-ledger-records.resolver.js'; import { storeInitialGeneratedRecords } from '../../../helpers/ledgrer-storage.helper.js'; import { generateMiscExpensesLedger } from '../../../helpers/misc-expenses-ledger.helper.js'; import { ledgerProtoToRecordsConverter } from '../../../helpers/utils.helper.js'; export const REVALUATION_LEDGER_DESCRIPTION = 'Revaluation of account'; export const generateLedgerRecordsForExchangeRevaluation: ResolverFn< Maybe<ResolversTypes['GeneratedLedgerRecords']>, ResolversParentTypes['Charge'], GraphQLModules.Context, { insertLedgerRecordsIfNotExists: boolean } > = async (charge, { insertLedgerRecordsIfNotExists }, context, info) => { try { const { injector } = context; if (!charge.user_description) { return { __typename: 'CommonError', message: `Revaluation charge must include user description with designated date`, }; } // get revaluation date - search for "yyyy-mm-dd" in description const dateRegex = /\b(\d{4})-(\d{2})-(\d{2})\b/; const matches = charge.user_description.match(dateRegex); if (!matches?.length) { return { __typename: 'CommonError', message: `Revaluation charge description must include designated date in yyyy-mm-dd format`, }; } const revaluationDate = matches[0] as TimelessDateString; const foreignAccountsPromise = injector .get(TaxCategoriesProvider) .taxCategoryByFinancialAccountOwnerIdsLoader.load(charge.owner_id); const [accountTaxCategories] = await Promise.all([foreignAccountsPromise]); if (accountTaxCategories.length === 0) { throw new Error('No accounts found'); } const { defaultLocalCurrency, general: { taxCategories: { exchangeRevaluationTaxCategoryId }, }, } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); const foreignAccounts = accountTaxCategories.filter( ({ currency }) => currency !== defaultLocalCurrency, ); if (foreignAccounts.length === 0) { throw new Error('No foreign accounts found'); } const currencies = new Set<Currency>( foreignAccounts.map(({ currency }) => currency as Currency), ); const cumulativeDate = dateToTimelessDateString(endOfDay(new Date(revaluationDate))); const accountsCumulativeBalancePromise = businessTransactionsSumFromLedgerRecords( {}, { filters: { ownerIds: [charge.owner_id], businessIDs: foreignAccounts.map(account => account.id), toDate: cumulativeDate, includeRevaluation: false, }, }, context, info, ); const exchangeRates = new Map<Currency, number>(); const [accountsCumulativeBalanceData] = await Promise.all([ accountsCumulativeBalancePromise, ...Array.from(currencies).map(async currency => { const rates = await injector .get(ExchangeProvider) .getExchangeRates(currency, defaultLocalCurrency, new Date(revaluationDate)); exchangeRates.set(currency, rates); }), ]); if (!accountsCumulativeBalanceData || 'message' in accountsCumulativeBalanceData) { throw new GraphQLError('Failed to get accounts transaction-based balance'); } const accountsCumulativeBalance = await Promise.all( accountsCumulativeBalanceData.businessTransactionsSum, ); const errors: Set<string> = new Set(); const entriesPromises: Promise<void>[] = []; const ledgerEntries: LedgerProto[] = []; const foreignAccountsLedger = foreignAccounts.map(async account => { const rate = exchangeRates.get(formatCurrency(account.currency)); if (!rate) { errors.add( `No exchange rate found for date ${revaluationDate}, currency "${account.currency}"`, ); return; } const cumulativeSums = accountsCumulativeBalance.find( balance => balance.businessId === account.id, ); if ( !cumulativeSums || (cumulativeSums[account.currency].total === 0 && cumulativeSums[defaultLocalCurrency].total === 0) ) { return; } const cumulativeLocalBalance = cumulativeSums[defaultLocalCurrency].total; const cumulativeForeignBalance = cumulativeSums[account.currency].total; const revaluationDiff = cumulativeLocalBalance - cumulativeForeignBalance * rate; if (Math.abs(revaluationDiff) < 0.005) { return; } const isCreditorCounterparty = revaluationDiff < 0; const ledgerEntry: LedgerProto = { id: EMPTY_UUID, invoiceDate: new Date(revaluationDate), valueDate: new Date(revaluationDate), currency: defaultLocalCurrency, isCreditorCounterparty, ...(isCreditorCounterparty ? { creditAccountID1: account.id, debitAccountID1: exchangeRevaluationTaxCategoryId, } : { creditAccountID1: exchangeRevaluationTaxCategoryId, debitAccountID1: account.id, }), localCurrencyCreditAmount1: Math.abs(revaluationDiff), localCurrencyDebitAmount1: Math.abs(revaluationDiff), description: `${REVALUATION_LEDGER_DESCRIPTION} "${account.name}"`, ownerId: charge.owner_id, chargeId: charge.id, currencyRate: rate, }; ledgerEntries.push(ledgerEntry); }); entriesPromises.push(...foreignAccountsLedger); // generate ledger from misc expenses const expensesLedgerPromise = generateMiscExpensesLedger(charge, injector).then(entries => { entries.map(entry => { entry.ownerId = charge.owner_id; ledgerEntries.push(entry); }); }); entriesPromises.push(expensesLedgerPromise); await Promise.all(entriesPromises); if (insertLedgerRecordsIfNotExists) { await storeInitialGeneratedRecords(charge.id, ledgerEntries, injector); } return { records: ledgerProtoToRecordsConverter(ledgerEntries), charge, balance: { isBalanced: true, unbalancedEntities: [], balanceSum: 0, financialEntities: [], }, errors: Array.from(errors), }; } catch (e) { return { __typename: 'CommonError', message: `Failed to generate ledger records for charge ID="${charge.id}"\n${e}`, }; } };