@accounter/server
Version:
290 lines (258 loc) • 9.18 kB
text/typescript
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;
}