feat(approval): clean up Approval services. creation of a "shared" folder

This commit is contained in:
Matthieu Haineault 2025-08-06 14:17:52 -04:00
parent ef5af80471
commit c23da925e7
9 changed files with 140 additions and 33 deletions

View File

@ -0,0 +1,26 @@
import { NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
//abstract class for approving or rejecting a shift, expense, timesheet or pay-period
export abstract class BaseApprovalService<T> {
protected constructor(protected readonly prisma: PrismaService) {}
//returns the corresponding Prisma delegate
protected abstract get delegate(): {
update(args: {where: {id: number };
data: { is_approved: boolean }
}): Promise<T>;
};
//standard update Aproval
async updateApproval(id: number, isApproved: boolean): Promise<T> {
const entity = await this.delegate.update({
where: { id },
data: { is_approved: isApproved },
});
if(!entity) throw new NotFoundException(`Entity #${id} not found`);
return entity;
}
}

View File

@ -1,4 +1,4 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
import { ExpensesService } from "../services/expenses.service";
import { CreateExpenseDto } from "../dtos/create-expense";
import { Expenses } from "@prisma/client";
@ -8,13 +8,17 @@ import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagg
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard";
import { ExpenseEntity } from "../dtos/swagger-entities/expenses.entity";
import { ExpensesApprovalService } from "../services/expenses-approval.service";
@ApiTags('Expenses')
@ApiBearerAuth('access-token')
@UseGuards(JwtAuthGuard)
@Controller('Expenses')
export class ExpensesController {
constructor(private readonly expensesService: ExpensesService) {}
constructor(
private readonly expensesService: ExpensesService,
private readonly expensesApprovalService: ExpensesApprovalService,
) {}
@Post()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ -61,4 +65,10 @@ export class ExpensesController {
return this.expensesService.remove(id);
}
@Patch(':id/approval')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) {
return this.expensesApprovalService.updateApproval(id, isApproved);
}
}

View File

