diff --git a/src/business-logic/sick-leave.service.ts b/src/business-logic/sick-leave.service.ts index e69de29..2057e05 100644 --- a/src/business-logic/sick-leave.service.ts +++ b/src/business-logic/sick-leave.service.ts @@ -0,0 +1,61 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class SickLeaveService { + constructor(private readonly prisma: PrismaService) {} + + private readonly logger = new Logger(SickLeaveService.name); + + async calculateSickLeavePay(employeeId: number, startDate: Date, daysRequested: number, modifier: number): Promise { + //sets the year to jan 1st to dec 31st + const periodStart = new Date(startDate.getFullYear(), 0, 1); + const periodEnd = startDate; + + //fetches all shifts of a selected employee + const shifts = await this.prisma.shifts.findMany({ + where: { + timesheet: { employee_id: employeeId }, + date: { gte: periodStart, lte: periodEnd}, + }, + select: { date: true }, + }); + + //count the amount of worked days + const workedDates = new Set( + shifts.map(shift => shift.date.toISOString().slice(0,10)) + ); + const daysWorked = workedDates.size; + this.logger.debug(`Sick leave: days worked= ${daysWorked} in ${periodStart.toDateString()} -> ${periodEnd.toDateString()}`); + + //less than 30 worked days returns 0 + if (daysWorked < 30) { + return 0; + } + + //default 3 days allowed after 30 worked days + let acquiredDays = 3; + + //identify the date of the 30th worked day + const orderedDates = Array.from(workedDates).sort(); + const thresholdDate = new Date(orderedDates[29]); // index 29 is the 30th day + + //calculate each completed month, starting the 1st of the next month + const firstBonusDate = new Date(thresholdDate.getFullYear(), thresholdDate.getMonth() +1, 1); + let months = (periodEnd.getFullYear() - firstBonusDate.getFullYear()) * 12 + + (periodEnd.getMonth() - firstBonusDate.getMonth()) + 1; + if(months < 0) months = 0; + acquiredDays += months; + + //cap of 10 days + if (acquiredDays > 10) acquiredDays = 10; + + this.logger.debug(`Sick leave: threshold Date = ${thresholdDate.toDateString()}, bonusMonths = ${months}, acquired Days = ${acquiredDays}`); + + const payableDays = Math.min(acquiredDays, daysRequested); + const rawHours = payableDays * 8 * modifier; + const rounded = Math.round(rawHours * 4) / 4; + this.logger.debug(`Sick leave pay: days= ${payableDays}, modifier= ${modifier}, hours= ${rounded}`); + return rounded; + } +} \ No newline at end of file diff --git a/src/business-logic/vacation.service.ts b/src/business-logic/vacation.service.ts index e69de29..094b64d 100644 --- a/src/business-logic/vacation.service.ts +++ b/src/business-logic/vacation.service.ts @@ -0,0 +1,85 @@ +import { Injectable, Logger, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class VacationService { + constructor(private readonly prisma: PrismaService) {} + + private readonly logger = new Logger(VacationService.name); +/** + * Calculate the ammount allowed for vacation days. + * + * @param employeeId employee ID + * @param startDate first day of vacation + * @param daysRequested number of days requested + * @param modifier Coefficient of hours(1) + * @returns amount of payable hours + */ + async calculateVacationPay(employeeId: number, startDate: Date, daysRequested: number, modifier: number): Promise { + //fetch hiring date + const employee = await this.prisma.employees.findUnique({ + where: { id: employeeId }, + select: { first_work_day: true }, + }); + if(!employee) { + throw new NotFoundException(`Employee #${employeeId} not found`); + } + const hireDate = employee.first_work_day; + + //sets "year" to may 1st to april 30th + //check if hiring date is in may or later, we use hiring year, otherwise we use the year before + const yearOfRequest = startDate.getMonth() >= 4 + ? startDate.getFullYear() : startDate.getFullYear() -1; + const periodStart = new Date(yearOfRequest, 4, 1); //may = 4 + const periodEnd = new Date(yearOfRequest + 1, 4, 0); //day 0 of may == april 30th + + this.logger.debug(`Vacation period for request: ${periodStart.toDateString()} -> ${periodEnd.toDateString()}`); + + //steps to reach to get more vacation weeks in years + const checkpoint = [5, 10, 15]; + const anniversaries = checkpoint.map(years => { + const anniversaryDate = new Date(hireDate); + anniversaryDate.setFullYear(anniversaryDate.getFullYear() + years); + return anniversaryDate; + }).filter(d => d>= periodStart && d <= periodEnd).sort((a,b) => a.getTime() - b.getTime()); + + this.logger.debug(`anniversatries steps during the period: ${anniversaries.map(date => date.toDateString()).join(',') || 'aucun'}`); + + const boundaries = [periodStart, ...anniversaries,periodEnd]; + //calculate prorata per segment + let totalVacationDays = 0; + const msPerDay = 1000 * 60 * 60 * 24; + + for (let i = 0; i < boundaries.length -1; i++) { + const segmentStart = boundaries[i]; + const segmentEnd = boundaries[i+1]; + + //number of days in said segment + const daysInSegment = Math.round((segmentEnd.getTime() - segmentStart.getTime())/ msPerDay); + const yearsSinceHire = (segmentStart.getFullYear() - hireDate.getFullYear()) - + (segmentStart < new Date(segmentStart.getFullYear(), hireDate.getMonth()) ? 1 : 0); + let allocDays: number; + if(yearsSinceHire < 5) allocDays = 10; + else if(yearsSinceHire < 10) allocDays = 15; + else if(yearsSinceHire < 15) allocDays = 20; + else allocDays = 25; + + //prorata for said segment + const prorata = (allocDays / 365) * daysInSegment; + totalVacationDays += prorata; + } + //compares allowed vacation pools with requested days + const payableDays = Math.min(totalVacationDays, daysRequested); + + + const rawHours = payableDays * 8 * modifier; + const roundedHours = Math.round(rawHours * 4) / 4; + this.logger.debug(`Vacation pay: entitledDays=${totalVacationDays.toFixed(2)}, requestedDays=${daysRequested}, + payableDays=${payableDays.toFixed(2)}, hours=${roundedHours}`); + + return roundedHours; + } + + + +} \ No newline at end of file diff --git a/src/modules/leave-requests/leave-requests.module.ts b/src/modules/leave-requests/leave-requests.module.ts index 27394bf..f000f18 100644 --- a/src/modules/leave-requests/leave-requests.module.ts +++ b/src/modules/leave-requests/leave-requests.module.ts @@ -3,6 +3,8 @@ import { LeaveRequestController } from "./controllers/leave-requests.controller" import { LeaveRequestsService } from "./services/leave-requests.service"; import { Module } from "@nestjs/common"; import { HolidayService } from "src/business-logic/holiday.service"; +import { VacationService } from "src/business-logic/vacation.service"; +import { SickLeaveService } from "src/business-logic/sick-leave.service"; @Module({ controllers: [LeaveRequestController], @@ -10,6 +12,8 @@ import { HolidayService } from "src/business-logic/holiday.service"; LeaveRequestsService, PrismaService, HolidayService, + VacationService, + SickLeaveService, ], exports: [ LeaveRequestsService], }) diff --git a/src/modules/leave-requests/services/leave-requests.service.ts b/src/modules/leave-requests/services/leave-requests.service.ts index 4e54e49..9ba05cc 100644 --- a/src/modules/leave-requests/services/leave-requests.service.ts +++ b/src/modules/leave-requests/services/leave-requests.service.ts @@ -1,15 +1,19 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto"; import { LeaveRequests, LeaveRequestsArchive } from "@prisma/client"; import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto"; import { HolidayService } from "src/business-logic/holiday.service"; +import { VacationService } from "src/business-logic/vacation.service"; +import { SickLeaveService } from "src/business-logic/sick-leave.service"; @Injectable() export class LeaveRequestsService { constructor( private readonly prisma: PrismaService, private readonly holidayService: HolidayService, + private readonly vacationService: VacationService, + private readonly sickLeaveService: SickLeaveService ) {} async create(dto: CreateLeaveRequestsDto): Promise { @@ -20,7 +24,9 @@ export class LeaveRequestsService { data: { employee_id, bank_code_id, leave_type, start_date_time, end_date_time, comment, approval_status: approval_status ?? undefined }, - include: { employee: { include: { user: true } } }, + include: { employee: { include: { user: true } }, + bank_code: true + }, }); } @@ -31,18 +37,38 @@ export class LeaveRequestsService { }, }); + const msPerDay = 1000 * 60 * 60 * 24; + return Promise.all( list.map(async request => { - if(request.bank_code?.type === 'holiday') { - const cost = await this.holidayService.calculateHolidayPay( - request.employee_id, - request.start_date_time, - request.bank_code.modifier - ); - return { ...request, cost }; + // end_date fallback + const endDate = request.end_date_time ?? request.start_date_time; + + //Requested days + const diffDays = Math.round((endDate.getTime() - request.start_date_time.getTime()) / msPerDay) +1; + + // modifier fallback/validation + if (!request.bank_code || request.bank_code.modifier == null) { + throw new BadRequestException(`Modifier manquant pour bank_code_id=${request.bank_code_id}`); } - return request; - }), + const modifier = request.bank_code.modifier; + + let cost: number; + switch (request.bank_code.type) { + case 'holiday' : + cost = await this.holidayService.calculateHolidayPay( request.employee_id, request.start_date_time, modifier ); + break; + case 'vacation' : + cost = await this.vacationService.calculateVacationPay( request.employee_id, request.start_date_time,diffDays, modifier ); + break; + case 'sick' : + cost = await this.sickLeaveService.calculateSickLeavePay( request.employee_id, request.start_date_time, diffDays, modifier ); + break; + default: + cost = diffDays * modifier; + } + return {...request, daysRequested: diffDays, cost }; + }) ); } @@ -56,17 +82,34 @@ export class LeaveRequestsService { if(!request) { throw new NotFoundException(`LeaveRequest #${id} not found`); } - - //search for leave type. if holiday - if (request.bank_code?.type === 'holiday') { - const cost = await this.holidayService.calculateHolidayPay( - request.employee_id, - request.start_date_time, - request.bank_code.modifier, - ); - return { ...request, cost }; + //validation and fallback for end_date_time + const endDate = request.end_date_time ?? request.start_date_time; + + //calculate included days + const msPerDay = 1000 * 60 * 60 * 24; + const diffDays = Math.floor((endDate.getTime() - request.start_date_time.getTime())/ msPerDay) + 1; + + if (!request.bank_code || request.bank_code.modifier == null) { + throw new BadRequestException(`Modifier missing for code ${request.bank_code_id}`); } - return request; + const modifier = request.bank_code.modifier; + + //calculate cost based on bank_code types + let cost = diffDays * modifier; + switch(request.bank_code.type) { + case 'holiday': + cost = await this.holidayService.calculateHolidayPay( request.employee_id, request.start_date_time, modifier ); + break; + case 'vacation': + cost = await this.vacationService.calculateVacationPay( request.employee_id, request.start_date_time, diffDays, modifier ); + break; + case 'sick': + cost = await this.sickLeaveService.calculateSickLeavePay( request.employee_id, request.start_date_time, diffDays, modifier ); + break; + default: + cost = diffDays * modifier; + } + return {...request, daysRequested: diffDays, cost }; } async update( @@ -78,12 +121,12 @@ export class LeaveRequestsService { return this.prisma.leaveRequests.update({ where: { id }, data: { - ...(employee_id !== undefined && { employee_id }), - ...(leave_type !== undefined && { leave_type } ), + ...(employee_id !== undefined && { employee_id }), + ...(leave_type !== undefined && { leave_type } ), ...(start_date_time !== undefined && { start_date_time }), - ...(end_date_time !== undefined && { end_date_time }), - ...(comment !== undefined && { comment }), - ...(approval_status == undefined && { approval_status }), + ...(end_date_time !== undefined && { end_date_time }), + ...(comment !== undefined && { comment }), + ...(approval_status !== undefined && { approval_status }), }, include: { employee: { include: { user:true } } }, }); @@ -112,14 +155,14 @@ export class LeaveRequestsService { //copy unto archive table await transaction.leaveRequestsArchive.createMany({ data: expired.map(request => ({ - leave_request_id: request.id, - employee_id: request.employee_id, - leave_type: request.leave_type, - start_date_time: request.start_date_time, - end_date_time: request.end_date_time, - comment: request.comment, - approval_status: request.approval_status, - })), + leave_request_id: request.id, + employee_id: request.employee_id, + leave_type: request.leave_type, + start_date_time: request.start_date_time, + end_date_time: request.end_date_time, + comment: request.comment, + approval_status: request.approval_status, + })), }); //delete from leave_requests table await transaction.leaveRequests.deleteMany({