@accounter/server
Version:
Accounter GraphQL server
200 lines • 10.7 kB
JavaScript
import { differenceInDays } from 'date-fns';
import { GraphQLError } from 'graphql';
import { Currency } from '../../../shared/enums.js';
import { dateToTimelessDateString } from '../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { ExchangeProvider } from '../../exchange-rates/providers/exchange.provider.js';
import { EmployeesProvider } from '../../salaries/providers/employees.provider.js';
import { getAttendeeTravelAndSubsistenceMaxTax, getExpenseCoreData, } from '../helpers/business-trip-report.helper.js';
import { createTravelAndSubsistenceExpense } from '../helpers/business-trips-expenses.helper.js';
import { BusinessTripAttendeesProvider } from '../providers/business-trips-attendees.provider.js';
import { BusinessTripAccommodationsExpensesProvider } from '../providers/business-trips-expenses-accommodations.provider.js';
import { BusinessTripTravelAndSubsistenceExpensesProvider } from '../providers/business-trips-expenses-travel-and-subsistence.provider.js';
import { BusinessTripTaxVariablesProvider } from '../providers/business-trips-tax-variables.provider.js';
import { BusinessTripsProvider } from '../providers/business-trips.provider.js';
import { businessTripSummary } from './business-trip-summary.resolver.js';
export const creditShareholdersBusinessTripTravelAndSubsistence = async (_, { businessTripId }, context) => {
const { injector } = context;
async function summaryDataPromise() {
const businessTrip = await injector
.get(BusinessTripsProvider)
.getBusinessTripsByIdLoader.load(businessTripId);
if (!businessTrip) {
throw new GraphQLError(`Business trip with id ${businessTripId} not found`);
}
const attendees = await injector
.get(BusinessTripAttendeesProvider)
.getBusinessTripsAttendeesByBusinessTripIdLoader.load(businessTripId);
if (!attendees?.length) {
throw new GraphQLError(`Business trip with id ${businessTripId} is missing attendees`);
}
let toDate;
attendees.map(({ departure }) => {
if (departure && (!toDate || departure > toDate)) {
toDate = departure;
}
});
if (!toDate) {
throw new GraphQLError(`Business trip with id ${businessTripId} is missing end date`);
}
const { ledgerLock } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
if (ledgerLock && dateToTimelessDateString(toDate) < ledgerLock) {
throw new GraphQLError(`Cannot credit shareholders for business trip ${businessTrip.name}, because ledger is locked`);
}
const summaryData = await businessTripSummary(injector, businessTrip);
return { businessTrip: { ...businessTrip, toDate }, summaryData };
}
const accommodationExpensesPromise = injector
.get(BusinessTripAccommodationsExpensesProvider)
.getBusinessTripsAccommodationsExpensesByBusinessTripIdLoader.load(businessTripId);
const attendeePayedTnSExpensesPromise = injector
.get(BusinessTripTravelAndSubsistenceExpensesProvider)
.getBusinessTripsTravelAndSubsistenceExpensesByBusinessTripIdLoader.load(businessTripId)
.then(expenses => expenses.filter(expense => expense.payed_by_employee));
const [{ businessTrip, summaryData }, accommodationExpenses, attendeePayedTnSExpenses, { shareholdersMap, attendeesMap },] = await Promise.all([
summaryDataPromise(),
accommodationExpensesPromise,
attendeePayedTnSExpensesPromise,
shareholdersMapPromise(injector, businessTripId),
]);
if (!businessTrip.toDate) {
throw new GraphQLError(`Business trip with id ${businessTripId} is missing end date`);
}
if (shareholdersMap.size === 0) {
return []; // No shareholders to distribute the credit to
}
async function availableAmountToDistributePromise() {
const travelAndSubsistenceRow = await summaryData.rows.find(row => 'type' in row && row.type === 'TRAVEL_AND_SUBSISTENCE');
if (!travelAndSubsistenceRow) {
throw new GraphQLError(`Travel and subsistence row not found`);
}
const [{ raw: taxableAmount }, { raw: maxTaxableAmount }] = await Promise.all([
travelAndSubsistenceRow.maxTaxableForeignCurrency,
travelAndSubsistenceRow.taxableForeignCurrency,
]);
if (Number.isNaN(taxableAmount) || Number.isNaN(maxTaxableAmount)) {
throw new GraphQLError(`Taxable amounts are missing`);
}
const availableAmountToDistribute = Math.max(maxTaxableAmount - taxableAmount, 0);
return availableAmountToDistribute;
}
const [availableAmountToDistribute, shareholdersPotentialAmountToDistribute] = await Promise.all([
availableAmountToDistributePromise(),
shareholdersPotentialAmountToDistributePromise(injector, accommodationExpenses, shareholdersMap, attendeesMap, attendeePayedTnSExpenses, businessTrip),
]);
if (availableAmountToDistribute <= 0) {
return []; // No available amount to distribute
}
const commonFields = {
businessTripId,
currency: Currency.Usd,
date: dateToTimelessDateString(businessTrip.toDate),
valueDate: dateToTimelessDateString(businessTrip.toDate),
};
const totalPotentialAmountToDistribute = Object.values(shareholdersPotentialAmountToDistribute).reduce((acc, amount) => acc + amount, 0);
const creditRatio = totalPotentialAmountToDistribute <= availableAmountToDistribute
? 1
: availableAmountToDistribute / totalPotentialAmountToDistribute;
return Object.entries(shareholdersPotentialAmountToDistribute).map(([id, amount]) => {
const shareholder = shareholdersMap.get(id);
return createTravelAndSubsistenceExpense(context, {
...commonFields,
amount: amount * creditRatio,
expenseType: `${shareholder.first_name}'s travel and subsistence expenses, no invoice`,
employeeBusinessId: id,
});
});
};
async function shareholdersMapPromise(injector, businessTripId) {
const shareholdersMap = new Map();
const attendeesMap = new Map();
const attendees = await injector
.get(BusinessTripAttendeesProvider)
.getBusinessTripsAttendeesByBusinessTripIdLoader.load(businessTripId);
const shareholders = await injector
.get(EmployeesProvider)
.getEmployeesByIdLoader.loadMany(attendees.map(a => a.id))
.then(res => res.filter(employee => {
if (!employee) {
return false;
}
if (employee instanceof Error) {
throw employee;
}
if (employee.role_status !== 'Controlling shareholder') {
return false;
}
return true;
}));
shareholders.map(shareholder => shareholdersMap.set(shareholder.business_id, shareholder));
attendees.map(attendee => {
if (shareholdersMap.has(attendee.id)) {
attendeesMap.set(attendee.id, attendee);
}
});
return { shareholdersMap, attendeesMap };
}
async function shareholdersPotentialAmountToDistributePromise(injector, accommodationExpenses, shareholdersMap, attendeesMap, attendeePayedTnSExpenses, businessTrip) {
const shareholdersPotentialAmountToDistribute = {};
const shareholdersAccommodatedNightsMap = new Map();
const { defaultCryptoConversionFiatCurrency } = await injector
.get(AdminContextProvider)
.getVerifiedAdminContext();
// collect shareholders accommodated data
accommodationExpenses.map(expense => {
expense.attendees_stay.filter(Boolean).map(({ attendeeId, nightsCount }) => {
if (shareholdersMap.has(attendeeId)) {
if (shareholdersAccommodatedNightsMap.has(attendeeId)) {
shareholdersAccommodatedNightsMap.set(attendeeId, shareholdersAccommodatedNightsMap.get(attendeeId) + nightsCount);
}
else {
shareholdersAccommodatedNightsMap.set(attendeeId, nightsCount);
}
}
});
});
await Promise.all(Array.from(shareholdersMap.entries()).map(async ([id, shareholder]) => {
const attendee = attendeesMap.get(id);
if (!attendee) {
throw new Error(`Attendee with id ${id} not found, but is a shareholder`);
}
if (!attendee.arrival || !attendee.departure) {
console.log(`Shareholder ${shareholder.first_name} is missing arrival or departure date`);
return;
}
const taxVariables = await injector
.get(BusinessTripTaxVariablesProvider)
.getTaxVariablesByDateLoader.load(businessTrip.toDate);
if (!taxVariables) {
throw new GraphQLError(`Tax variables are not set for date ${businessTrip.toDate}`);
}
const totalDays = differenceInDays(attendee.departure, attendee.arrival) + 1;
const accommodatedNights = shareholdersAccommodatedNightsMap.get(id) ?? 0;
const unAccommodatedDays = accommodatedNights
? totalDays - accommodatedNights - 1
: totalDays;
const maxTaxableAmount = getAttendeeTravelAndSubsistenceMaxTax(totalDays, unAccommodatedDays, businessTrip.destination, taxVariables) * -1;
const shareholderPayedTnSExpenses = attendeePayedTnSExpenses.filter(expense => expense.employee_business_id === id);
let payedAmount = 0;
if (shareholderPayedTnSExpenses.length) {
await Promise.all(shareholderPayedTnSExpenses.map(async (expense) => {
const { amount, currency, date } = getExpenseCoreData(expense);
if (expense.currency === defaultCryptoConversionFiatCurrency) {
payedAmount += amount;
}
else {
const rate = await injector
.get(ExchangeProvider)
.getExchangeRates(currency, defaultCryptoConversionFiatCurrency, new Date(date));
payedAmount += amount * rate;
}
}));
}
if (payedAmount > maxTaxableAmount) {
return; // Shareholder already got payed more than the maximum taxable amount
}
shareholdersPotentialAmountToDistribute[id] = maxTaxableAmount - payedAmount;
}));
return shareholdersPotentialAmountToDistribute;
}
//# sourceMappingURL=credit-shareholders-business-trip-travel-and-subsistence.resolver.js.map