UNPKG

@accounter/server

Version:
205 lines 10.6 kB
import { GraphQLError } from 'graphql'; import xlsx from 'node-xlsx'; import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js'; import { ChargesProvider } from '../../charges/providers/charges.provider.js'; import { EmployeesProvider } from '../providers/employees.provider.js'; import { FundsProvider } from '../providers/funds.provider.js'; import { SalariesProvider } from '../providers/salaries.provider.js'; export class SalaryError extends Error { constructor(message) { super(message); } } function getFormattedSalaryMonth(rawDate) { const dateParts = rawDate.split('/'); return `${dateParts[1]}-${dateParts[0]}`; } function getEmployeeIdsFromSheet(dataArray) { return dataArray .map((data, index) => [data, index]) .filter(([data, index]) => { if (index < 2) return false; if (!data || data === '') return false; return true; }); } function normalizeNationalId(nationalId) { const stringNationalId = nationalId.toString(); return stringNationalId.length === 9 ? stringNationalId : '0'.repeat(9 - stringNationalId.length) + stringNationalId; } function validateGenericSalaryData(salaryData) { if (!salaryData) { throw new SalaryError('Missing salary data sheet'); } return salaryData; } function validateRowByCategory(data, category) { const rowNum = data.findIndex(row => Array.isArray(row) && row[1] === category); if (!data[rowNum]) { throw new SalaryError(`Missing ${category} in row ${rowNum + 1}`); } if (!Array.isArray(data[rowNum])) { throw new SalaryError(`Invalid row for ${category} in row ${rowNum + 1}`); } if (!data[rowNum][1] || data[rowNum][1] !== category) { throw new SalaryError(`Invalid category for ${category} in row ${rowNum + 1}`); } return { row: data[rowNum], rowNum }; } function validateColumn(row, columnNum, category, rowNum, nullable = false) { const columns = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; if (!nullable && row[columnNum] == null) { throw new SalaryError(`Missing column ${columns[columnNum]} for ${category} in row ${rowNum + 1}`); } return { cell: row[columnNum], colLetter: columns[columnNum] }; } function validateCell(salaryData, category, columnNum, nullable = false) { const { row, rowNum } = validateRowByCategory(salaryData, category); const { cell, colLetter } = validateColumn(row, columnNum, category, rowNum, nullable); return { cell, colLetter, rowNum }; } function validateNumericCell(salaryData, category, columnNum, nullable = false) { const { cell, colLetter, rowNum } = validateCell(salaryData, category, columnNum, nullable); if (typeof cell !== 'number') { throw new SalaryError(`Invalid ${category} in row ${rowNum + 1}, column ${colLetter}: ${cell}`); } return cell; } export const insertSalaryRecordsFromFile = async (_, { file, chargeId }, { injector }) => { try { const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); const buffer = await file.arrayBuffer(); const workSheetsFromFile = xlsx.parse(buffer); if (!workSheetsFromFile) { throw new SalaryError('No salary data file found'); } const salaryRawData = workSheetsFromFile.find(sheet => sheet.name === 'ריכוז נתוני שכר')?.data; if (!salaryRawData) { throw new SalaryError('No salary data sheet found in file'); } const rawSalaryMonth = salaryRawData[1][3]; const salaryMonth = getFormattedSalaryMonth(rawSalaryMonth); const salaryData = validateGenericSalaryData(salaryRawData); const { row: employeesRow } = validateRowByCategory(salaryData, 'מספר זהות'); const employeesNationalIds = getEmployeeIdsFromSheet(employeesRow); const allEmployeesPromise = injector .get(EmployeesProvider) .getEmployeesByEmployerLoader.load(ownerId) .catch(e => { console.error(e); throw new SalaryError('Failed to get employees for employer'); }); const salaryChargePromise = chargeId ? injector.get(ChargesProvider).getChargeByIdLoader.load(chargeId) : injector .get(ChargesProvider) .generateCharge({ type: 'PAYROLL', ownerId, userDescription: `Salaries ${rawSalaryMonth}`, }) .catch(e => { console.error(e); throw new SalaryError('Failed to generate salary charge'); }); const allFundsPromise = injector.get(FundsProvider).getAllFunds(); const [allEmployees, salaryCharge, allFunds] = await Promise.all([ allEmployeesPromise, salaryChargePromise, allFundsPromise, ]); if (!salaryCharge) { throw new SalaryError('Failed to get or generate salary charge'); } const salaryRecords = []; for (const [nationalId, employeeColumn] of employeesNationalIds) { const employee = allEmployees.find(e => e.national_id === normalizeNationalId(Number(nationalId))); if (!employee) { throw new SalaryError(`Employee with national id ${nationalId} not found`); } function validateNumericCellWrapper(category, nullable) { return validateNumericCell(salaryData, category, employeeColumn, nullable); } function validateFund(category, nullable) { const { rowNum: prevRowNum } = validateRowByCategory(salaryData, category); const rowNum = prevRowNum + 1; const row = salaryData[rowNum]; if (!Array.isArray(row)) { throw new SalaryError(`Invalid row for ${category} in row ${rowNum + 2}`); } const { cell: fundKey, colLetter } = validateColumn(row, employeeColumn, category, rowNum, nullable); if (typeof fundKey !== 'number') { throw new SalaryError(`Invalid ${category} in row ${rowNum + 1}, column ${colLetter}: ${fundKey}`); } const fundId = allFunds.find(f => Number(f.otsar_id) === fundKey)?.id; if (!fundId) { throw new SalaryError(`Fund with key ${fundKey} not found`); } return fundId; } const record = { ownerId: salaryCharge.owner_id, month: salaryMonth, chargeId: salaryCharge.id, employeeId: employee.business_id, employer: employee.employer, employee: employee.national_id, baseSalary: validateNumericCellWrapper('שכר יסוד'), globalAdditionalHours: validateNumericCellWrapper('שעות נוספות גלובליות', true), hourlyRate: validateNumericCellWrapper('Hourly rate', true), bonus: validateNumericCellWrapper('בונוס', true), gift: validateNumericCellWrapper('מתנות', true), recovery: validateNumericCellWrapper('הבראה', true), vacationTakeout: validateNumericCellWrapper('פדיון חופשה', true), zkufot: validateNumericCellWrapper('שווי קרן השתלמות', true) + validateNumericCellWrapper('שווי קיצבה', true), travelAndSubsistence: validateNumericCellWrapper('החזר הוצאות אש"ל', true), directPaymentAmount: validateNumericCellWrapper('נטו לתשלום'), socialSecurityAmountEmployee: validateNumericCellWrapper('ביטוח לאומי עובד'), socialSecurityAmountEmployer: validateNumericCellWrapper('ביטוח לאומי מעסיק'), healthPaymentAmount: validateNumericCellWrapper('דמי בריאות'), taxAmount: validateNumericCellWrapper('מס הכנסה'), trainingFundId: validateFund('קרן השתלמות'), trainingFundEmployeeAmount: validateNumericCellWrapper('ניכוי קה"ש עובד'), trainingFundEmployeePercentage: validateNumericCellWrapper('אחוז קה"ש עובד') * 100, trainingFundEmployerAmount: validateNumericCellWrapper('סכום קה"ש מעסיק'), trainingFundEmployerPercentage: validateNumericCellWrapper('אחוז קה"ש מעסיק') * 100, pensionFundId: validateFund('קרן פנסיה'), pensionEmployeeAmount: validateNumericCellWrapper('ניכוי פנסיה עובד'), pensionEmployeePercentage: validateNumericCellWrapper('אחוז פנסיה עובד') * 100, pensionEmployerAmount: validateNumericCellWrapper('סכום פנסיה מעסיק'), pensionEmployerPercentage: validateNumericCellWrapper('אחוז פנסיה מעסיק') * 100, compensationsEmployerAmount: validateNumericCellWrapper('סכום פיצויים'), compensationsEmployerPercentage: validateNumericCellWrapper('אחוז פיצויים') * 100, workDays: validateNumericCellWrapper('ימי עבודה'), hours: validateNumericCellWrapper('שעות עבודה'), jobPercentage: validateNumericCellWrapper('Job percentage') * 100, vacationDaysBalance: -1 * validateNumericCellWrapper('ניצול חופשה', true), sicknessDaysBalance: -1 * validateNumericCellWrapper('ניצול מחלה', true), // not currently in use: addedVacationDays: null, }; salaryRecords.push(record); } await injector .get(SalariesProvider) .insertSalaryRecords({ salaryRecords }) .catch(e => { console.error(e); throw new SalaryError('Failed to insert salary records from file'); }); return true; } catch (e) { if (e instanceof SalaryError) { throw new GraphQLError(e.message); } console.error(e); throw new GraphQLError('Failed to insert salary records from file'); } }; //# sourceMappingURL=insert-salary-records-from-file.resolvers.js.map