UNPKG

@accounter/server

Version:

275 lines • 15 kB
import { ExchangeProvider } from '../../exchange-rates/providers/exchange.provider.js'; import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js'; import { Currency, } from '../../../__generated__/types.js'; import { formatCurrency, formatFinancialAmount } from '../../../shared/helpers/index.js'; import { BusinessTripError } from '../resolvers/business-trip-summary.resolver.js'; export function convertSummaryCategoryDataToRow(category, data) { const excessExpenditure = (data[Currency.Ils]?.taxable ?? 0) - (data[Currency.Ils]?.total ?? 0); return { type: category, totalForeignCurrency: formatFinancialAmount(data[Currency.Usd]?.total ?? 0, Currency.Usd), taxableForeignCurrency: formatFinancialAmount(data[Currency.Usd]?.taxable ?? 0, Currency.Usd), maxTaxableForeignCurrency: formatFinancialAmount(data[Currency.Usd]?.maxTaxable ?? 0, Currency.Usd), totalLocalCurrency: formatFinancialAmount(data[Currency.Ils]?.total ?? 0, Currency.Ils), taxableLocalCurrency: formatFinancialAmount(data[Currency.Ils]?.taxable ?? 0, Currency.Ils), maxTaxableLocalCurrency: formatFinancialAmount(data[Currency.Ils]?.maxTaxable ?? 0, Currency.Ils), excessExpenditure: formatFinancialAmount(Math.max(excessExpenditure, 0), Currency.Ils), }; } export function calculateTotalReportSummaryCategory(data) { const totalSumCategory = Object.values(data).reduce((acc, category) => { Object.entries(category).map(([currency, { total, taxable }]) => { acc[currency] ||= { total: 0, taxable: 0, maxTaxable: 0 }; acc[currency].total += total; acc[currency].taxable += taxable; }); return acc; }, {}); return totalSumCategory; } export function getExpenseCoreData(tripExpense) { if (tripExpense.payed_by_employee) { if (!tripExpense.currency || !tripExpense.amount || !tripExpense.date) { throw new BusinessTripError(`Currency, amount or date not found for employee-paid trip expense ID ${tripExpense.id}`); } return { amount: Number(tripExpense.amount), currency: formatCurrency(tripExpense.currency), date: new Date(tripExpense.date), }; } if (!tripExpense.currency || !tripExpense.amount || !tripExpense.value_date) { throw new BusinessTripError(`Currency, amount or date not found for business trip expense ID ${tripExpense.id}`); } return { amount: Number(tripExpense.amount), currency: formatCurrency(tripExpense.currency), date: new Date(tripExpense.value_date), }; } async function getDefaultCurrenciesAmountsAndExchangeRate(context, currency, amount, date) { const { injector, adminContext: { defaultLocalCurrency, defaultCryptoConversionFiatCurrency }, } = context; const exchangeRatePromise = currency === defaultLocalCurrency ? Promise.resolve(1) : injector.get(ExchangeProvider).getExchangeRates(currency, defaultLocalCurrency, date); const usdRatePromise = currency === defaultCryptoConversionFiatCurrency ? Promise.resolve(1) : injector .get(ExchangeProvider) .getExchangeRates(currency, defaultCryptoConversionFiatCurrency, date); const [localRate, foreignRate] = await Promise.all([exchangeRatePromise, usdRatePromise]); const localAmount = localRate * amount; const foreignAmount = foreignRate * amount; return { localAmount, foreignAmount }; } export async function getExpenseAmountsData(context, businessTripExpense) { const { injector } = context; try { const { amount, currency, date } = getExpenseCoreData(businessTripExpense); const { localAmount, foreignAmount } = await getDefaultCurrenciesAmountsAndExchangeRate(context, currency, amount, date); return { localAmount, foreignAmount }; } catch (error) { // handle merged transaction of different currencies if (!(error instanceof Error) || !error?.message?.includes('Currency, amount or date not found for business trip expense') || !businessTripExpense.transaction_ids || businessTripExpense.transaction_ids.length <= 1) { throw error; } const transactions = await injector .get(TransactionsProvider) .getTransactionByIdLoader.loadMany(businessTripExpense.transaction_ids); const allTransactions = transactions.filter(transaction => transaction && !(transaction instanceof Error)); let localAmount = 0; let foreignAmount = 0; await Promise.all(allTransactions.map(async (transaction) => { if (!transaction || transaction instanceof Error) { return; } const amount = Number(transaction.amount); const currency = transaction.currency; const date = transaction.debit_timestamp || transaction.debit_date || transaction.event_date; if (!amount || !currency || !date) { const errorMessage = `Currency, amount or date not found for transaction ID ${transaction.id}`; console.error(errorMessage); throw new BusinessTripError(errorMessage); } const { localAmount: transactionLocalAmount, foreignAmount: transactionForeignAmount } = await getDefaultCurrenciesAmountsAndExchangeRate(context, currency, amount, date); localAmount += transactionLocalAmount; foreignAmount += transactionForeignAmount; })); return { localAmount, foreignAmount }; } } export async function flightExpenseDataCollector(context, businessTripExpense, partialSummaryData) { // populate category partialSummaryData['FLIGHT'] ??= {}; const category = partialSummaryData['FLIGHT']; const { localAmount, foreignAmount } = await getExpenseAmountsData(context, businessTripExpense); // calculate taxable amount const fullyTaxableClasses = ['ECONOMY', 'PREMIUM_ECONOMY', 'BUSINESS']; if (!businessTripExpense.class) { console.error(`Flight class not found for flight expense ID ${businessTripExpense.id}`); throw new BusinessTripError('Flights expenses: some flights are missing class'); } if (!fullyTaxableClasses.includes(businessTripExpense.class)) { console.error(`Taxability logic for flight class ${businessTripExpense.class} is not implemented yet (trip expense ID: ${businessTripExpense.id})`); throw new BusinessTripError(`Flights expenses: taxability logic for class ${businessTripExpense.class} is not implemented yet`); } // for all classes <= business, the amount is fully taxable const localTaxable = localAmount; const foreignTaxable = foreignAmount; // update amounts const { defaultLocalCurrency, defaultCryptoConversionFiatCurrency } = context.adminContext; category[defaultLocalCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 }; category[defaultLocalCurrency].total += localAmount; category[defaultLocalCurrency].taxable += localTaxable; category[defaultLocalCurrency].maxTaxable += localTaxable; category[defaultCryptoConversionFiatCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 }; category[defaultCryptoConversionFiatCurrency].total += foreignAmount; category[defaultCryptoConversionFiatCurrency].taxable += foreignTaxable; category[defaultCryptoConversionFiatCurrency].maxTaxable += foreignTaxable; return void 0; } export async function employeeAccommodationDataByTrip() { } export async function otherExpensesDataCollector(context, otherExpenses, partialSummaryData) { const { defaultLocalCurrency, defaultCryptoConversionFiatCurrency } = context.adminContext; // populate category partialSummaryData['OTHER'] ??= {}; const category = partialSummaryData['OTHER']; category[defaultLocalCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 }; category[defaultCryptoConversionFiatCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 }; if (otherExpenses.length === 0) { return void 0; } await Promise.all(otherExpenses.map(async (businessTripExpense) => { const { localAmount, foreignAmount } = await getExpenseAmountsData(context, businessTripExpense); category[defaultLocalCurrency].total += localAmount; category[defaultLocalCurrency].taxable += localAmount; category[defaultLocalCurrency].maxTaxable += localAmount; category[defaultCryptoConversionFiatCurrency].total += foreignAmount; category[defaultCryptoConversionFiatCurrency].taxable += foreignAmount; category[defaultCryptoConversionFiatCurrency].maxTaxable += foreignAmount; })); return void 0; } export async function travelAndSubsistenceExpensesDataCollector(context, businessTripExpenses, partialSummaryData, taxVariables, { destinationCode, unAccommodatedDays, attendees }) { const { defaultLocalCurrency, defaultCryptoConversionFiatCurrency } = context.adminContext; // populate category partialSummaryData['TRAVEL_AND_SUBSISTENCE'] ??= {}; const category = partialSummaryData['TRAVEL_AND_SUBSISTENCE']; category[defaultLocalCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 }; category[defaultCryptoConversionFiatCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 }; const totalBusinessDays = Array.from(attendees.values()).reduce((acc, attendee) => acc + attendee.daysCount, 0); const maxTaxableUsd = getAttendeeTravelAndSubsistenceMaxTax(totalBusinessDays, unAccommodatedDays, destinationCode, taxVariables); category[defaultLocalCurrency].maxTaxable += 0; // TODO: calculate category[defaultCryptoConversionFiatCurrency].maxTaxable += maxTaxableUsd; // set actual expense amounts await Promise.all(businessTripExpenses.map(async (businessTripExpense) => { const { localAmount, foreignAmount } = await getExpenseAmountsData(context, businessTripExpense); category[defaultLocalCurrency].total += localAmount; category[defaultCryptoConversionFiatCurrency].total += foreignAmount; })); // set taxable amounts const taxableAmount = Math.max(category[defaultCryptoConversionFiatCurrency].total, maxTaxableUsd); const taxablePortion = category[defaultCryptoConversionFiatCurrency].total === 0 ? 0 : taxableAmount / category[defaultCryptoConversionFiatCurrency].total; category[defaultLocalCurrency].taxable += category[defaultLocalCurrency].total * taxablePortion; category[defaultCryptoConversionFiatCurrency].taxable += taxableAmount; return void 0; } export async function carRentalExpensesDataCollector(context, businessTripExpenses, partialSummaryData, taxVariables, destinationCode) { const { defaultLocalCurrency, defaultCryptoConversionFiatCurrency } = context.adminContext; // populate category partialSummaryData['CAR_RENTAL'] ??= {}; const category = partialSummaryData['CAR_RENTAL']; category[defaultLocalCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 }; category[defaultCryptoConversionFiatCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 }; let rentalDays = 0; // set actual expense amounts await Promise.all(businessTripExpenses.map(async (businessTripExpense) => { if (!businessTripExpense.is_fuel_expense) { rentalDays += businessTripExpense.days; } const { localAmount, foreignAmount } = await getExpenseAmountsData(context, businessTripExpense); category[defaultLocalCurrency].total += localAmount; category[defaultCryptoConversionFiatCurrency].total += foreignAmount; })); // set max taxable amounts const { max_car_rental_per_day } = taxVariables; const maxDailyRentalAmount = Number(max_car_rental_per_day); if (Number.isNaN(maxDailyRentalAmount)) { throw new BusinessTripError('Tax variables are not set'); } const increasedLimitDestination = isIncreasedLimitDestination(destinationCode) ? 1.25 : 1; const maxTaxableUsd = maxDailyRentalAmount * rentalDays * increasedLimitDestination * -1; category[defaultLocalCurrency].maxTaxable += 0; // TODO: calculate category[defaultCryptoConversionFiatCurrency].maxTaxable += maxTaxableUsd; // set taxable amounts const taxableAmount = Math.max(category[defaultCryptoConversionFiatCurrency].total, maxTaxableUsd); const taxablePortion = category[defaultCryptoConversionFiatCurrency].total === 0 ? 0 : taxableAmount / category[defaultCryptoConversionFiatCurrency].total; category[defaultLocalCurrency].taxable += category[defaultLocalCurrency].total * taxablePortion; category[defaultCryptoConversionFiatCurrency].taxable += taxableAmount; return void 0; } export function isIncreasedLimitDestination(destinationCode) { if (!destinationCode) { return false; } const increasedLimitDestinations = [ 'AGO', // angola 'AUS', // australia 'AUT', // austria 'BEL', // belgium 'CMR', // cameroon 'CAN', // canada 'DNK', // denmark 'ARE', // dubai 'FIN', // finland 'FRA', // france 'DEU', // germany 'GRC', // greece 'HKG', // hong kong 'ISL', // iceland 'IRL', // ireland 'ITA', // italy 'JPN', // japan 'KOR', // south korea 'PRK', // north korea 'LUX', // luxembourg 'NLD', // netherlands 'NOR', // norway 'OMN', // oman 'QAT', // qatar 'ESP', // spain 'SWE', // sweden 'CHE', // switzerland 'TWN', // taiwan 'GBR', // united kingdom ]; return increasedLimitDestinations.includes(destinationCode.toLocaleUpperCase()); } export function onlyUnique(value, index, array) { return array.indexOf(value) === index; } export function getAttendeeTravelAndSubsistenceMaxTax(totalBusinessDays, unAccommodatedDays, destinationCode, taxVariables) { const { max_tns_with_accommodation, max_tns_without_accommodation } = taxVariables; const maxExpenseWithAccommodation = Number(max_tns_with_accommodation); const maxExpenseWithoutAccommodation = Number(max_tns_without_accommodation); if (Number.isNaN(maxExpenseWithAccommodation) || Number.isNaN(maxExpenseWithoutAccommodation)) { throw new BusinessTripError('Tax variables are not set'); } const accommodatedDays = totalBusinessDays - unAccommodatedDays; const increasedLimitFactor = isIncreasedLimitDestination(destinationCode) ? 1.25 : 1; const maxTaxableUsd = (maxExpenseWithAccommodation * accommodatedDays + maxExpenseWithoutAccommodation * unAccommodatedDays) * increasedLimitFactor * -1; return maxTaxableUsd; } export * from './business-trip-report-accommodation.helper.js'; //# sourceMappingURL=business-trip-report.helper.js.map