feat(business-logic): base setup for business logic implementation, overtime.service and updated timesheets.service to returned overtime infos.

This commit is contained in:
Matthieu Haineault 2025-07-31 10:16:25 -04:00
parent e91fad5105
commit 75615f7c33
6 changed files with 142 additions and 40 deletions

View File

@ -17,6 +17,7 @@ import { PayperiodsModule } from './modules/pay-periods/pay-periods.module';
import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleModule } from '@nestjs/schedule';
import { ArchivalModule } from './modules/archival/archival.module'; import { ArchivalModule } from './modules/archival/archival.module';
import { BankCodesModule } from './modules/bank-codes/bank-codes.module'; import { BankCodesModule } from './modules/bank-codes/bank-codes.module';
import { OvertimeService } from './business-logic/overtime.service';
@Module({ @Module({
imports: [ imports: [
@ -37,6 +38,6 @@ import { BankCodesModule } from './modules/bank-codes/bank-codes.module';
PayperiodsModule, PayperiodsModule,
], ],
controllers: [AppController, HealthController], controllers: [AppController, HealthController],
providers: [AppService], providers: [AppService, OvertimeService],
}) })
export class AppModule {} export class AppModule {}

View File

View File

@ -0,0 +1,82 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
@Injectable()
export class OvertimeService {
private logger = new Logger(OvertimeService.name);
private dailyMax = 8; // maximum for regular hours per day
private weeklyMax = 40; //maximum for regular hours per week
constructor(private prisma: PrismaService) {}
// calculate decimal hours rounded to nearest 5 min
computedHours(start: Date, end: Date): number {
const durationMs = end.getTime() - start.getTime();
const totalMinutes = durationMs / 60000;
//rounded to 5 min
const rounded = Math.round(totalMinutes / 5) * 5;
const hours = rounded / 60;
this.logger.debug(`computedHours: raw=${totalMinutes.toFixed(1)}min rounded = ${rounded}min (${hours.toFixed(2)}h)`);
return hours;
}
//calculate Daily overtime
getDailyOvertimeHours(start: Date, end: Date): number {
const hours = this.computedHours(start, end);
const overtime = Math.max(0, hours - this.dailyMax);
this.logger.debug(`getDailyOvertimeHours : ${overtime.toFixed(2)}h (threshold ${this.dailyMax})`);
return overtime;
}
//sets first day of the week to be sunday
private getWeekStart(date:Date): Date {
const d = new Date(date);
const day = d.getDay(); // return sunday = 0, monday = 1, etc
d.setDate(d.getDate() - day);
d.setHours(0,0,0,0,); // puts start of the week at sunday morning at 00:00
return d;
}
//sets last day of the week to be saturday
private getWeekEnd(startDate:Date): Date {
const d = new Date(startDate);
d.setDate(d.getDate() +6); //sets last day to be saturday
d.setHours(23,59,59,999); //puts end of the week at saturday night at 00:00 minus 1ms
return d;
}
//calculate Weekly overtime
async getWeeklyOvertimeHours(employeeId: number, refDate: Date): Promise<number> {
const weekStart = this.getWeekStart(refDate);
const weekEnd = this.getWeekEnd(weekStart);
//fetches all shifts containing hours
const shifts = await this.prisma.shifts.findMany({
where: { timesheet: { employee_id: employeeId, shift: {
every: {date: { gte: weekStart, lte: weekEnd } }
},
},
},
select: { start_time: true, end_time: true },
});
//calculate total hours of those shifts minus weekly Max to find total overtime hours
const total = shifts.map(shift => this.computedHours(shift.start_time, shift.end_time))
.reduce((sum, hours)=> sum+hours, 0);
const overtime = Math.max(0, total - this.weeklyMax);
this.logger.debug(`weekly total = ${total.toFixed(2)}h, weekly Overtime= ${overtime.toFixed(2)}h`);
return overtime;
}
//apply modifier to overtime hours
calculateOvertimePay(overtimeHours: number, modifier: number): number {
const pay = overtimeHours * modifier;
this.logger.debug(`Overtime payable hours = ${pay.toFixed(2)} (hours ${overtimeHours}, modifier ${modifier})`);
return pay;
}
}

View File

View File

View File

