penisularhr / src / modules / incentive / incentive-record.service.ts
incentive-record.service.ts
Raw
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import moment from 'moment';
import { type FindOptionsWhere, Repository } from 'typeorm';

import { type PageDto } from '../../common/dto/page.dto';
import {
  IncentiveName,
  IncentivePeriod,
  IncentiveType,
  OperateType,
} from '../../constants';
import { ActivityRecordService } from '../activity-record/activity-record.service';
import { AuditLogService } from '../audit-log/audit-log.service';
import { type EmployeeEntity } from '../employee/employee.entity';
import { EmployeeService } from '../employee/employee.service';
import { OtRecordService } from '../ot/ot-record.service';
import { type GetByEmployeeDto } from '../report/dtos/get-report-by-employee.dto';
import { type GetIncentiveDto } from '../report/dtos/get-report-incentive.dto';
import { type UserEntity } from '../user/user.entity';
import { UserService } from '../user/user.service';
import { type IncentiveRecordPageOptionsDto } from './dtos/get-incentive-record-page.dto';
import { type IncentiveRecordDto } from './dtos/incentive-record.dto';
import { type UpdateIncentiveRecordDto } from './dtos/update-incentive-record.dto';
import { IncentiveRecordEntity } from './incentive-record.entity';
import { IncentiveSettingService } from './incentive-settings.service';

@Injectable()
export class IncentiveRecordService {
  constructor(
    @InjectRepository(IncentiveRecordEntity)
    private incentiveRecordReposiory: Repository<IncentiveRecordEntity>,
    @Inject(forwardRef(() => OtRecordService))
    private otRecordService: OtRecordService,
    @Inject(forwardRef(() => ActivityRecordService))
    private activityRecordService: ActivityRecordService,
    private incentiveSettingService: IncentiveSettingService,
    private employeeService: EmployeeService,
    private userService: UserService,
    private auditLogService: AuditLogService,
  ) {}

  async findOne(
    findData: FindOptionsWhere<IncentiveRecordEntity>,
  ): Promise<IncentiveRecordEntity | null> {
    const queryBuilder =
      this.incentiveRecordReposiory.createQueryBuilder('incentiveRecord');

    queryBuilder.where(findData);

    queryBuilder
      .leftJoin('incentiveRecord.employee', 'employee')
      .addSelect(['employee.name']);

    queryBuilder
      .leftJoin('incentiveRecord.incentiveSetting', 'incentiveSetting')
      .addSelect(['incentiveSetting.name', 'incentiveSetting.threshold']);

    return queryBuilder.getOne();
  }

  async findOneByFunction(options: {
    date: Date;
    incentiveSettingName: string;
    employeeId: Uuid;
  }): Promise<IncentiveRecordEntity | null> {
    const { date, employeeId, incentiveSettingName } = options;

    const queryBuilder =
      this.incentiveRecordReposiory.createQueryBuilder('incentiveRecord');

    queryBuilder
      .leftJoin('incentiveRecord.employee', 'employee')
      .addSelect(['employee.name']);

    queryBuilder
      .leftJoin('incentiveRecord.incentiveSetting', 'incentiveSetting')
      .addSelect(['incentiveSetting.name', 'incentiveSetting.threshold']);

    queryBuilder.where('incentiveRecord.date = :date', { date });
    queryBuilder.andWhere('employee.id = :employeeId', { employeeId });
    queryBuilder.andWhere('incentiveSetting.name = :incentiveSettingName', {
      incentiveSettingName,
    });

    return queryBuilder.getOne();
  }

