UNPKG

@accounter/server

Version:

334 lines (303 loc) • 13.4 kB
import { GraphQLError } from 'graphql'; import { ExchangeProvider } from '@modules/exchange-rates/providers/exchange.provider.js'; import { FinancialAccountsProvider } from '@modules/financial-accounts/providers/financial-accounts.provider.js'; import { TaxCategoriesProvider } from '@modules/financial-entities/providers/tax-categories.provider.js'; import { storeInitialGeneratedRecords } from '@modules/ledger/helpers/ledgrer-storage.helper.js'; import { EmployeesProvider } from '@modules/salaries/providers/employees.provider.js'; import { FundsProvider } from '@modules/salaries/providers/funds.provider.js'; import { SalariesProvider } from '@modules/salaries/providers/salaries.provider.js'; import { TransactionsProvider } from '@modules/transactions/providers/transactions.provider.js'; import { BATCHED_EMPLOYEE_BUSINESS_ID, BATCHED_PENSION_BUSINESS_ID, DEFAULT_LOCAL_CURRENCY, SALARY_BATCHED_BUSINESSES, } from '@shared/constants'; import { Maybe, ResolverFn, ResolversParentTypes, ResolversTypes } from '@shared/gql-types'; import type { LedgerProto, StrictLedgerProto } from '@shared/types'; import { getEntriesFromFeeTransaction, splitFeeTransactions, } from '../../helpers/fee-transactions.js'; import { generateEntriesFromSalaryRecords } from '../../helpers/salary-charge-ledger.helper.js'; import { generatePartialLedgerEntry, getLedgerBalanceInfo, getTaxCategoryNameByAccountCurrency, ledgerProtoToRecordsConverter, updateLedgerBalanceByEntry, ValidateTransaction, validateTransactionRequiredVariables, } from '../../helpers/utils.helper.js'; import { UnbalancedBusinessesProvider } from '../../providers/unbalanced-businesses.provider.js'; import { generateLedgerRecordsForCommonCharge } from './common-ledger-generation.resolver.js'; export const generateLedgerRecordsForSalary: ResolverFn< Maybe<ResolversTypes['GeneratedLedgerRecords']>, ResolversParentTypes['Charge'], GraphQLModules.Context, object > = async (charge, _args, context, _info) => { if (charge.business_array?.length === 1) { // for one business, use the default ledger generation return generateLedgerRecordsForCommonCharge(charge, _args, context, _info); } const chargeId = charge.id; const { injector } = context; try { // validate ledger records are balanced const ledgerBalance = new Map<string, { amount: number; entityId: string }>(); let entriesPromises: Array<Promise<void>> = []; const accountingLedgerEntries: LedgerProto[] = []; // generate ledger from salary records const transactionDate = charge.transactions_min_debit_date ?? charge.transactions_min_event_date; if (!transactionDate) { throw new GraphQLError(`Charge ID="${chargeId}" is missing transaction date`); } // Get relevant data for generation const salaryRecordsPromise = injector .get(SalariesProvider) .getSalaryRecordsByChargeIdLoader.load(chargeId); const transactionsPromise = injector .get(TransactionsProvider) .getTransactionsByChargeIDLoader.load(chargeId); const unbalancedBusinessesPromise = injector .get(UnbalancedBusinessesProvider) .getChargeUnbalancedBusinessesByChargeIds.load(chargeId); const [salaryRecords, transactions, unbalancedBusinesses] = await Promise.all([ salaryRecordsPromise, transactionsPromise, unbalancedBusinessesPromise, ]); // generate ledger from salary records const { entries, monthlyEntriesProto, month } = generateEntriesFromSalaryRecords( salaryRecords, charge, transactionDate, ); const salaryEntriesPromises = entries.map(async ledgerEntry => { accountingLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); }); entriesPromises.push(...salaryEntriesPromises); // generate monthly expenses ledger entries const monthlyEntriesPromises = monthlyEntriesProto .filter(({ amount }) => amount !== 0) .map(async ({ taxCategoryId, amount, isCredit }) => { const taxCategory = await injector .get(TaxCategoriesProvider) .taxCategoryByIDsLoader.load(taxCategoryId); if (!taxCategory) { throw new GraphQLError(`Tax category "${taxCategoryId}" not found`); } const ledgerEntry: LedgerProto = { id: taxCategoryId, invoiceDate: transactionDate, valueDate: transactionDate, currency: DEFAULT_LOCAL_CURRENCY, ...(isCredit ? { creditAccountID1: taxCategoryId } : { debitAccountID1: taxCategoryId }), localCurrencyCreditAmount1: amount, localCurrencyDebitAmount1: amount, description: `${month} salary: ${taxCategory.name}`, isCreditorCounterparty: false, ownerId: charge.owner_id, chargeId, }; accountingLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); }); entriesPromises.push(...monthlyEntriesPromises); const { mainTransactions, feeTransactions } = splitFeeTransactions(transactions); // generate ledger from main transactions const batchedTransactionEntriesMaterials: { transaction: ValidateTransaction; partialEntry: Omit<StrictLedgerProto, 'debitAccountID1' | 'creditAccountID1'>; taxCategoryId: string; }[] = []; // for each common transaction, create a ledger record const financialAccountLedgerEntries: LedgerProto[] = []; const transactionEntriesPromises = mainTransactions.map(async preValidatedTransaction => { const transaction = validateTransactionRequiredVariables(preValidatedTransaction); // preparations for core ledger entries let exchangeRate: number | undefined = undefined; if (transaction.currency !== DEFAULT_LOCAL_CURRENCY) { // get exchange rate for currency exchangeRate = await injector .get(ExchangeProvider) .getExchangeRates( transaction.currency, DEFAULT_LOCAL_CURRENCY, transaction.debit_timestamp, ); } const partialEntry = generatePartialLedgerEntry(transaction, charge.owner_id, exchangeRate); const account = await injector .get(FinancialAccountsProvider) .getFinancialAccountByAccountIDLoader.load(transaction.account_id); if (!account) { throw new GraphQLError(`Transaction ID="${transaction.id}" is missing account`); } const taxCategoryName = getTaxCategoryNameByAccountCurrency(account, transaction.currency); const taxCategory = await injector .get(TaxCategoriesProvider) .taxCategoryByNamesLoader.load(taxCategoryName); if (!taxCategory) { throw new GraphQLError(`Account ID="${account.id}" is missing tax category`); } if (transaction.business_id && SALARY_BATCHED_BUSINESSES.includes(transaction.business_id)) { batchedTransactionEntriesMaterials.push({ transaction, partialEntry, taxCategoryId: taxCategory.id, }); return; } const ledgerEntry: StrictLedgerProto = { ...partialEntry, ...(partialEntry.isCreditorCounterparty ? { creditAccountID1: transaction.business_id, debitAccountID1: taxCategory.id, } : { creditAccountID1: taxCategory.id, debitAccountID1: transaction.business_id, }), }; financialAccountLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); }); entriesPromises.push(...transactionEntriesPromises); await Promise.all(entriesPromises); // for each batched transaction, create a ledger record const batchedLedgerEntries: LedgerProto[] = []; entriesPromises = []; const batchedTransactionEntriesPromises = batchedTransactionEntriesMaterials.map( async ({ transaction, partialEntry, taxCategoryId }) => { const unbatchedBusinesses: Array<string> = []; switch (transaction.business_id) { case BATCHED_EMPLOYEE_BUSINESS_ID: { const employees = await injector .get(EmployeesProvider) .getEmployeesByEmployerLoader.load(charge.owner_id); unbatchedBusinesses.push(...employees.map(({ business_id }) => business_id)); break; } case BATCHED_PENSION_BUSINESS_ID: { const funds = await injector.get(FundsProvider).getAllFunds(); unbatchedBusinesses.push(...funds.map(({ id }) => id)); break; } default: { throw new GraphQLError( `Business ID="${transaction.business_id}" is not supported as batched ID`, ); } } const batchedEntries = accountingLedgerEntries .filter(accountingLedgerEntry => accountingLedgerEntry.creditAccountID1 ? unbatchedBusinesses.includes(accountingLedgerEntry.creditAccountID1) : false, ) .map(accountingLedgerEntry => { const externalPayments = financialAccountLedgerEntries .filter( financialAccountEntry => !!financialAccountEntry.debitAccountID1 && accountingLedgerEntry.creditAccountID1 === financialAccountEntry.debitAccountID1, ) .map(entry => entry.localCurrencyCreditAmount1) .reduce((a, b) => a + b, 0); if (externalPayments === 0) { return accountingLedgerEntry; } if (externalPayments < accountingLedgerEntry.localCurrencyCreditAmount1) { return { ...accountingLedgerEntry, localCurrencyCreditAmount1: accountingLedgerEntry.localCurrencyCreditAmount1 - externalPayments, localCurrencyDebitAmount1: accountingLedgerEntry.localCurrencyDebitAmount1 - externalPayments, }; } return { ...accountingLedgerEntry, localCurrencyCreditAmount1: externalPayments - accountingLedgerEntry.localCurrencyCreditAmount1, creditAccountID1: accountingLedgerEntry.debitAccountID1, localCurrencyDebitAmount1: externalPayments - accountingLedgerEntry.localCurrencyDebitAmount1, debitAccountID1: accountingLedgerEntry.creditAccountID1, }; }); const balance = batchedEntries .map(entry => entry.localCurrencyCreditAmount1) .reduce((a, b) => a + b, 0); if (balance !== partialEntry.localCurrencyCreditAmount1) { throw new GraphQLError( `Batched business ID="${transaction.business_id}" cannot be unbatched as it is not balanced`, ); } for (const batchedEntry of batchedEntries) { const ledgerEntry: StrictLedgerProto = { ...partialEntry, ...(partialEntry.isCreditorCounterparty ? { creditAccountID1: batchedEntry.creditAccountID1!, debitAccountID1: taxCategoryId, } : { creditAccountID1: taxCategoryId, debitAccountID1: batchedEntry.creditAccountID1!, }), creditAmount1: batchedEntry.creditAmount1, localCurrencyCreditAmount1: batchedEntry.localCurrencyCreditAmount1, debitAmount1: batchedEntry.creditAmount1, localCurrencyDebitAmount1: batchedEntry.localCurrencyCreditAmount1, }; batchedLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); } }, ); entriesPromises.push(...batchedTransactionEntriesPromises); // create a ledger record for fee transactions const feeFinancialAccountLedgerEntries: LedgerProto[] = []; const feeFinancialAccountLedgerEntriesPromises = feeTransactions.map(async transaction => { await getEntriesFromFeeTransaction(transaction, charge, context).then(ledgerEntries => { feeFinancialAccountLedgerEntries.push(...ledgerEntries); ledgerEntries.map(ledgerEntry => { updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance); }); }); }); entriesPromises.push(...feeFinancialAccountLedgerEntriesPromises); await Promise.all(entriesPromises); const allowedUnbalancedBusinesses = new Set( unbalancedBusinesses.map(({ business_id }) => business_id), ); const ledgerBalanceInfo = await getLedgerBalanceInfo( injector, ledgerBalance, allowedUnbalancedBusinesses, ); const records = [ ...accountingLedgerEntries, ...financialAccountLedgerEntries, ...batchedLedgerEntries, ...feeFinancialAccountLedgerEntries, ]; await storeInitialGeneratedRecords(charge, records, injector); return { records: ledgerProtoToRecordsConverter(records), charge, balance: ledgerBalanceInfo, }; } catch (e) { return { __typename: 'CommonError', message: `Failed to generate ledger records for charge ID="${chargeId}"\n${e}`, }; } };