@ -1,11 +1,13 @@
import { Injectable } from "@nestjs/common";
import { Expenses } from "@prisma/client";
import { BaseApprovalService } from "src/common/shared/base-approval.service";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class ExpensesApprovalService {
constructor(private readonly prisma: PrismaService) {}
async updateApproval(expenseId: number, isApproved: boolean) {
export class ExpensesApprovalService extends BaseApprovalService<Expenses> {
constructor(prisma: PrismaService) { super(prisma); }
protected get delegate() {
return this.prisma.expenses;
}
}

View File

@ -1,5 +1,40 @@
import { NotFoundException } from "@nestjs/common";
import { TimesheetsApprovalService } from "src/modules/timesheets/services/timesheets-approval.service";
import { PrismaService } from "src/prisma/prisma.service";
export class PayPeriodsApprovalService {
constructor(private readonly timesheetsApproval: TimesheetsApprovalService) {}
constructor(
private readonly prisma: PrismaService,
private readonly timesheetsApproval: TimesheetsApprovalService,
) {}
async approvaPayperdiod(periodNumber: number): Promise<void> {
const period = await this.prisma.payPeriods.findUnique({
where: { period_number: periodNumber },
});
if (!period) throw new NotFoundException(`PayPeriod #${periodNumber} not found`);
//fetches timesheet of selected period if the timesheet as atleast 1 shift or 1 expense
const timesheetList = await this.prisma.timesheets.findMany({
where: {
OR: [
{ shift: {some: { date: { gte: period.start_date,
lte: period.end_date,
},
}},
},
{ expense: { some: { date: { gte: period.start_date,
lte: period.end_date,
},
}},
},
],
},
});
//approval of both timesheet (cascading to the approval of related shifts and expenses)
for(const timesheet of timesheetList) {
await this.timesheetsApproval.updateApproval(timesheet.id, true);
}
}
}

View File

@ -1,10 +1,15 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { Injectable, NotFoundException, Param, ParseIntPipe, Patch } from "@nestjs/common";
import { PayPeriods } from "@prisma/client";
import { PrismaService } from "src/prisma/prisma.service";
import { PayPeriodsApprovalService } from "./pay-periods-approval.service";
import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { Roles as RoleEnum } from '.prisma/client';
@Injectable()
export class PayPeriodsService {
constructor(private readonly prisma: PrismaService) {}
constructor(private readonly prisma: PrismaService,
private readonly payperiodsApprovalService: PayPeriodsApprovalService
) {}
async findAll(): Promise<PayPeriods[]> {
return this.prisma.payPeriods.findMany({
@ -32,4 +37,13 @@ export class PayPeriodsService {
}
return period;
}
@Patch(':periodNumber/approval')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
async approve(@Param('periodNumber', ParseIntPipe) periodNumber: number): Promise<{message:string}> {
await this.payperiodsApprovalService.approvaPayperdiod(periodNumber);
return {message: `Pay-period #${periodNumber} approved`};
}
}

View File

@ -1,21 +1,13 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { Shifts } from "@prisma/client";
import { BaseApprovalService } from "src/common/shared/base-approval.service";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class ShiftsApprovalService {
constructor(private readonly prisma: PrismaService) {}
export class ShiftsApprovalService extends BaseApprovalService<Shifts> {
constructor(prisma: PrismaService) { super(prisma); }
async updateApproval(shiftId: number, isApproved: boolean): Promise<Shifts> {
const shift = await this.prisma.shifts.update({
where: { id: shiftId },
data: { is_approved: isApproved },
});
if(!shift) {
throw new NotFoundException(`Shift # ${shiftId} not found`);
}
return shift;
protected get delegate() {
return this.prisma.shifts;
}
}

View File

@ -17,7 +17,7 @@ export interface OverviewRow {
export class ShiftsOverviewService {
constructor(private readonly prisma: PrismaService) {}
async getSummary(periodId: number): Promise<ValidationRow[]> {
async getSummary(periodId: number): Promise<OverviewRow[]> {
//fetch pay-period to display
const period = await this.prisma.payPeriods.findUnique({
where: { period_number: periodId },

View File

@ -1,4 +1,4 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, UseGuards } from '@nestjs/common';
import { TimesheetsService } from '../services/timesheets.service';
import { CreateTimesheetDto } from '../dtos/create-timesheet.dto';
import { Timesheets } from '@prisma/client';
@ -8,13 +8,17 @@ import { Roles as RoleEnum } from '.prisma/client';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from 'src/modules/authentication/guards/jwt-auth.guard';
import { TimesheetEntity } from '../dtos/swagger-entities/timesheet.entity';
import { TimesheetsApprovalService } from '../services/timesheets-approval.service';
@ApiTags('Timesheets')
@ApiBearerAuth('access-token')
@UseGuards(JwtAuthGuard)
@Controller('timesheets')
export class TimesheetsController {
constructor(private readonly timesheetsService: TimesheetsService) {}
constructor(
private readonly timesheetsService: TimesheetsService,
private readonly timesheetsApprovalService: TimesheetsApprovalService,
) {}
@Post()
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ -63,4 +67,10 @@ export class TimesheetsController {
remove(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> {
return this.timesheetsService.remove(id);
}
@Patch(':id/approval')
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) {
return this.timesheetsApprovalService.updateApproval(id, isApproved);
}
}

View File

@ -1,19 +1,37 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { Timesheets } from "@prisma/client";
import { BaseApprovalService } from "src/common/shared/base-approval.service";
import { ExpensesApprovalService } from "src/modules/expenses/services/expenses-approval.service";
import { ShiftsApprovalService } from "src/modules/shifts/services/shifts-approval.service";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class TimesheetsApprovalService {
constructor(private readonly prisma: PrismaService) {}
export class TimesheetsApprovalService extends BaseApprovalService<Timesheets>{
constructor(
prisma: PrismaService,
private readonly shiftsApproval: ShiftsApprovalService,
private readonly expensesApproval: ExpensesApprovalService,
) {super(prisma);}
protected get delegate() {
return this.prisma.timesheets;
}
async updateApproval(timesheetId: number, isApproved: boolean): Promise<Timesheets> {
const timesheet = await this.prisma.timesheets.update({
where: { id: timesheetId },
const timesheet = await super.updateApproval(timesheetId, isApproved);
await this.prisma.shifts.updateMany({
where: { timesheet_id: timesheetId },
data: { is_approved: isApproved },
});
await this.prisma.expenses.updateMany({
where: { timesheet_id: timesheetId },
data: { is_approved: isApproved },
});
if (!timesheet) throw new NotFoundException(`Timesheet # ${timesheetId} not found`);
return timesheet;
}
}