UNPKG

@accounter/server

Version:
438 lines • 24.5 kB
import { AdminContextProvider } from '../../../admin-context/providers/admin-context.provider.js'; import { validateTransactionAgainstBusinessTrips } from '../../../business-trips/helpers/business-trips-expenses.helper.js'; import { BusinessTripAttendeesProvider } from '../../../business-trips/providers/business-trips-attendees.provider.js'; import { BusinessTripEmployeePaymentsProvider } from '../../../business-trips/providers/business-trips-employee-payments.provider.js'; import { BusinessTripExpensesProvider } from '../../../business-trips/providers/business-trips-expenses.provider.js'; import { BusinessTripsProvider } from '../../../business-trips/providers/business-trips.provider.js'; import { calculateTotalAmount, getChargeBusinesses, getChargeDocumentsMeta, getChargeTaxCategoryId, } from '../../../charges/helpers/common.helper.js'; import { DocumentsProvider } from '../../../documents/providers/documents.provider.js'; import { ExchangeProvider } from '../../../exchange-rates/providers/exchange.provider.js'; import { BusinessesProvider } from '../../../financial-entities/providers/businesses.provider.js'; import { TransactionsProvider } from '../../../transactions/providers/transactions.provider.js'; import { getExchangeDates, ledgerEntryFromDocument, } from '../../helpers/common-charge-ledger.helper.js'; import { handleCrossYearLedgerEntries } from '../../helpers/cross-year-ledger.helper.js'; import { validateExchangeRate } from '../../helpers/exchange-ledger.helper.js'; import { getEntriesFromFeeTransaction, splitFeeTransactions, } 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, multipleForeignCurrenciesBalanceEntries, updateLedgerBalanceByEntry, validateTransactionRequiredVariables, } from '../../helpers/utils.helper.js'; import { UnbalancedBusinessesProvider } from '../../providers/unbalanced-businesses.provider.js'; const AMOUNT_REQUIRING_DOCUMENT = 1000; const AMOUNT_FOR_SELF_CLOSING = 300; export const generateLedgerRecordsForBusinessTrip = async (charge, { insertLedgerRecordsIfNotExists }, { injector }) => { const chargeId = charge.id; const { defaultLocalCurrency, defaultTaxCategoryId, general: { taxCategories: { incomeExchangeRateTaxCategoryId }, }, } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); const errors = new Set(); try { const tripTaxCategory = await getChargeTaxCategoryId(charge.id, injector); if (!tripTaxCategory) { errors.add(`Business trip is missing tax category`); } // validate ledger records are balanced const ledgerBalance = new Map(); const [{ invoiceCount, receiptCount }, formattedChargeAmount] = await Promise.all([ getChargeDocumentsMeta(chargeId, injector), calculateTotalAmount(chargeId, injector, defaultLocalCurrency), ]); const gotRelevantDocuments = invoiceCount + receiptCount > 0; // Get all transactions and business trip transactions const transactionsPromise = injector .get(TransactionsProvider) .transactionsByChargeIDLoader.load(chargeId); const documentsPromise = injector .get(DocumentsProvider) .getDocumentsByChargeIdLoader.load(chargeId); const documentsTaxCategoryIdPromise = async () => { if (charge.tax_category_id) { return charge.tax_category_id; } const taxCategoryId = await getChargeTaxCategoryId(chargeId, injector); if (taxCategoryId) { return taxCategoryId; } if (!gotRelevantDocuments) { return undefined; } errors.add('Tax category not found'); return defaultTaxCategoryId; }; const businessTripExpensesPromise = injector .get(BusinessTripsProvider) .getBusinessTripsByChargeIdLoader.load(chargeId) .then(businessTrip => { if (businessTrip) { return injector .get(BusinessTripExpensesProvider) .getBusinessTripsExpensesByBusinessTripIdLoader.load(businessTrip.id); } return []; }); const businessTripsEmployeePaymentsPromise = injector .get(BusinessTripEmployeePaymentsProvider) .getBusinessTripEmployeePaymentsByChargeIdLoader.load(chargeId); const businessTripAttendeesPromise = injector .get(BusinessTripAttendeesProvider) .getBusinessTripsAttendeesByChargeIdLoader.load(chargeId); const unbalancedBusinessesPromise = injector .get(UnbalancedBusinessesProvider) .getChargeUnbalancedBusinessesByChargeIds.load(chargeId); const [transactions, documents, documentsTaxCategoryId, businessTripExpenses, businessTripsEmployeePayments, businessTripAttendees, chargeUnbalancedBusinesses, { mainBusinessId },] = await Promise.all([ transactionsPromise, documentsPromise, documentsTaxCategoryIdPromise(), businessTripExpensesPromise, businessTripsEmployeePaymentsPromise, businessTripAttendeesPromise, unbalancedBusinessesPromise, getChargeBusinesses(chargeId, injector), ]); // generate ledger from transactions const entriesPromises = []; const financialAccountLedgerEntries = []; const feeFinancialAccountLedgerEntries = []; const accountingLedgerEntries = []; const dates = new Set(); const currencies = new Set(); // generate ledger from transactions const { mainTransactions, feeTransactions } = splitFeeTransactions(transactions); let isSelfClosingLedger = mainTransactions.length === 1; let transactionsTotalLocalAmount = 0; // create ledger records for misc expenses const miscExpensesLedgerPromise = generateMiscExpensesLedger(charge, injector).then(entries => { entries.map(entry => { entry.ownerId = charge.owner_id; feeFinancialAccountLedgerEntries.push(entry); updateLedgerBalanceByEntry(entry, ledgerBalance, defaultLocalCurrency); dates.add(entry.valueDate.getTime()); currencies.add(entry.currency); }); }); // for each transaction, create a ledger record const mainTransactionsPromises = mainTransactions.map(async (preValidatedTransaction) => { try { const transaction = validateTransactionRequiredVariables(preValidatedTransaction, { skipBusinessId: isSelfClosingLedger, }); if (transaction.business_id) { isSelfClosingLedger = false; } const validateTransactionAgainstBusinessTripsPromise = validateTransactionAgainstBusinessTrips(injector, transaction).catch(e => { if (e instanceof LedgerError) { errors.add(e.message); } else { throw e; } }); const ledgerEntryPromise = async () => { // get tax category const accountTaxCategoryId = await getFinancialAccountTaxCategoryId(injector, transaction); // 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); transactionsTotalLocalAmount += (partialEntry.isCreditorCounterparty ? 1 : -1) * partialEntry.localCurrencyCreditAmount1; isSelfClosingLedger &&= partialEntry.localCurrencyCreditAmount1 < AMOUNT_FOR_SELF_CLOSING; const entryBusinessId = (isSelfClosingLedger ? tripTaxCategory : null) ?? transaction.business_id; if (!entryBusinessId) { throw new LedgerError(`Transaction reference "${transaction.source_reference}" is missing business ID`); } const ledgerEntry = { ...partialEntry, creditAccountID1: partialEntry.isCreditorCounterparty ? entryBusinessId : accountTaxCategoryId, debitAccountID1: partialEntry.isCreditorCounterparty ? accountTaxCategoryId : entryBusinessId, }; return ledgerEntry; }; const [ledgerEntry] = await Promise.all([ ledgerEntryPromise(), validateTransactionAgainstBusinessTripsPromise, ]); financialAccountLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency); dates.add(ledgerEntry.valueDate.getTime()); currencies.add(ledgerEntry.currency); } catch (e) { if (e instanceof LedgerError) { errors.add(e.message); } else { throw e; } } }); // create a ledger record for fee transactions const feeTransactionsPromises = feeTransactions.map(async (transaction) => { const ledgerEntries = await getEntriesFromFeeTransaction(transaction, charge, injector).catch(e => { if (e instanceof LedgerError) { errors.add(e.message); } else { throw e; } }); if (!ledgerEntries) { return; } feeFinancialAccountLedgerEntries.push(...ledgerEntries); ledgerEntries.map(ledgerEntry => { updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency); dates.add(ledgerEntry.valueDate.getTime()); currencies.add(ledgerEntry.currency); }); }); // generate ledger from documents if (gotRelevantDocuments) { // Get all relevant documents for charge const relevantDocuments = documents.filter(d => ['INVOICE', 'INVOICE_RECEIPT', 'CREDIT_INVOICE'].includes(d.type)); // if no relevant documents found and business can settle with receipts, look for receipts if (!relevantDocuments.length && mainBusinessId) { const business = await injector .get(BusinessesProvider) .getBusinessByIdLoader.load(mainBusinessId); if (business?.can_settle_with_receipt) { relevantDocuments.push(...documents.filter(d => d.type === 'RECEIPT')); } } // for each invoice - generate accounting ledger entry const documentsEntriesPromises = relevantDocuments.map(async (document) => { if (!documentsTaxCategoryId) { return; } return ledgerEntryFromDocument(injector, document, charge, charge.owner_id, documentsTaxCategoryId) .then(ledgerEntry => { accountingLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency); dates.add(ledgerEntry.valueDate.getTime()); currencies.add(ledgerEntry.currency); }) .catch(e => { if (e instanceof LedgerError) { errors.add(e.message); } else { throw e; } }); }); entriesPromises.push(...documentsEntriesPromises); } entriesPromises.push(...mainTransactionsPromises, ...feeTransactionsPromises, miscExpensesLedgerPromise); await Promise.all(entriesPromises); if (Math.abs(transactionsTotalLocalAmount) > AMOUNT_REQUIRING_DOCUMENT && accountingLedgerEntries.length === 0 && !charge.documents_optional_flag) { errors.add(`Charge of more than ${AMOUNT_REQUIRING_DOCUMENT} ${defaultLocalCurrency} requires a document`); } // generate ledger from business trip transactions if (!isSelfClosingLedger && !accountingLedgerEntries.length) { if (businessTripsEmployeePayments.length) { const businessTripsEmployeePaymentsPromises = businessTripsEmployeePayments.map(async (businessTripsEmployeePayment) => { if (!tripTaxCategory) { return; } if (!businessTripsEmployeePayment.employee_business_id || !businessTripsEmployeePayment.date || !businessTripsEmployeePayment.amount || !businessTripsEmployeePayment.currency) { errors.add(`Business trip flight transaction ID="${businessTripsEmployeePayment.id}" is missing required fields`); return; } // preparations for core ledger entries let exchangeRate = undefined; if (businessTripsEmployeePayment.currency !== defaultLocalCurrency) { // get exchange rate for currency exchangeRate = await injector .get(ExchangeProvider) .getExchangeRates(businessTripsEmployeePayment.currency, defaultLocalCurrency, businessTripsEmployeePayment.date); } // set amounts let amount = Number(businessTripsEmployeePayment.amount); let foreignAmount = undefined; if (exchangeRate) { foreignAmount = amount; // calculate amounts in ILS amount = exchangeRate * amount; } const absAmount = Math.abs(amount); const absForeignAmount = foreignAmount ? Math.abs(foreignAmount) : undefined; const isCreditorCounterparty = amount > 0; const ledgerEntry = { id: businessTripsEmployeePayment.id, invoiceDate: businessTripsEmployeePayment.date, valueDate: businessTripsEmployeePayment.date, currency: businessTripsEmployeePayment.currency, creditAccountID1: isCreditorCounterparty ? businessTripsEmployeePayment.employee_business_id : tripTaxCategory, creditAmount1: absForeignAmount, localCurrencyCreditAmount1: absAmount, debitAccountID1: isCreditorCounterparty ? tripTaxCategory : businessTripsEmployeePayment.employee_business_id, debitAmount1: absForeignAmount, localCurrencyDebitAmount1: absAmount, reference: businessTripsEmployeePayment.id, isCreditorCounterparty, ownerId: charge.owner_id, currencyRate: exchangeRate, chargeId: charge.id, }; accountingLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency); dates.add(ledgerEntry.valueDate.getTime()); currencies.add(ledgerEntry.currency); }); await Promise.all(businessTripsEmployeePaymentsPromises); } else { const businessTripExpensesPromises = businessTripExpenses.map(async (businessTripExpense) => { if (!tripTaxCategory) { return; } const matchingEntry = financialAccountLedgerEntries.find(entry => businessTripExpense.transaction_ids?.includes(entry.id)); if (!matchingEntry) { return; } const isCreditorCounterparty = !matchingEntry.isCreditorCounterparty; const businessId = isCreditorCounterparty ? matchingEntry.debitAccountID1 : matchingEntry.creditAccountID1; const ledgerEntry = { ...matchingEntry, id: businessTripExpense.id, isCreditorCounterparty, creditAccountID1: isCreditorCounterparty ? businessId : tripTaxCategory, debitAccountID1: isCreditorCounterparty ? tripTaxCategory : businessId, }; accountingLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency); }); await Promise.all(businessTripExpensesPromises); } } const allowedUnbalancedBusinesses = new Set([ ...businessTripAttendees.map(attendee => attendee.id), ...chargeUnbalancedBusinesses.map(({ business_id }) => business_id), ]); const ledgerBalanceInfo = await getLedgerBalanceInfo(injector, ledgerBalance, undefined, allowedUnbalancedBusinesses); const miscLedgerEntries = []; // multiple currencies balance const businessBalance = ledgerBalance.get(mainBusinessId ?? ''); if (mainBusinessId && Object.keys(businessBalance?.foreignAmounts ?? {}).length > (charge.invoice_payment_currency_diff ? 0 : 1)) { const transactionEntries = financialAccountLedgerEntries.filter(entry => [entry.creditAccountID1, entry.debitAccountID1].includes(mainBusinessId)); const documentEntries = accountingLedgerEntries.filter(entry => [entry.creditAccountID1, entry.debitAccountID1].includes(mainBusinessId)); try { const entries = await multipleForeignCurrenciesBalanceEntries(injector, defaultLocalCurrency, documentEntries, transactionEntries, charge, businessBalance.foreignAmounts, charge.invoice_payment_currency_diff ?? false); for (const ledgerEntry of entries) { miscLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency); } } catch (e) { if (e instanceof LedgerError) { errors.add(e.message); } else { throw e; } } } // check if exchange rate record is needed const hasMultipleDates = dates.size > 1; const foreignCurrencyCount = currencies.size - (currencies.has(defaultLocalCurrency) ? 1 : 0); const mightRequireExchangeRateRecord = (hasMultipleDates && !!foreignCurrencyCount) || foreignCurrencyCount >= 2; const unbalancedBusinesses = ledgerBalanceInfo.unbalancedEntities.filter(({ entityId }) => ledgerBalanceInfo.financialEntities.some(fe => fe.id === entityId && fe.type === 'business') && !chargeUnbalancedBusinesses.map(b => b.business_id).includes(entityId)); if (mightRequireExchangeRateRecord && unbalancedBusinesses.length === 1) { const entryDate = getExchangeDates(financialAccountLedgerEntries.length ? financialAccountLedgerEntries : accountingLedgerEntries); const { entityId, balance } = unbalancedBusinesses[0]; const amount = Math.abs(balance.raw); const isCreditorCounterparty = balance.raw < 0; const exchangeRateEntry = accountingLedgerEntries.find(entry => [entry.creditAccountID1, entry.debitAccountID1].includes(entityId)); let exchangeRateTaxCategory = undefined; if (exchangeRateEntry) { exchangeRateTaxCategory = exchangeRateEntry.debitAccountID1 === entityId ? exchangeRateEntry.creditAccountID1 : exchangeRateEntry.debitAccountID1; const isIncomeCharge = !!formattedChargeAmount && formattedChargeAmount.raw > 0; if (isIncomeCharge) { exchangeRateTaxCategory = incomeExchangeRateTaxCategoryId; } } // validate exchange rate const validation = validateExchangeRate(entityId, [ ...financialAccountLedgerEntries, ...feeFinancialAccountLedgerEntries, ...accountingLedgerEntries, ...miscLedgerEntries, ], balance.raw, defaultLocalCurrency); if (validation === true) { if (exchangeRateTaxCategory) { const ledgerEntry = { id: 'exchange-ledger', // NOTE: this field is dummy creditAccountID1: isCreditorCounterparty ? entityId : exchangeRateTaxCategory, localCurrencyCreditAmount1: amount, debitAccountID1: isCreditorCounterparty ? exchangeRateTaxCategory : entityId, localCurrencyDebitAmount1: amount, description: 'Exchange ledger record', isCreditorCounterparty, invoiceDate: entryDate, valueDate: entryDate, currency: defaultLocalCurrency, ownerId: charge.owner_id, chargeId, }; miscLedgerEntries.push(ledgerEntry); updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency); } else { errors.add(`Failed to locate tax category for exchange rate for business ID="${entityId}"`); } } else { errors.add(validation); } } const crossYearLedgerEntries = await handleCrossYearLedgerEntries(charge, injector, accountingLedgerEntries); const records = [ ...(crossYearLedgerEntries ?? accountingLedgerEntries), ...feeFinancialAccountLedgerEntries, ...financialAccountLedgerEntries, ...miscLedgerEntries, ]; const updatedLedgerBalanceInfo = await getLedgerBalanceInfo(injector, ledgerBalance, errors, allowedUnbalancedBusinesses); if (insertLedgerRecordsIfNotExists) { await storeInitialGeneratedRecords(charge.id, records, injector); } return { records: ledgerProtoToRecordsConverter(records), charge, balance: updatedLedgerBalanceInfo, errors: Array.from(errors), }; } catch (e) { return { __typename: 'CommonError', message: `Failed to generate ledger records for charge ID="${chargeId}"\n${e}`, }; } }; //# sourceMappingURL=business-trip-ledger-generation.resolver.js.map