UNPKG

@accounter/server

Version:
199 lines • 9.44 kB
import { differenceInYears, endOfDay, endOfMonth, endOfYear } from 'date-fns'; import { GraphQLError } from 'graphql'; import { AVERAGE_MONTHLY_WORK_HOURS } from '../../../shared/constants.js'; import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js'; import { EmployeesProvider } from '../../salaries/providers/employees.provider.js'; import { RecoveryProvider } from '../../salaries/providers/recovery.provider.js'; import { SalariesProvider } from '../../salaries/providers/salaries.provider.js'; import { LedgerProvider } from '../providers/ledger.provider.js'; function recoveryDaysPerYearsOfExperience(years) { if (years === 1) { return 5; } if (years >= 2 && years <= 3) { return 6; } if (years >= 4 && years <= 10) { return 7; } if (years >= 11 && years <= 15) { return 8; } if (years >= 16 && years <= 19) { return 9; } if (years >= 20) { return 10; } return 0; } // returns the parts of the month that the employee worked, first is pre-anniversary function calculateMonthPart(year, month, startDate, endDate) { const isAnniversaryMonth = startDate.getMonth() + 1; const isTerminationMonth = endDate && endDate.getFullYear() === year && endDate.getMonth() + 1 === month; if (isAnniversaryMonth) { if (isTerminationMonth) { // case both anniversary month and termination month const isPreAnniversaryTermination = endDate.getDate() < startDate.getDate(); const tillTerminationPart = (endDate.getDate() - 1) / endOfMonth(endDate).getDate(); if (isPreAnniversaryTermination) { return [tillTerminationPart, 0]; } const inBetweenPart = (endDate.getDate() - startDate.getDate()) / endOfMonth(endDate).getDate(); return [tillTerminationPart - inBetweenPart, inBetweenPart]; } const part1 = (startDate.getDate() - 1) / endOfMonth(startDate).getDate(); return [part1, 1 - part1]; } if (isTerminationMonth) { const part1 = (endDate.getDate() - 1) / endOfMonth(endDate).getDate(); return [part1, 0]; } // TODO: calculate working days / unpaid leave days / any other relevant factor return [1, 0]; } export async function calculateRecoveryReserveAmount(injector, year) { const { ownerId, salaries: { recoveryReserveExpensesTaxCategoryId, recoveryReserveTaxCategoryId }, } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); if (!recoveryReserveExpensesTaxCategoryId) { throw new GraphQLError('Recovery reserve expenses tax category is not set'); } if (!recoveryReserveTaxCategoryId) { throw new GraphQLError('Recovery reserve tax category is not set'); } const salariesPromise = injector .get(SalariesProvider) .getSalaryRecordsByDates({ fromDate: '2000-01', toDate: `${year}-12` }); const employeesPromise = injector .get(EmployeesProvider) .getEmployeesByEmployerLoader.load(ownerId); const recoveryDataPromise = injector .get(RecoveryProvider) .getRecoveryData() .then(data => { const recoveryDayValueByYear = new Map(); for (const record of data) { recoveryDayValueByYear.set(record.year, Number(record.day_value)); } return recoveryDayValueByYear; }); const recoveryLedgerRecordsPromise = injector .get(LedgerProvider) .getLedgerRecordsByFinancialEntityIdLoader.load(recoveryReserveTaxCategoryId); const [salaries, employees, recoveryDayValueByYear, recoveryLedgerRecords] = await Promise.all([ salariesPromise, employeesPromise, recoveryDataPromise, recoveryLedgerRecordsPromise, ]); const employeeMap = new Map(); for (const employee of employees) { if (employee.start_work_date.getFullYear() > year) { continue; } if (employee.end_work_date && employee.end_work_date.getTime() < endOfYear(new Date(`${year}-01-01`)).getTime()) { continue; } employeeMap.set(employee.business_id, { startDate: employee.start_work_date, endDate: employee.end_work_date, salaries: {}, payedRecoveryAmount: 0, totalRecoveryAmount: 0, employee, }); } for (const salaryRecord of salaries) { // skip if employee is not found if (!employeeMap.has(salaryRecord.employee_id)) { continue; } const employee = employeeMap.get(salaryRecord.employee_id); // skip if salary record outside of employee's employment dates if (employee.startDate.getTime() > endOfMonth(new Date(`${salaryRecord.month}-01`)).getTime()) { console.log(`salary record of employee ${employee.employee.first_name} before start date - ${employee.startDate}`); continue; } if (employee.endDate && employee.endDate.getTime() < new Date(`${salaryRecord.month}-01`).getTime()) { console.log(`salary record of employee ${employee.employee.first_name} after end date - ${employee.endDate}`); continue; } const [yearString, monthString] = salaryRecord.month.split('-'); // validate year const year = Number(yearString); if (Number.isNaN(year)) { throw new GraphQLError(`Year is not valid, got ${yearString}`); } if (year < 2000) { throw new GraphQLError(`Year is below coverage limit (2000), got ${yearString}`); } // validate month const month = Number(monthString); if (Number.isNaN(month) || month < 1 || month > 12) { throw new GraphQLError(`Month is not valid, got ${monthString}`); } const yearForRecoveryValue = month >= 7 ? year + 1 : year; let dayValue = recoveryDayValueByYear.get(yearForRecoveryValue); // in case of no recovery day value for the year, try to get it from the previous year (yearly value is published in delay) if (!dayValue && yearForRecoveryValue >= new Date().getFullYear()) { dayValue = recoveryDayValueByYear.get(year); } if (!dayValue) { throw new GraphQLError(`No recovery day value for year ${yearForRecoveryValue}`); } const isAnniversaryMonth = employee.startDate.getMonth() + 1 === month; const isLastSalary = employee.endDate && employee.endDate.getFullYear() === year && employee.endDate.getMonth() + 1 === month; const isHourlyPayed = Number.isNaN(Number(salaryRecord.job_percentage)) || Number(salaryRecord.job_percentage) === 0; const shouldCalculateMonthParts = (isAnniversaryMonth || isLastSalary) && !isHourlyPayed; const monthParts = shouldCalculateMonthParts ? calculateMonthPart(year, month, employee.startDate, employee.endDate) : [1, 0]; const yearsWorked = differenceInYears(endOfMonth(new Date(`${year}-${month}-01`)), employee.startDate) + (isAnniversaryMonth && !isHourlyPayed ? 0 : 1); const recoveryDays = recoveryDaysPerYearsOfExperience(yearsWorked) / 12; const jobPercentage = isHourlyPayed ? Number(salaryRecord.hours) / AVERAGE_MONTHLY_WORK_HOURS : Number(salaryRecord.job_percentage) / 100; employee.totalRecoveryAmount += recoveryDays * monthParts[0] * jobPercentage * dayValue; if (isAnniversaryMonth && !isHourlyPayed) { const recoveryDays = recoveryDaysPerYearsOfExperience(yearsWorked + 1) / 12; employee.totalRecoveryAmount += recoveryDays * monthParts[1] * jobPercentage * dayValue; } employee.salaries[year] ??= {}; employee.salaries[year][month] = salaryRecord; if (salaryRecord.recovery) { employee.payedRecoveryAmount += Number(salaryRecord.recovery); } } let recoveryReserveAmount = 0; let reservePrompt = `Recovery reserve for ${year}:`; for (const employeeData of Array.from(employeeMap.values())) { const employeeReserve = employeeData.totalRecoveryAmount - employeeData.payedRecoveryAmount; reservePrompt += `\n- Employee ${employeeData.employee.first_name} reserve: ${employeeReserve}`; recoveryReserveAmount += employeeReserve; } const prevRecoveryReserveAmount = recoveryLedgerRecords.reduce((acc, record) => { if (endOfDay(record.value_date).getTime() >= new Date(year, 11, 31).getTime()) { return acc; } let factor = 0; if (record.credit_entity1 === recoveryReserveTaxCategoryId || record.debit_entity1 === recoveryReserveExpensesTaxCategoryId) { factor = 1; } else if (record.credit_entity1 === recoveryReserveExpensesTaxCategoryId || record.debit_entity1 === recoveryReserveTaxCategoryId) { factor = -1; } return acc + Number(record.credit_local_amount1) * factor; }, 0); recoveryReserveAmount -= prevRecoveryReserveAmount; reservePrompt += `\n- Prev reserve: ${prevRecoveryReserveAmount}`; console.debug(reservePrompt); return { recoveryReserveAmount }; } //# sourceMappingURL=recovery-reserve.helper.js.map