UNPKG

@accounter/server

Version:

290 lines (258 loc) • 9.18 kB
import { GraphQLError } from 'graphql'; import type { IGetChargesByIdsResult } from '@modules/charges/types'; import type { IGetSalaryRecordsByChargeIdsResult } from '@modules/salaries/types.js'; import { COMPENSATION_FUND_EXPENSES_TAX_CATEGORY_ID, DEFAULT_LOCAL_CURRENCY, PENSION_EXPENSES_TAX_CATEGORY_ID, SALARY_EXPENSES_TAX_CATEGORY_ID, SOCIAL_SECURITY_BUSINESS_ID, SOCIAL_SECURITY_EXPENSES_TAX_CATEGORY_ID, TAX_DEDUCTIONS_BUSINESS_ID, TRAINING_FUND_EXPENSES_TAX_CATEGORY_ID, ZKUFOT_EXPENSES_TAX_CATEGORY_ID, ZKUFOT_INCOME_TAX_CATEGORY_ID, } from '@shared/constants'; import type { LedgerProto } from '@shared/types'; function generateEntryRaw( accountId: string, amount: number, month: string, transactionDate: Date, ownerId: string, isCreditor: boolean, chargeId: string, ): LedgerProto { return { id: `${accountId}|${month}`, invoiceDate: transactionDate, valueDate: transactionDate, currency: DEFAULT_LOCAL_CURRENCY, ...(isCreditor ? { creditAccountID1: accountId } : { debitAccountID1: accountId }), localCurrencyCreditAmount1: amount, localCurrencyDebitAmount1: amount, description: `${month} salary: ${accountId}`, isCreditorCounterparty: false, ownerId, chargeId, }; } function getTotalPension(salaryRecord: IGetSalaryRecordsByChargeIdsResult): number { const pensionEmployerAmount = Number(salaryRecord.pension_employer_amount ?? '0'); const pensionEmployeeAmount = Number(salaryRecord.pension_employee_amount ?? '0'); const compensationsEmployerAmount = Number(salaryRecord.compensations_employer_amount ?? '0'); const totalPension = pensionEmployeeAmount + pensionEmployerAmount + compensationsEmployerAmount; return totalPension; } function getTotalTrainingFund(salaryRecord: IGetSalaryRecordsByChargeIdsResult) { const trainingFundEmployerAmount = Number(salaryRecord.training_fund_employer_amount ?? '0'); const trainingFundEmployeeAmount = Number(salaryRecord.training_fund_employee_amount ?? '0'); const totalTrainingFund = trainingFundEmployeeAmount + trainingFundEmployerAmount; return totalTrainingFund; } type MonthlyLedgerProto = { taxCategoryId: string; amount: number; isCredit: boolean }; export function generateEntriesFromSalaryRecords( salaryRecords: IGetSalaryRecordsByChargeIdsResult[], charge: IGetChargesByIdsResult, transactionDate: Date, ): { entries: LedgerProto[]; monthlyEntriesProto: MonthlyLedgerProto[]; month: string; } { const chargeId = charge.id; if (!salaryRecords.length) { throw new GraphQLError(`No salary records found for charge ${chargeId}`); } const entries: LedgerProto[] = []; function generateEntry( chargeId: string, accountId: string, amount: number, month: string, isCreditor = true, date?: Date, ) { return generateEntryRaw( accountId, amount, month, date ?? transactionDate, charge.owner_id, isCreditor, chargeId, ); } const amountPerBusiness: Record<string, number> = {}; const taxAmountPerMonth: Record<string, number> = {}; let month: string | undefined = undefined; // expenses let salaryExpensesAmount = 0; let socialSecurityExpensesAmount = 0; let pensionFundExpensesAmount = 0; let compensationProvidentFundExpensesAmount = 0; let trainingFundExpensesAmount = 0; let zkufotAmount = 0; for (const salaryRecord of salaryRecords) { // record validations if (!salaryRecord.base_salary) { throw new GraphQLError( `Base salary record for ${salaryRecord.month}, employee ID=${salaryRecord.employee_id} is missing amount`, ); } month ??= salaryRecord.month; const salaryExpense = Number(salaryRecord.base_salary ?? '0') + Number(salaryRecord.global_additional_hours ?? '0') + Number(salaryRecord.bonus ?? '0') + Number(salaryRecord.gift ?? '0') + Number(salaryRecord.recovery ?? '0') + Number(salaryRecord.vacation_takeout ?? '0'); const directPayment = salaryExpense - Number(salaryRecord.social_security_amount_employee ?? '0') - Number(salaryRecord.health_payment_amount ?? '0') - Number(salaryRecord.tax_amount ?? '0') - Number(salaryRecord.pension_employee_amount ?? '0') - Number(salaryRecord.training_fund_employee_amount ?? '0'); // generate salary entry const salaryDate = new Date(transactionDate); salaryDate.setDate(salaryDate.getDate() - 2); // adjusted date to match exchange rate of transaction initiation date entries.push( generateEntry( charge.id, salaryRecord.employee_id, directPayment, salaryRecord.month, undefined, salaryDate, ), ); // salary expenses handling salaryExpensesAmount += salaryExpense; // social security handling const socialSecurityEmployeeAmount = Number( salaryRecord.social_security_amount_employee ?? '0', ); const socialSecurityEmployerAmount = Number( salaryRecord.social_security_amount_employer ?? '0', ); const HealthPaymentAmount = Number(salaryRecord.health_payment_amount ?? '0'); amountPerBusiness[SOCIAL_SECURITY_BUSINESS_ID] ??= 0; amountPerBusiness[SOCIAL_SECURITY_BUSINESS_ID] += socialSecurityEmployeeAmount + socialSecurityEmployerAmount + HealthPaymentAmount; socialSecurityExpensesAmount += Number(salaryRecord.social_security_amount_employer ?? '0'); // pension handling const totalPension = getTotalPension(salaryRecord); const pensionAccount = salaryRecord.pension_fund_id; if (totalPension > 0) { if (!pensionAccount) { throw new GraphQLError(`Missing pension account for ${chargeId}`); } amountPerBusiness[pensionAccount] ??= 0; amountPerBusiness[pensionAccount] += totalPension; } pensionFundExpensesAmount += Number(salaryRecord.pension_employer_amount ?? '0'); compensationProvidentFundExpensesAmount += Number( salaryRecord.compensations_employer_amount ?? '0', ); // training fund handling const totalTrainingFund = getTotalTrainingFund(salaryRecord); const trainingFundAccount = salaryRecord.training_fund_id; if (totalTrainingFund > 0) { if (!trainingFundAccount) { throw new GraphQLError(`Missing training fund account for ${chargeId}`); } amountPerBusiness[trainingFundAccount] ??= 0; amountPerBusiness[trainingFundAccount] += totalTrainingFund; } trainingFundExpensesAmount += Number(salaryRecord.training_fund_employer_amount ?? '0'); // tax handling taxAmountPerMonth[salaryRecord.month] ??= 0; taxAmountPerMonth[salaryRecord.month] += Number(salaryRecord.tax_amount ?? '0'); // zkufot handling zkufotAmount += Number(salaryRecord.zkufot ?? '0'); } if (!month) { throw new GraphQLError(`No month found for salary charge ${chargeId}`); } // generate pension/training funds entries for (const [businessId, amount] of Object.entries(amountPerBusiness)) { if (amount) { entries.push(generateEntry(charge.id, businessId, amount, month)); } } // generate tax entries for (const [month, amount] of Object.entries(taxAmountPerMonth)) { if (amount > 0) { entries.push(generateEntry(charge.id, TAX_DEDUCTIONS_BUSINESS_ID, amount, month)); } } const monthlyEntriesProto = getMonthlyExpensesEntriesProto({ salaryExpensesAmount, socialSecurityExpensesAmount, pensionFundExpensesAmount, compensationProvidentFundExpensesAmount, trainingFundExpensesAmount, zkufotAmount, }); return { entries, monthlyEntriesProto, month, }; } export function getMonthlyExpensesEntriesProto({ salaryExpensesAmount, socialSecurityExpensesAmount, pensionFundExpensesAmount, compensationProvidentFundExpensesAmount, trainingFundExpensesAmount, zkufotAmount, }: { salaryExpensesAmount: number; socialSecurityExpensesAmount: number; pensionFundExpensesAmount: number; compensationProvidentFundExpensesAmount: number; trainingFundExpensesAmount: number; zkufotAmount: number; }) { const monthlyEntriesProto: MonthlyLedgerProto[] = [ { taxCategoryId: ZKUFOT_EXPENSES_TAX_CATEGORY_ID, amount: zkufotAmount, isCredit: false, }, { taxCategoryId: ZKUFOT_INCOME_TAX_CATEGORY_ID, amount: zkufotAmount, isCredit: true, }, { taxCategoryId: SOCIAL_SECURITY_EXPENSES_TAX_CATEGORY_ID, amount: socialSecurityExpensesAmount, isCredit: false, }, { taxCategoryId: SALARY_EXPENSES_TAX_CATEGORY_ID, amount: salaryExpensesAmount, isCredit: false, }, { taxCategoryId: TRAINING_FUND_EXPENSES_TAX_CATEGORY_ID, amount: trainingFundExpensesAmount, isCredit: false, }, { taxCategoryId: PENSION_EXPENSES_TAX_CATEGORY_ID, amount: pensionFundExpensesAmount, isCredit: false, }, { taxCategoryId: COMPENSATION_FUND_EXPENSES_TAX_CATEGORY_ID, amount: compensationProvidentFundExpensesAmount, isCredit: false, }, ]; return monthlyEntriesProto; }