penisularhr / src / modules / activity-record / calculate-rest-day-wages-advances.consumer.ts
calculate-rest-day-wages-advances.consumer.ts
Raw
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,
    );
  }
}