  async findMany(
    pageOptionsDto: IncentiveRecordPageOptionsDto,
  ): Promise<PageDto<IncentiveRecordDto>> {
    const { order, date, employeeName, incentiveName, id } = pageOptionsDto;

    const queryBuilder =
      this.incentiveRecordReposiory.createQueryBuilder('incentiveRecord');

    queryBuilder
      .leftJoin('incentiveRecord.employee', 'employee')
      .addSelect(['employee.name']);

    queryBuilder
      .leftJoin('incentiveRecord.incentiveSetting', 'incentiveSetting')
      .addSelect(['incentiveSetting.name', 'incentiveSetting.unit']);

    if (id) {
      queryBuilder.andWhere('incentiveRecord.id = :id', { id });
    }

    if (date) {
      queryBuilder.andWhere('incentiveRecord.date = :date', { date });
    }

    if (employeeName) {
      queryBuilder.searchByString(employeeName, ['employee.name']);
    }

    if (incentiveName) {
      queryBuilder.andWhere('incentiveSetting.name = :incentiveName', {
        incentiveName,
      });
    }

    queryBuilder.orderBy('incentiveRecord.date', order);

    const [items, pageMetaDto] = await queryBuilder.paginate(pageOptionsDto);

    return items.toPageDto(pageMetaDto);
  }

  async findReportMany(dto: GetIncentiveDto) {
    const { employeeName, incentiveName, monthFrom, monthTo, order, year } =
      dto;

    const currentYear = year ?? moment.utc().year();

    const queryBuilder =
      this.incentiveRecordReposiory.createQueryBuilder('incentiveRecord');

    queryBuilder
      .leftJoin('incentiveRecord.employee', 'employee')
      .addSelect(['employee.name']);

    queryBuilder
      .leftJoin('incentiveRecord.incentiveSetting', 'incentiveSetting')
      .addSelect(['incentiveSetting.name', 'incentiveSetting.unit']);

    if (employeeName) {
      queryBuilder.andWhere('employee.name = :employeeName', { employeeName });
    }

    if (incentiveName) {
      queryBuilder.andWhere('incentiveSetting.name = :incentiveName', {
        incentiveName,
      });
    }

    let fromDate: Date | undefined;
    let toDate: Date | undefined;

    if (monthFrom) {
      fromDate = moment
        .utc({
          year: currentYear,
          month: monthFrom - 1,
          date: 1,
        })
        .toDate();

      queryBuilder.andWhere('incentiveRecord.date >= :fromDate', { fromDate });
    }

    if (monthTo) {
      toDate = moment
        .utc({ year: currentYear, month: monthTo - 1, date: 1 })
        .add(1, 'months')
        .toDate();

      queryBuilder.andWhere('incentiveRecord.date < :toDate', { toDate });
    }

    queryBuilder.orderBy('incentiveRecord.date', order);

    const response = await queryBuilder.getMany();

    return response.map((el) => el.toDto());
  }

  async updateIncentiveRecordByAdmin(
    incentiveRecordEntity: IncentiveRecordEntity,
    dto: UpdateIncentiveRecordDto,
    user: UserEntity,
  ): Promise<IncentiveRecordEntity> {
    const { amount, quantity, threshold } = dto;

    const oldValue = JSON.stringify({ ...incentiveRecordEntity });
    let summaryChanges = 'UPDATE ';

    if (amount !== undefined) {
      summaryChanges +=
        incentiveRecordEntity.amount === amount
          ? ''
          : `Amount %:${incentiveRecordEntity.amount} -> ${amount}, `;

      incentiveRecordEntity.amount = amount;
    }

    if (quantity !== undefined) {
      summaryChanges +=
        incentiveRecordEntity.quantity === quantity
          ? ''
          : `Quantity %:${incentiveRecordEntity.quantity} -> ${quantity}, `;

      incentiveRecordEntity.quantity = quantity;
    }

    if (threshold !== undefined) {
      summaryChanges +=
        incentiveRecordEntity.threshold === threshold
          ? ''
          : `Threshold %:${incentiveRecordEntity.threshold} -> ${threshold}, `;

      incentiveRecordEntity.threshold = threshold;
    }

    const updatedEntity = await this.incentiveRecordReposiory.save(
      incentiveRecordEntity,
    );

    await this.auditLogService.create({
      itemId: updatedEntity.id,
      newValue: JSON.stringify(updatedEntity),
      oldValue,
      operateType: OperateType.UPDATE,
      summaryChanges,
      tableName: 'incentiveRecord',
      user,
    });

    return incentiveRecordEntity;
  }

