@accounter/server
Version:
Accounter GraphQL server
345 lines • 15.3 kB
JavaScript
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