UNPKG

@accounter/server

Version:
345 lines • 15.3 kB
import { EMPTY_UUID } from '../../../shared/constants.js'; import { formatCurrency, formatFinancialAmount, getCurrencySymbol, } from '../../../shared/helpers/index.js'; import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js'; import { getChargeBusinesses } from '../../charges/helpers/common.helper.js'; import { FinancialEntitiesProvider } from '../../financial-entities/providers/financial-entities.provider.js'; import { TaxCategoriesProvider } from '../../financial-entities/providers/tax-categories.provider.js'; export class LedgerError extends Error { constructor(message) { super(message); } } export function isTransactionsOppositeSign([first, second]) { if (!first || !second) { throw new LedgerError('Transactions are missing'); } const firstAmount = Number(first.amount); const secondAmount = Number(second.amount); if (Number.isNaN(firstAmount) || Number.isNaN(secondAmount)) { throw new LedgerError('Transaction amount is not a number'); } return Number(first.amount) > 0 !== Number(second.amount) > 0; } export function validateTransactionBasicVariables(transaction) { const currency = formatCurrency(transaction.currency); const debitDate = transaction.debit_date_override ?? transaction.debit_date; if (!debitDate) { throw new LedgerError(`Transaction reference "${transaction.source_reference}" is missing debit date for currency ${currency}`); } const valueDate = transaction.debit_timestamp ?? debitDate; if (!transaction.business_id) { throw new LedgerError(`Transaction reference "${transaction.source_reference}" is missing business_id`); } const transactionBusinessId = transaction.business_id; return { currency, valueDate, transactionBusinessId, }; } export function validateTransactionRequiredVariables(transaction, validationOptions = {}) { if (!transaction.debit_date && !transaction.debit_date_override) { throw new LedgerError(`Transaction reference "${transaction.source_reference}" is missing debit date for currency ${transaction.currency}`); } if (!transaction.business_id && !validationOptions.skipBusinessId) { throw new LedgerError(`Transaction reference "${transaction.source_reference}" is missing business_id`); } const debit_timestamp = transaction.debit_date_override ?? transaction.debit_timestamp ?? transaction.debit_date; return { ...transaction, debit_timestamp, currency: formatCurrency(transaction.currency), }; } export function generatePartialLedgerEntry(transaction, ownerId, exchangeRate) { // set amounts let amount = Number(transaction.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; return { id: transaction.id, invoiceDate: transaction.event_date, valueDate: transaction.debit_timestamp, currency: transaction.currency, creditAmount1: absForeignAmount, localCurrencyCreditAmount1: absAmount, debitAmount1: absForeignAmount, localCurrencyDebitAmount1: absAmount, description: transaction.source_description ?? undefined, reference: transaction.origin_key, isCreditorCounterparty, ownerId, currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined, chargeId: transaction.charge_id, }; } function entrySingleAccountBalancer(ledgerBalance, entry, isCredit, ledgerEntityNumber, defaultLocalCurrency) { const entityId = isCredit ? ledgerEntityNumber === 1 ? entry.creditAccountID1 : entry.creditAccountID2 : ledgerEntityNumber === 1 ? entry.debitAccountID1 : entry.debitAccountID2; if (!entityId) { return; } const current = ledgerBalance.get(entityId); const factor = isCredit ? 1 : -1; const currency = entry.currency; const amount = ((isCredit ? ledgerEntityNumber === 1 ? entry.creditAmount1 : entry.creditAmount2 : ledgerEntityNumber === 1 ? entry.debitAmount1 : entry.debitAmount2) ?? 0) * factor; const localAmount = ((isCredit ? ledgerEntityNumber === 1 ? entry.localCurrencyCreditAmount1 : entry.localCurrencyCreditAmount2 : ledgerEntityNumber === 1 ? entry.localCurrencyDebitAmount1 : entry.localCurrencyDebitAmount2) ?? 0) * factor; if (current) { current.amount += localAmount; current.foreignAmounts ||= {}; if (amount && currency !== defaultLocalCurrency) { current.foreignAmounts[currency] = { foreign: (current.foreignAmounts[currency]?.foreign ?? 0) + amount, local: (current.foreignAmounts[currency]?.local ?? 0) + localAmount, }; } } else { ledgerBalance.set(entityId, { amount: localAmount, entityId, foreignAmounts: amount && currency !== defaultLocalCurrency ? { [currency]: { foreign: amount, local: localAmount, }, } : {}, }); } } export function updateLedgerBalanceByEntry(entry, ledgerBalance, defaultLocalCurrency) { entrySingleAccountBalancer(ledgerBalance, entry, true, 1, defaultLocalCurrency); entrySingleAccountBalancer(ledgerBalance, entry, true, 2, defaultLocalCurrency); entrySingleAccountBalancer(ledgerBalance, entry, false, 1, defaultLocalCurrency); entrySingleAccountBalancer(ledgerBalance, entry, false, 2, defaultLocalCurrency); return; } export async function getLedgerBalanceInfo(injector, ledgerBalance, errors = new Set(), allowedUnbalancedBusinesses = new Set(), financialEntities) { const { defaultLocalCurrency } = await injector .get(AdminContextProvider) .getVerifiedAdminContext(); let ledgerBalanceSum = 0; let isBalanced = true; const unbalancedEntities = []; if (!financialEntities) { const financialEntityIDs = new Set(Array.from(ledgerBalance.values()).map(v => v.entityId)); financialEntities = (await injector .get(FinancialEntitiesProvider) .getFinancialEntityByIdLoader.loadMany(Array.from(financialEntityIDs)) .then(res => res.filter(fe => !!fe && 'id' in fe))); } for (const { amount, entityId } of ledgerBalance.values()) { if (Math.abs(amount) < 0.005) { continue; } const isBusiness = financialEntities?.some(financialEntity => financialEntity.id === entityId && financialEntity.type === 'business') ?? true; if (isBusiness && !allowedUnbalancedBusinesses.has(entityId)) { let businessIdentification = `ID="${entityId}"`; if (financialEntities) { const businessName = financialEntities.find(fe => fe.id === entityId)?.name; if (businessName) { businessIdentification = `"${businessName}"`; } } errors.add(`Business ${businessIdentification} is unbalanced (By ${amount.toFixed(2)}${getCurrencySymbol(defaultLocalCurrency)})`); isBalanced = false; } unbalancedEntities.push({ entityId, balance: formatFinancialAmount(amount, defaultLocalCurrency), }); ledgerBalanceSum += amount; } if (Math.abs(ledgerBalanceSum) >= 0.005) { errors.add(`Total ledger balance is ${ledgerBalanceSum}`); isBalanced = false; } return { isBalanced, unbalancedEntities, balanceSum: ledgerBalanceSum, financialEntities, }; } export function ledgerProtoToRecordsConverter(records) { return records.map(record => { const adjustedRecord = { charge_id: record.chargeId, created_at: new Date(), credit_entity1: record.creditAccountID1 ?? null, credit_entity2: record.creditAccountID2 ?? null, credit_foreign_amount1: record.creditAmount1?.toString() ?? null, credit_foreign_amount2: record.creditAmount2?.toString() ?? null, credit_local_amount1: record.localCurrencyCreditAmount1?.toString(), credit_local_amount2: record.localCurrencyCreditAmount2?.toString() ?? null, currency: record.currency, debit_entity1: record.debitAccountID1 ?? null, debit_entity2: record.debitAccountID2 ?? null, debit_foreign_amount1: record.debitAmount1?.toString() ?? null, debit_foreign_amount2: record.debitAmount2?.toString() ?? null, debit_local_amount1: record.localCurrencyDebitAmount1?.toString(), debit_local_amount2: record.localCurrencyDebitAmount2?.toString() ?? null, description: record.description ?? null, id: EMPTY_UUID, invoice_date: record.invoiceDate, owner_id: record.ownerId ?? null, reference1: record.reference ?? null, updated_at: new Date(), value_date: record.valueDate, locked: false, }; return adjustedRecord; }); } export async function getFinancialAccountTaxCategoryId(injector, transaction) { const taxCategory = await injector .get(TaxCategoriesProvider) .taxCategoryByFinancialAccountIdsAndCurrenciesLoader.load({ financialAccountId: transaction.account_id, currency: transaction.currency, }); if (!taxCategory) { throw new LedgerError(`Account id "${transaction.account_id}", with currency ${transaction.currency} is missing tax category`); } return taxCategory.id; } export async function multipleForeignCurrenciesBalanceEntries(injector, defaultLocalCurrency, documentEntries, transactionEntries, charge, foreignAmounts, balanceAgainstLocal) { const useDocuments = !charge.documents_optional_flag; if (!transactionEntries.length) { throw new LedgerError(`Failed to locate transaction entries for charge "${charge.id}"`); } if (!documentEntries.length && useDocuments) { throw new LedgerError(`Failed to locate document entries for charge "${charge.id}"`); } const ledgerEntries = []; const { mainBusinessId } = await getChargeBusinesses(charge.id, injector); if (mainBusinessId && Object.keys(foreignAmounts).length > 0) { const transactionEntry = transactionEntries.reduce((prev, curr) => { if (!prev) { return curr; } return prev.valueDate.getTime() > curr.valueDate.getTime() ? prev : curr; }); const invoiceDate = new Date(Math.min(...(useDocuments ? documentEntries.map(entry => entry.invoiceDate.getTime()) : transactionEntries.map(entry => entry.valueDate.getTime())))); // get the main foreign currency + diff in local currency let mainForeignCurrency = undefined; let localDiff = 0; if (Object.keys(foreignAmounts).length === 1 && balanceAgainstLocal) { const [currency, { local, foreign }] = Object.entries(foreignAmounts)[0]; if (foreign === 0) { return []; } const isCreditorCounterparty = foreign < 0; const ledgerEntry = { id: transactionEntry.id + `|${currency}-balance`, // NOTE: this field is dummy creditAccountID1: mainBusinessId, debitAccountID1: mainBusinessId, localCurrencyCreditAmount1: Math.abs(local), localCurrencyDebitAmount1: Math.abs(local), ...(isCreditorCounterparty ? { creditAmount1: Math.abs(foreign), } : { debitAmount1: Math.abs(foreign), }), description: 'Foreign currency balance', isCreditorCounterparty, invoiceDate, valueDate: transactionEntry.valueDate, currency: currency, ownerId: transactionEntry.ownerId, chargeId: charge.id, }; return [ledgerEntry]; } for (const [currency, { local }] of Object.entries(foreignAmounts)) { if (mainForeignCurrency) { if (mainForeignCurrency.amount < local) { mainForeignCurrency = { amount: local, currency: currency }; } } else { mainForeignCurrency = { amount: local, currency: currency }; } localDiff += local; } if (balanceAgainstLocal && !foreignAmounts[defaultLocalCurrency]) { foreignAmounts[defaultLocalCurrency] = { local: -localDiff, foreign: 0, }; } for (const [currency, { local, foreign }] of Object.entries(foreignAmounts)) { let localToUse = local; if (!balanceAgainstLocal) { if (Math.abs(foreign) < 0.005) { continue; } if (mainForeignCurrency?.currency === currency) { localToUse -= localDiff; } } const isLocalCurrencyAndShouldBeBalanced = balanceAgainstLocal && mainForeignCurrency && defaultLocalCurrency === currency; const isCreditorCounterparty = isLocalCurrencyAndShouldBeBalanced ? mainForeignCurrency.amount > 0 : foreign < 0; const ledgerEntry = { id: transactionEntry.id + `|${currency}-balance`, // NOTE: this field is dummy ...(isCreditorCounterparty ? { creditAccountID1: mainBusinessId, } : { debitAccountID1: mainBusinessId, }), localCurrencyCreditAmount1: Math.abs(localToUse), localCurrencyDebitAmount1: Math.abs(localToUse), ...(balanceAgainstLocal && !foreign ? {} : { creditAmount1: Math.abs(foreign), debitAmount1: Math.abs(foreign), }), description: 'Foreign currency balance', isCreditorCounterparty, invoiceDate, valueDate: transactionEntry.valueDate, currency: currency, ownerId: transactionEntry.ownerId, chargeId: charge.id, }; ledgerEntries.push(ledgerEntry); } } return ledgerEntries; } //# sourceMappingURL=utils.helper.js.map