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; } }