  // eslint-disable-next-line sonarjs/cognitive-complexity
  async createOrUpdateIncentiveByFunction(
    employee: EmployeeEntity,
    options: { incentiveName: IncentiveName; date: Date },
  ): Promise<IncentiveRecordEntity | void> {
    const { date, incentiveName } = options;

    // 1. Check if incentive setting exist
    let incentiveSettingEntity = await this.incentiveSettingService.findOne({
      name: incentiveName,
    });

    if (!incentiveSettingEntity) {
      return;
    }

    // 2. Check if monthly or daily record
    const recordDate =
      incentiveSettingEntity.period === IncentivePeriod.MONTHLY
        ? moment.utc(date).startOf('month')
        : moment.utc(date).startOf('date');

    // 3. Check if current record exist
    let currentRecord = await this.findOneByFunction({
      date: recordDate.toDate(),
      incentiveSettingName: incentiveName,
      employeeId: employee.id,
    });

    // 4. If monthly record exits, return
    if (currentRecord?.incentiveSetting.name === IncentiveName.LONG_SERVICE) {
      return currentRecord;
    }

    // 5. Calculate quantity
    let quantity = 0;
    let referral: EmployeeEntity | null = null;

    // eslint-disable-next-line unicorn/prefer-ternary, unicorn/prefer-switch
    if (incentiveName === IncentiveName.MEAL) {
      quantity = await this.otRecordService.getOtQuantity(
        recordDate.toDate(),
        employee,
      );
    } else if (incentiveName === IncentiveName.PERFECT_ATTENDANCE) {
      quantity = await this.activityRecordService.getEmployeeWorkingHour(
        employee,
        {
          startDate: recordDate.toDate(),
          endDate: moment.utc(recordDate).add(1, 'month').toDate(),
        },
      );
    } else if (incentiveName === IncentiveName.LONG_SERVICE) {
      quantity = Math.floor(
        recordDate.diff(moment.utc(employee.dateJoin), 'days') / 365,
      );
    } else if (incentiveName === IncentiveName.REFERRAL) {
      quantity = Math.floor(
        recordDate.diff(moment.utc(employee.dateJoin), 'months'),
      );

      referral = await this.employeeService.findOne({
        name: employee.referralBy,
      });
    } else {
      quantity = await this.activityRecordService.getEmployeeIncentiveQuantity(
        recordDate.toDate(),
        employee,
        incentiveName,
      );
    }

    // 6. Calculate threshold
    let threshold = currentRecord
      ? currentRecord.threshold
      : incentiveSettingEntity.threshold;

    if (incentiveName === IncentiveName.LONG_SERVICE) {
      incentiveSettingEntity =
        await this.incentiveSettingService.findHighestLongService(quantity);

      if (!incentiveSettingEntity) {
        return;
      }

      threshold = currentRecord
        ? currentRecord.threshold
        : incentiveSettingEntity.threshold;
    }

    // 7. Calculate amount
    let amount = 0;

    if (incentiveSettingEntity.type === IncentiveType.FIXED_AMOUNT) {
      amount = quantity >= threshold ? incentiveSettingEntity.amount : 0;

      // Sucker harvest/planting need quantity / threshold * amount
      if (
        incentiveSettingEntity.name === IncentiveName.SUCKER_HARVEST ||
        incentiveSettingEntity.name === IncentiveName.SUCKER_PLANTING
      ) {
        amount =
          quantity >= threshold
            ? Math.floor(quantity / incentiveSettingEntity.threshold) *
              incentiveSettingEntity.amount
            : 0;
      }
    } else {
      amount =
        quantity >= threshold ? quantity * incentiveSettingEntity.amount : 0;
    }

    const firstUser = await this.userService.findFirstUser();

    // 8. Update incentive
    if (currentRecord) {
      const oldValue = JSON.stringify({ ...currentRecord });
      let summaryChanges = 'UPDATE ';

      summaryChanges +=
        currentRecord.quantity === quantity
          ? ''
          : `Quantity:${currentRecord.quantity} -> ${quantity}, `;

      summaryChanges +=
        currentRecord.threshold === threshold
          ? ''
          : `Threshold:${currentRecord.threshold} -> ${threshold}, `;

      summaryChanges +=
        currentRecord.amount === amount
          ? ''
          : `Amount:${currentRecord.amount} -> ${amount}, `;

      currentRecord.quantity = quantity;
      currentRecord.threshold = threshold;
      currentRecord.amount = amount;

      await this.auditLogService.create({
        itemId: currentRecord.id,
        newValue: JSON.stringify(currentRecord),
        oldValue,
        operateType: OperateType.UPDATE,
        summaryChanges,
        tableName: 'incentiveRecord',
        user: firstUser!,
      });

      return this.incentiveRecordReposiory.save(currentRecord);
    }

    // 9. Create incentive
    if (!incentiveSettingEntity.isActive || amount <= 0) {
      return;
    }

    if (incentiveName === IncentiveName.REFERRAL) {
      await this.employeeService.updateEmployee(employee, {
        referralFeePaidAt: recordDate.toDate(),
      });
    }

    currentRecord = this.incentiveRecordReposiory.create({
      amount,
      date: recordDate.toDate(),
      employee: referral ?? employee,
      incentiveSetting: incentiveSettingEntity,
      quantity,
      threshold,
    });

    const createdEntity =
      await this.incentiveRecordReposiory.save(currentRecord);

    await this.auditLogService.create({
      itemId: createdEntity.id,
      newValue: JSON.stringify(createdEntity),
      oldValue: null,
      operateType: OperateType.CREATE,
      summaryChanges: 'CREATE incentive-record',
      tableName: 'incentiveRecord',
      user: firstUser!,
    });

    return currentRecord;
  }