@ -3,51 +3,80 @@ import { PrismaService } from 'src/prisma/prisma.service';
import { CreateTimesheetDto } from '../dtos/create-timesheet.dto'; import { CreateTimesheetDto } from '../dtos/create-timesheet.dto';
import { Timesheets, TimesheetsArchive } from '@prisma/client'; import { Timesheets, TimesheetsArchive } from '@prisma/client';
import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto'; import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto';
import { OvertimeService } from 'src/business-logic/overtime.service';
@Injectable() @Injectable()
export class TimesheetsService { export class TimesheetsService {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
private readonly overtime: OvertimeService,
) {}
async create(dto : CreateTimesheetDto): Promise<Timesheets> { async create(dto : CreateTimesheetDto): Promise<Timesheets> {
const { employee_id, is_approved } = dto; const { employee_id, is_approved } = dto;
return this.prisma.timesheets.create({ return this.prisma.timesheets.create({
data: { data: { employee_id, is_approved: is_approved ?? false },
employee_id,
is_approved: is_approved ?? false,
},
include: { include: {
employee: { employee: { include: { user: true }
include: { user: true }
}, },
}, },
}); });
} }
findAll(): Promise<Timesheets[]> { async findAll(): Promise<any[]> {
return this.prisma.timesheets.findMany({ const list = await this.prisma.timesheets.findMany({
include: { include: {
employee: { shift: { include: { bank_code: true } },
include: { user: true }, expense: { include: { bank_code: true } },
}, employee: { include: { user : true } },
}, },
}); });
return Promise.all(
list.map(async timesheet => {
const detailedShifts = timesheet.shift.map(s => {
const hours = this.overtime.computedHours(s.start_time, s.end_time);
const regularHours = Math.min(8, hours);
const dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time);
const payRegular = regularHours * s.bank_code.modifier;
const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, s.bank_code.modifier);
return { ...s, hours, payRegular, payOvertime };
});
const weeklyOvertimeHours = detailedShifts.length
? await this.overtime.getWeeklyOvertimeHours(
timesheet.employee_id,
timesheet.shift[0].date): 0;
return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours };
})
);
} }
async findOne(id: number): Promise<Timesheets> { async findOne(id: number): Promise<any> {
const record = await this.prisma.timesheets.findUnique({ const timesheet = await this.prisma.timesheets.findUnique({
where: { id }, where: { id },
include: { include: {
employee: { shift: { include: { bank_code: true } },
include: { expense: { include: { bank_code: true } },
user:true employee: { include: { user: true } },
} },
},
},
}); });
if(!record) { if(!timesheet) {
throw new NotFoundException(`Timesheet #${id} not found`); throw new NotFoundException(`Timesheet #${id} not found`);
} }
return record;
const detailedShifts = timesheet.shift.map( s => {
const hours = this.overtime.computedHours(s.start_time, s.end_time);
const regularHours = Math.min(8, hours);
const dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time);
const payRegular = regularHours * s.bank_code.modifier;
const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, s.bank_code.modifier);
return { ...s, hours, payRegular, payOvertime };
});
const weeklyOvertimeHours = detailedShifts.length
? await this.overtime.getWeeklyOvertimeHours(
timesheet.employee_id,
timesheet.shift[0].date): 0;
return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours };
} }
async update(id: number, dto:UpdateTimesheetDto): Promise<Timesheets> { async update(id: number, dto:UpdateTimesheetDto): Promise<Timesheets> {
@ -59,19 +88,14 @@ export class TimesheetsService {
...(employee_id !== undefined && { employee_id }), ...(employee_id !== undefined && { employee_id }),
...(is_approved !== undefined && { is_approved }), ...(is_approved !== undefined && { is_approved }),
}, },
include: { include: { employee: { include: { user: true } },
employee: {
include: { user: true }
},
}, },
}); });
} }
async remove(id: number): Promise<Timesheets> { async remove(id: number): Promise<Timesheets> {
await this.findOne(id); await this.findOne(id);
return this.prisma.timesheets.delete({ return this.prisma.timesheets.delete({ where: { id } });
where: { id },
});
} }
@ -85,8 +109,7 @@ export class TimesheetsService {
await this.prisma.$transaction(async transaction => { await this.prisma.$transaction(async transaction => {
//fetches all timesheets to cutoff //fetches all timesheets to cutoff
const oldSheets = await transaction.timesheets.findMany({ const oldSheets = await transaction.timesheets.findMany({
where: { shift: { every: { date: { lt: cutoff } }, where: { shift: { every: { date: { lt: cutoff } } },
},
}, },
select: { select: {
id: true, id: true,
@ -106,14 +129,10 @@ export class TimesheetsService {
})); }));
//copying data from timesheets table to archive table //copying data from timesheets table to archive table
await transaction.timesheetsArchive.createMany({ await transaction.timesheetsArchive.createMany({ data: archiveDate });
data: archiveDate,
});
//removing data from timesheets table //removing data from timesheets table
await transaction.timesheets.deleteMany({ await transaction.timesheets.deleteMany({ where: { id: { in: oldSheets.map(s => s.id) } } });
where: { id: { in: oldSheets.map(s => s.id) } },
});
}); });
} }