import { OnQueueCompleted, OnQueueFailed, Process, Processor, } from '@nestjs/bull'; import { Logger } from '@nestjs/common'; import { DoneCallback, Job } from 'bull'; import moment from 'moment'; import { AdminConfigService } from '../admin-config/admin-config.service'; import { type EmployeeEntity } from '../employee/employee.entity'; import { MonthlyRecordService } from '../monthly-record/monthly-record.service'; import { ActivityRecordService } from './activity-record.service'; @Processor('advanceAndRestDayWages') export class CalculateAdvanceConsumer { private readonly logger = new Logger(CalculateAdvanceConsumer.name); constructor( private activityRecordService: ActivityRecordService, private monthlyRecordService: MonthlyRecordService, private adminConfigService: AdminConfigService, ) {} @Process('processAdvanceAndRestDayWagesCalculation') // eslint-disable-next-line sonarjs/cognitive-complexity async handleAdvanceAndRestDayWagesCalculationJob( job: Job<{ employee: EmployeeEntity; activityDate: Date; }>, done: DoneCallback, ) { try { const { id, data } = job; const { employee, activityDate } = data; // NOTE: Hard code not to compute Feb data if (activityDate <= new Date('2024-01-31')) { return done(null, data); } const adminConfigEntity = await this.adminConfigService.findOne(); const currentStartOfMonth = moment .utc(activityDate) .startOf('month') .toDate(); const currentEndOfMonth = moment .utc(activityDate) .endOf('month') .toDate(); // 1. Check if there is monthly record let employeeMonthlyRecord = await this.monthlyRecordService.findOne({ employee: { id: employee.id }, date: currentStartOfMonth, }); if (!employeeMonthlyRecord) { employeeMonthlyRecord = await this.monthlyRecordService.createMonthlyRecordByFunction( employee, { date: currentStartOfMonth, restDayWages: 0 }, ); } if (!employeeMonthlyRecord) { return done(null, data); } let advance = 0; // 2. Check if total hour >= required days const employeeTotalHour = await this.activityRecordService.getEmployeeWorkingHour(employee, { startDate: currentStartOfMonth, endDate: moment .utc(currentStartOfMonth) .add(adminConfigEntity.advanceLastExecutionDate, 'day') .toDate(), }); // 3. Check if wages >= 750 const employeeWages = await this.activityRecordService.getEmployeeSumRecordByDay(employee, { startDate: currentStartOfMonth, endDate: moment .utc(currentStartOfMonth) .add(adminConfigEntity.advanceLastExecutionDate, 'day') .toDate(), }); // 4. If both condition met, advance = 500 if ( employeeTotalHour >= adminConfigEntity.advanceRequiredDays * 8 && employeeWages >= adminConfigEntity.advanceRequiredWages ) { advance = adminConfigEntity.advanceAmount; } // 5. Calculate rest day wages let loopStartDate = moment .utc(currentStartOfMonth) .startOf('isoWeek') .toDate(); let loopEndDate = moment .utc(loopStartDate) .endOf('isoWeek') .startOf('day') .toDate(); let restDayWages = 0; while (loopEndDate <= currentEndOfMonth) { // 6. Check the number of employee's working hours const workingHours = // eslint-disable-next-line no-await-in-loop await this.activityRecordService.getEmployeeWorkingHour(employee, { startDate: loopStartDate, endDate: loopEndDate, }); // 7. If working days >= 6, count as rest day wages if ( workingHours >= adminConfigEntity.restDayWagesRequiredDaysInWeek * 8 ) { restDayWages += employee.dailyRateAmount; } loopStartDate = moment.utc(loopStartDate).add(7, 'day').toDate(); loopEndDate = moment.utc(loopEndDate).add(7, 'day').toDate(); // 8. If next month fall within that weeks and fulfill requirement, insert data if ( activityDate >= loopStartDate && activityDate <= loopEndDate && moment .utc(loopEndDate) .diff(moment.utc(currentStartOfMonth), 'month') > 0 ) { const lastWeekWorkingHours = // eslint-disable-next-line no-await-in-loop await this.activityRecordService.getEmployeeWorkingHour(employee, { startDate: loopStartDate, endDate: loopEndDate, }); // eslint-disable-next-line no-await-in-loop const nextMonthlyRecord = await this.monthlyRecordService.findOne({ employee: { id: employee.id }, date: moment(currentStartOfMonth).add(1, 'month').toDate(), }); // eslint-disable-next-line unicorn/prefer-ternary if (nextMonthlyRecord) { // eslint-disable-next-line no-await-in-loop await this.monthlyRecordService.updateMonthlyRecord( nextMonthlyRecord, { restDayWages: lastWeekWorkingHours >= adminConfigEntity.restDayWagesRequiredDaysInWeek * 8 ? employee.dailyRateAmount : 0, }, ); } else { // eslint-disable-next-line no-await-in-loop await this.monthlyRecordService.createMonthlyRecordByFunction( employee, { date: moment(currentStartOfMonth).add(1, 'month').toDate(), restDayWages: lastWeekWorkingHours >= adminConfigEntity.restDayWagesRequiredDaysInWeek * 8 ? employee.dailyRateAmount : 0, }, ); } } } // 9. Update monthly record await this.monthlyRecordService.updateMonthlyRecord( employeeMonthlyRecord, { advance, restDayWages, }, ); done(null, data); this.logger.log( `Advance and rest day wages calculation processed successfully: Job #${id}`, ); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorStack = error instanceof Error ? error.stack : undefined; this.logger.error( `Failed to process Advance and rest day wages Calculation: ${errorMessage}`, errorStack, ); const errorToProcess = error instanceof Error ? error : new Error(errorMessage); done(errorToProcess); } } @OnQueueCompleted() onCompleted(job: Job<{ refereeId: Uuid }>) { this.logger.log( `Completed Advance and rest day wages Calculation job #${job.id}`, ); } @OnQueueFailed() onFailed(job: Job, error: Error) { this.logger.error( `Failed Advance and rest day wages Calculation job #${job.id}: ${error.message}`, error.stack, ); } }