  async deleteIncentiveRecord(
    employee: EmployeeEntity,
    options: { incentiveName: IncentiveName; date: Date },
    user: UserEntity,
  ): Promise<void> {
    const { date, incentiveName } = options;

    // 2. Check if monthly or daily record
    const recordDate = moment.utc(date).startOf('date');

    // 3. Check if current record exist
    const currentRecord = await this.findOneByFunction({
      date: recordDate.toDate(),
      incentiveSettingName: incentiveName,
      employeeId: employee.id,
    });

    if (!currentRecord) {
      return;
    }

    await this.auditLogService.create({
      itemId: currentRecord.id,
      newValue: null,
      oldValue: JSON.stringify(currentRecord),
      operateType: OperateType.DELETE,
      summaryChanges: 'DELETE incentive-record',
      tableName: 'incentiveRecord',
      user,
    });

    await this.incentiveRecordReposiory.remove(currentRecord);
  }

  async getSummaryIncentiveByEmployee(
    dto: GetByEmployeeDto,
    employee: EmployeeEntity,
  ) {
    const { monthFrom, monthTo, year, incentiveName } = dto;

    const currentYear = year ?? moment.utc().year();

    const queryBuilder = this.incentiveRecordReposiory
      .createQueryBuilder('incentiveRecord')
      .select('SUM(incentiveRecord.amount)', 'sumAmount')
      .leftJoin('incentiveRecord.employee', 'employee')
      .addSelect('employee.id', 'employeeId')
      .where('employee.id = :id', { id: employee.id });

    queryBuilder.groupBy('employee.id');

    if (monthFrom) {
      const fromDate = moment
        .utc({
          year: currentYear,
          month: monthFrom - 1,
          date: 1,
        })
        .toDate();

      queryBuilder.andWhere('incentiveRecord.date >= :fromDate', { fromDate });
    }

    if (monthTo) {
      const toDate = moment
        .utc({ year: currentYear, month: monthTo - 1, date: 1 })
        .add(1, 'months')
        .toDate();

      queryBuilder.andWhere('incentiveRecord.date < :toDate', { toDate });
    }

    if (incentiveName) {
      queryBuilder
        .leftJoin('incentiveRecord.incentiveSetting', 'incentiveSetting')
        .addSelect('incentiveSetting.name', 'incentiveName')
        .andWhere('incentiveSetting.name = :incentiveName', {
          incentiveName,
        });

      queryBuilder.addGroupBy('incentiveSetting.name');
    }

    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    let returnData = await queryBuilder.getRawOne();

    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    returnData = returnData
      ? {
          ...returnData,
          // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
          sumAmount: Number(returnData.sumAmount),
        }
      : {
          employeeId: employee.id,
          incentiveName,
          sumAmount: 0,
        };

    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
    return returnData;
  }
}