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