@accounter/server
Version:
Accounter GraphQL server
368 lines • 19.7 kB
JavaScript
import { formatStringifyAmount } from '../../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../../admin-context/providers/admin-context.provider.js';
import { calculateTotalAmount, getChargeBusinesses, getChargeDocumentsMeta, getChargeTaxCategoryId, } from '../../../charges/helpers/common.helper.js';
import { getDeelEmployeeId, isDeelDocument } from '../../../deel/helpers/deel.helper.js';
import { DocumentsProvider } from '../../../documents/providers/documents.provider.js';
import { BusinessesProvider } from '../../../financial-entities/providers/businesses.provider.js';
import { TransactionsProvider } from '../../../transactions/providers/transactions.provider.js';
import { getExchangeDates, isRefundCharge, ledgerEntryFromBalanceCancellation, ledgerEntryFromDocument, ledgerEntryFromMainTransaction, } 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 { getLedgerBalanceInfo, LedgerError, ledgerProtoToRecordsConverter, multipleForeignCurrenciesBalanceEntries, updateLedgerBalanceByEntry, } from '../../helpers/utils.helper.js';
import { BalanceCancellationProvider } from '../../providers/balance-cancellation.provider.js';
import { UnbalancedBusinessesProvider } from '../../providers/unbalanced-businesses.provider.js';
export const generateLedgerRecordsForCommonCharge = async (charge, { insertLedgerRecordsIfNotExists }, { injector }) => {
const { defaultLocalCurrency, defaultTaxCategoryId, general: { taxCategories: { incomeExchangeRateTaxCategoryId, exchangeRateTaxCategoryId }, }, } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
const chargeId = charge.id;
const errors = new Set();
try {
// validate ledger records are balanced
const ledgerBalance = new Map();
function updateLedgerBalance(entry) {
updateLedgerBalanceByEntry(entry, ledgerBalance, defaultLocalCurrency);
}
const dates = new Set();
const currencies = new Set();
const [{ invoiceCount, receiptCount }, formattedChargeAmount] = await Promise.all([
getChargeDocumentsMeta(chargeId, injector),
calculateTotalAmount(chargeId, injector, defaultLocalCurrency),
]);
const gotRelevantDocuments = invoiceCount + receiptCount > 0;
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 documentsPromise = gotRelevantDocuments
? injector.get(DocumentsProvider).getDocumentsByChargeIdLoader.load(chargeId)
: Promise.resolve([]);
const transactionsPromise = injector
.get(TransactionsProvider)
.transactionsByChargeIDLoader.load(chargeId);
const unbalancedBusinessesPromise = injector
.get(UnbalancedBusinessesProvider)
.getChargeUnbalancedBusinessesByChargeIds.load(chargeId);
const chargeBallanceCancellationsPromise = injector
.get(BalanceCancellationProvider)
.getBalanceCancellationByChargesIdLoader.load(chargeId);
const [documentsTaxCategoryId, documents, transactions, unbalancedBusinesses, balanceCancellations, { mainBusinessId },] = await Promise.all([
documentsTaxCategoryIdPromise(),
documentsPromise,
transactionsPromise,
unbalancedBusinessesPromise,
chargeBallanceCancellationsPromise,
getChargeBusinesses(chargeId, injector),
]);
const entriesPromises = [];
const accountingLedgerEntries = [];
const financialAccountLedgerEntries = [];
const feeFinancialAccountLedgerEntries = [];
const miscLedgerEntries = [];
// 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(async (ledgerEntry) => {
await getDeelEmployeeId(injector, document, ledgerEntry, miscLedgerEntries, updateLedgerBalance);
accountingLedgerEntries.push(ledgerEntry);
updateLedgerBalance(ledgerEntry);
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);
}
// generate ledger from transactions
if (transactions.length > 0) {
const { mainTransactions, feeTransactions } = splitFeeTransactions(transactions);
// for each transaction, create a ledger record
const mainTransactionsPromises = mainTransactions.map(async (transaction) => {
const ledgerEntry = await ledgerEntryFromMainTransaction(transaction, injector, chargeId, charge.owner_id, mainBusinessId ?? undefined, gotRelevantDocuments).catch(e => {
if (e instanceof LedgerError) {
errors.add(e.message);
}
else {
throw e;
}
});
if (!ledgerEntry) {
return;
}
financialAccountLedgerEntries.push(ledgerEntry);
updateLedgerBalance(ledgerEntry);
dates.add(ledgerEntry.valueDate.getTime());
currencies.add(ledgerEntry.currency);
});
// 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 => {
updateLedgerBalance(ledgerEntry);
dates.add(ledgerEntry.valueDate.getTime());
currencies.add(ledgerEntry.currency);
});
});
entriesPromises.push(...mainTransactionsPromises, ...feeTransactionsPromises);
}
// generate ledger from misc expenses
const expensesLedgerPromise = generateMiscExpensesLedger(charge, injector).then(entries => {
entries.map(entry => {
entry.ownerId = charge.owner_id;
feeFinancialAccountLedgerEntries.push(entry);
updateLedgerBalance(entry);
dates.add(entry.valueDate.getTime());
currencies.add(entry.currency);
});
});
entriesPromises.push(expensesLedgerPromise);
await Promise.all(entriesPromises);
// generate ledger from balance cancellation
for (const balanceCancellation of balanceCancellations) {
try {
const ledgerEntry = await ledgerEntryFromBalanceCancellation(balanceCancellation, ledgerBalance, financialAccountLedgerEntries, chargeId, charge.owner_id, injector);
miscLedgerEntries.push(ledgerEntry);
updateLedgerBalance(ledgerEntry);
}
catch (e) {
if (e instanceof LedgerError) {
errors.add(e.message);
continue;
}
else {
throw e;
}
}
}
const allowedUnbalancedBusinesses = new Set(unbalancedBusinesses.map(({ business_id }) => business_id));
// 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 => {
if ([entry.creditAccountID1, entry.debitAccountID1].includes(mainBusinessId))
return true;
return false;
});
const documentEntries = accountingLedgerEntries.filter(entry => {
if ([entry.creditAccountID1, entry.debitAccountID1].includes(mainBusinessId))
return true;
return false;
});
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);
updateLedgerBalance(ledgerEntry);
}
}
catch (e) {
if (e instanceof LedgerError) {
errors.add(e.message);
}
else {
throw e;
}
}
}
// Add ledger completion entries
const { balanceSum, isBalanced, unbalancedEntities, financialEntities } = await getLedgerBalanceInfo(injector, ledgerBalance, undefined, allowedUnbalancedBusinesses);
if (errors.size) {
const records = [
...financialAccountLedgerEntries,
...feeFinancialAccountLedgerEntries,
...accountingLedgerEntries,
...miscLedgerEntries,
];
if (records.length && insertLedgerRecordsIfNotExists) {
await storeInitialGeneratedRecords(charge.id, records, injector);
}
return {
records: ledgerProtoToRecordsConverter(records),
charge,
balance: { balanceSum, isBalanced, unbalancedEntities },
errors: Array.from(errors),
};
}
if (Math.abs(balanceSum) > 0.005) {
errors.add(`Failed to balance: ${formatStringifyAmount(balanceSum)} diff; ${unbalancedEntities.map(entity => entity.entityId).join(', ')} are not balanced`);
}
else if (!isBalanced) {
// check if business doesn't require documents
if (!accountingLedgerEntries.length && mainBusinessId) {
const business = await injector
.get(BusinessesProvider)
.getBusinessByIdLoader.load(mainBusinessId);
if (business?.no_invoices_required) {
const unbalancedBusinesses = unbalancedEntities.find(b => b.entityId === business.id);
if (unbalancedBusinesses) {
errors.add(`Business "${business.name}" is unbalanced (in the scope of this charge)`);
}
else {
errors.add('Some businesses are not balanced in the scope of this charge');
}
const records = [...financialAccountLedgerEntries, ...feeFinancialAccountLedgerEntries];
if (insertLedgerRecordsIfNotExists) {
await storeInitialGeneratedRecords(charge.id, records, injector);
}
return {
records: ledgerProtoToRecordsConverter(records),
charge,
balance: { balanceSum, isBalanced, unbalancedEntities },
errors: Array.from(errors),
};
}
}
// 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 = unbalancedEntities.filter(({ entityId }) => financialEntities.some(fe => fe.id === entityId && fe.type === 'business') &&
!allowedUnbalancedBusinesses.has(entityId));
if (mightRequireExchangeRateRecord && unbalancedBusinesses.length === 1) {
const transactionEntry = financialAccountLedgerEntries[0];
const entryDate = getExchangeDates(financialAccountLedgerEntries);
const { entityId, balance } = unbalancedBusinesses[0];
const amount = Math.abs(balance.raw);
const isCreditorCounterparty = balance.raw < 0;
let exchangeRateEntry = accountingLedgerEntries.find(entry => [entry.creditAccountID1, entry.debitAccountID1].includes(entityId));
let exchangeRateTaxCategory = undefined;
const deelCharge = documents.some(doc => isDeelDocument(doc));
if (deelCharge) {
exchangeRateEntry ||= miscLedgerEntries.find(entry => [entry.creditAccountID1, entry.debitAccountID1].includes(entityId));
}
if (exchangeRateEntry) {
if (documentsTaxCategoryId) {
exchangeRateTaxCategory = documentsTaxCategoryId;
}
else {
exchangeRateTaxCategory =
exchangeRateEntry.debitAccountID1 === entityId
? exchangeRateEntry.creditAccountID1
: exchangeRateEntry.debitAccountID1;
}
if (deelCharge) {
// If Deel - override to generic exchange rate tax category
exchangeRateTaxCategory = exchangeRateTaxCategoryId;
}
const isRefund = isRefundCharge(charge.user_description);
const isIncomeCharge = !isRefund && !!formattedChargeAmount && formattedChargeAmount.raw > 0;
if (isIncomeCharge) {
exchangeRateTaxCategory = incomeExchangeRateTaxCategoryId;
}
}
else if (charge.documents_optional_flag) {
exchangeRateTaxCategory = exchangeRateTaxCategoryId;
}
// validate exchange rate
const validation = validateExchangeRate(entityId, [
...accountingLedgerEntries,
...financialAccountLedgerEntries,
...feeFinancialAccountLedgerEntries,
...miscLedgerEntries,
], balance.raw, defaultLocalCurrency);
if (validation === true) {
if (exchangeRateTaxCategory) {
const ledgerEntry = {
id: transactionEntry.id + '|fee', // 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: transactionEntry.ownerId,
chargeId,
};
miscLedgerEntries.push(ledgerEntry);
updateLedgerBalance(ledgerEntry);
}
else {
errors.add(`Failed to locate tax category for exchange rate for business ID="${entityId}"`);
}
}
else {
errors.add(validation);
}
}
else {
errors.add(`Failed to balance: ${hasMultipleDates ? 'Dates are different' : 'Dates are consistent'} and ${foreignCurrencyCount ? 'currencies are foreign' : 'currencies are local'}`);
}
}
const crossYearLedgerEntries = await handleCrossYearLedgerEntries(charge, injector, accountingLedgerEntries);
const records = [
...(crossYearLedgerEntries ?? accountingLedgerEntries),
...financialAccountLedgerEntries,
...feeFinancialAccountLedgerEntries,
...miscLedgerEntries,
];
if (insertLedgerRecordsIfNotExists) {
await storeInitialGeneratedRecords(charge.id, records, injector);
}
const ledgerBalanceInfo = await getLedgerBalanceInfo(injector, ledgerBalance, errors, allowedUnbalancedBusinesses);
return {
records: ledgerProtoToRecordsConverter(records),
charge,
balance: ledgerBalanceInfo,
errors: Array.from(errors),
};
}
catch (e) {
const message = `Failed to generate ledger records for charge ID="${chargeId}"\n${e}`;
console.error(message);
return {
__typename: 'CommonError',
message,
};
}
};
//# sourceMappingURL=common-ledger-generation.resolver.js.map