UNPKG

@accounter/server

Version:
224 lines • 11.5 kB
import { GraphQLError } from 'graphql'; import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js'; import { ChargesProvider } from '../../charges/providers/charges.provider.js'; import { isSupplementalFeeTransaction } from '../../ledger/helpers/fee-transactions.js'; import { LedgerError } from '../../ledger/helpers/utils.helper.js'; import { generateLedgerRecordsForBusinessTrip } from '../../ledger/resolvers/ledger-generation/business-trip-ledger-generation.resolver.js'; import { ChargeTagsProvider } from '../../tags/providers/charge-tags.provider.js'; import { BusinessTripEmployeePaymentsProvider } from '../providers/business-trips-employee-payments.provider.js'; import { BusinessTripAccommodationsExpensesProvider } from '../providers/business-trips-expenses-accommodations.provider.js'; import { BusinessTripCarRentalExpensesProvider } from '../providers/business-trips-expenses-car-rental.provider.js'; import { BusinessTripFlightsExpensesProvider } from '../providers/business-trips-expenses-flights.provider.js'; import { BusinessTripOtherExpensesProvider } from '../providers/business-trips-expenses-other.provider.js'; import { BusinessTripExpensesTransactionsMatchProvider } from '../providers/business-trips-expenses-transactions-match.provider.js'; import { BusinessTripTravelAndSubsistenceExpensesProvider } from '../providers/business-trips-expenses-travel-and-subsistence.provider.js'; import { BusinessTripExpensesProvider } from '../providers/business-trips-expenses.provider.js'; import { BusinessTripsProvider } from '../providers/business-trips.provider.js'; function validateTransactionAgainstBusinessTripsExpenses(transaction, transactionMatchingExpenses, miscExpenses, financialAccounts) { if (!transactionMatchingExpenses?.length && !miscExpenses?.length) { throw new LedgerError(`Transaction reference "${transaction.source_reference}" is not part of a business trip`); } const totalAmount = transactionMatchingExpenses.reduce((acc, expense) => acc + Number(expense.amount), 0); const direction = isSupplementalFeeTransaction(transaction, financialAccounts) ? -1 : 1; const miscExpensesAmount = miscExpenses.reduce((acc, expense) => Number(expense.amount) * direction + acc, 0); if (Math.abs(Number(transaction.amount) - miscExpensesAmount - totalAmount) > 0.005) { throw new LedgerError(`Transaction reference "${transaction.source_reference}" amount does not match the business trip expenses total amount`); } return true; } export const validateTransactionAgainstBusinessTrips = async (injector, transaction) => { const transactionMatchingExpenses = await injector .get(BusinessTripExpensesTransactionsMatchProvider) .getBusinessTripsExpenseMatchesByTransactionIdLoader.load(transaction.id); const { financialAccounts } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); return validateTransactionAgainstBusinessTripsExpenses(transaction, transactionMatchingExpenses, [], financialAccounts); }; export const getTransactionMatchedAmount = async (injector, transaction) => { const transactionMatchingExpensesPromise = injector .get(BusinessTripExpensesTransactionsMatchProvider) .getBusinessTripsExpenseMatchesByTransactionIdLoader.load(transaction.id); const [transactionMatchingExpenses] = await Promise.all([transactionMatchingExpensesPromise]); const expensesSum = transactionMatchingExpenses.reduce((acc, expense) => (expense.amount ? acc + Number(expense.amount) : acc), 0); const { financialAccounts } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); try { validateTransactionAgainstBusinessTripsExpenses(transaction, transactionMatchingExpenses, [], financialAccounts); } catch (e) { const errors = []; if (e instanceof LedgerError) { errors.push(e.message); } return { isFullyMatched: false, amount: expensesSum, errors, }; } const isFullyMatched = Math.abs(Number(transaction.amount) - expensesSum) < 0.005; return { isFullyMatched, amount: Number(transaction.amount), }; }; export async function coreExpenseUpdate(injector, fields, categoryToValidate) { const { id, businessTripId, date, valueDate, amount, currency, employeeBusinessId, chargeId } = fields; const currentExpense = await injector .get(BusinessTripExpensesProvider) .getBusinessTripsExpensesByIdLoader.load(id); if (!currentExpense) { throw new GraphQLError(`Business trip expense with id ${id} not found`); } if (categoryToValidate && currentExpense.category !== categoryToValidate) { throw new GraphQLError(`Business trip expense with id ${id} is not a ${categoryToValidate} expense`); } const updateCoreExpensePromise = async () => { return businessTripId ? injector.get(BusinessTripExpensesProvider).updateBusinessTripExpense({ businessTripExpenseId: id, businessTripId, ...(currentExpense.payed_by_employee ? { date, valueDate, currency, employeeBusinessId, } : {}), }) : Promise.resolve(); }; const updateEmployeePaymentPromise = async () => { const hasCommonFieldsToUpdate = date || valueDate || amount || currency || employeeBusinessId || chargeId; return hasCommonFieldsToUpdate ? injector.get(BusinessTripEmployeePaymentsProvider).updateBusinessTripEmployeePayment({ businessTripExpenseId: id, chargeId, date, valueDate, currency, employeeBusinessId, }) : Promise.resolve(); }; const [updatedTripExpense] = await Promise.all([ updateCoreExpensePromise(), updateEmployeePaymentPromise(), ]); return updatedTripExpense; } export async function generateChargeForEmployeePayment(injector, businessTripId, description) { const { ownerId, businessTrips: { businessTripTaxCategoryId }, } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); if (!businessTripTaxCategoryId) { throw new GraphQLError('Business trip tax category not set'); } try { const { id: chargeId } = await injector.get(ChargesProvider).generateCharge({ ownerId, taxCategoryId: businessTripTaxCategoryId, userDescription: description || 'Employee payment charge', type: 'BUSINESS_TRIP', }); if (!chargeId) { throw new GraphQLError('Failed to generate initial charge'); } await injector.get(BusinessTripsProvider).updateChargeBusinessTrip(chargeId, businessTripId); return chargeId; } catch (e) { console.error('Failed to generate charge for employee payment', e); throw new GraphQLError('Failed to generate charge for employee payment'); } } export const updateExistingTripExpense = async (injector, businessTripExpenseId, transactionId, category, amount) => { const updateTransactionMatchPromise = injector .get(BusinessTripExpensesTransactionsMatchProvider) .insertBusinessTripExpenseMatch({ businessTripExpenseId, transactionId, amount, }); const insertToCategoryPromise = async () => { switch (category) { case 'FLIGHT': return injector.get(BusinessTripFlightsExpensesProvider).insertBusinessTripFlightsExpense({ id: businessTripExpenseId, }); case 'ACCOMMODATION': return injector .get(BusinessTripAccommodationsExpensesProvider) .insertBusinessTripAccommodationsExpense({ id: businessTripExpenseId, }); case 'TRAVEL_AND_SUBSISTENCE': return injector .get(BusinessTripTravelAndSubsistenceExpensesProvider) .insertBusinessTripTravelAndSubsistenceExpense({ id: businessTripExpenseId }); case 'OTHER': return injector.get(BusinessTripOtherExpensesProvider).insertBusinessTripOtherExpense({ id: businessTripExpenseId, }); case 'CAR_RENTAL': return injector .get(BusinessTripCarRentalExpensesProvider) .insertBusinessTripCarRentalExpense({ id: businessTripExpenseId, days: 0, isFuelExpense: false, }); default: throw new GraphQLError(`Invalid category ${category}`); } }; await Promise.all([updateTransactionMatchPromise, insertToCategoryPromise()]); }; export async function createTravelAndSubsistenceExpense(context, fields) { const { injector } = context; const adminContext = await injector.get(AdminContextProvider).getVerifiedAdminContext(); const { businessTripTagId } = adminContext.businessTrips; try { const coreExpensePromise = injector .get(BusinessTripExpensesProvider) .insertBusinessTripExpense({ businessTripId: fields.businessTripId, category: 'TRAVEL_AND_SUBSISTENCE', }) .then(res => res[0]); const chargeGenerationPromise = generateChargeForEmployeePayment(injector, fields.businessTripId, fields.expenseType ?? undefined); const [coreExpense, chargeId] = await Promise.all([ coreExpensePromise, chargeGenerationPromise, ]); const [charge] = await Promise.all([ injector.get(ChargesProvider).getChargeByIdLoader.load(chargeId), injector .get(BusinessTripTravelAndSubsistenceExpensesProvider) .insertBusinessTripTravelAndSubsistenceExpense({ id: coreExpense.id, expenseType: fields.expenseType, }), injector.get(BusinessTripEmployeePaymentsProvider).insertBusinessTripEmployeePayment({ businessTripExpenseId: coreExpense.id, chargeId, date: fields.date, valueDate: fields.valueDate, amount: fields.amount, currency: fields.currency, employeeBusinessId: fields.employeeBusinessId, }), businessTripTagId ? injector.get(ChargeTagsProvider).insertChargeTag({ chargeId, tagId: businessTripTagId }) : Promise.resolve(), ]); if (!charge) { throw new GraphQLError('Failed to generate charge for employee payment'); } // generate ledger records await generateLedgerRecordsForBusinessTrip(charge, { insertLedgerRecordsIfNotExists: true }, context, {}); return coreExpense.id; } catch (e) { console.error(`Error adding new business trip travel & subsistence expense`, e); throw new GraphQLError('Error adding new business trip travel & subsistence expense'); } } //# sourceMappingURL=business-trips-expenses.helper.js.map