From 8dca65d00eacecf74f126ac20b008a3b27bce20e Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Fri, 7 Nov 2025 13:09:32 -0500 Subject: [PATCH] refactor(expenses): ajusted the controller and service to match new session set-up and did some cleaning --- .../controllers/expense.controller.ts | 13 +-- .../expenses/dtos/expense-create.dto.ts | 1 + .../expenses/dtos/expense-entity.dto.ts | 13 +++ .../expenses/dtos/expense-update.dto.ts | 6 - .../services/expense-upsert.service.ts | 110 ++++++++---------- ...ift-payload.dto.ts => shift-entity.dto.ts} | 0 .../shifts/services/shifts-upsert.service.ts | 2 +- src/time-and-attendance/utils/type.utils.ts | 31 +++-- 8 files changed, 89 insertions(+), 87 deletions(-) create mode 100644 src/time-and-attendance/expenses/dtos/expense-entity.dto.ts delete mode 100644 src/time-and-attendance/expenses/dtos/expense-update.dto.ts rename src/time-and-attendance/time-tracker/shifts/dtos/{shift-payload.dto.ts => shift-entity.dto.ts} (100%) diff --git a/src/time-and-attendance/expenses/controllers/expense.controller.ts b/src/time-and-attendance/expenses/controllers/expense.controller.ts index fb81eb2..9d9b08d 100644 --- a/src/time-and-attendance/expenses/controllers/expense.controller.ts +++ b/src/time-and-attendance/expenses/controllers/expense.controller.ts @@ -1,17 +1,16 @@ import { Controller, Post, Param, Body, Patch, Delete, Req, UnauthorizedException } from "@nestjs/common"; -import { CreateExpenseResult, UpdateExpenseResult } from "src/time-and-attendance/utils/type.utils"; +import { CreateExpenseResult } from "src/time-and-attendance/utils/type.utils"; import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service"; -import { updateExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-update.dto"; import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; import { RolesAllowed } from "src/common/decorators/roles.decorators"; -import { Roles as RoleEnum } from '.prisma/client'; +import { GLOBAL_CONTROLLER_ROLES } from "src/common/shared/role-groupes"; @Controller('expense') +@RolesAllowed(...GLOBAL_CONTROLLER_ROLES) export class ExpenseController { constructor( private readonly upsert_service: ExpenseUpsertService ){} @Post('create') - @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) create( @Req() req, @Body() dto: ExpenseDto): Promise{ const email = req.user?.email; if(!email) throw new UnauthorizedException('Unauthorized User'); @@ -19,13 +18,11 @@ export class ExpenseController { } @Patch('update') - @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) - update(@Body() body: { update :{ id: number; dto: updateExpenseDto }}): Promise{ - return this.upsert_service.updateExpense(body.update); + update(@Body() dto: ExpenseDto): Promise{ + return this.upsert_service.updateExpense(dto); } @Delete('delete/:expense_id') - @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) remove(@Param('expense_id') expense_id: number) { return this.upsert_service.deleteExpense(expense_id); } diff --git a/src/time-and-attendance/expenses/dtos/expense-create.dto.ts b/src/time-and-attendance/expenses/dtos/expense-create.dto.ts index 7e037e8..9b028cc 100644 --- a/src/time-and-attendance/expenses/dtos/expense-create.dto.ts +++ b/src/time-and-attendance/expenses/dtos/expense-create.dto.ts @@ -1,6 +1,7 @@ import { IsBoolean, IsInt, IsOptional, IsString, MaxLength } from "class-validator"; export class ExpenseDto { + @IsInt() @IsOptional() id: number; @IsInt() bank_code_id!: number; @IsInt() timesheet_id!: number; @IsInt() @IsOptional() attachment?: number; diff --git a/src/time-and-attendance/expenses/dtos/expense-entity.dto.ts b/src/time-and-attendance/expenses/dtos/expense-entity.dto.ts new file mode 100644 index 0000000..f04c989 --- /dev/null +++ b/src/time-and-attendance/expenses/dtos/expense-entity.dto.ts @@ -0,0 +1,13 @@ + +export class ExpenseEntity { + id: number; + timesheet_id: number; + bank_code_id: number; + attachment?:number; + date: Date; + amount?: number; + mileage?:number; + comment: string; + supervisor_comment?:string; + is_approved: boolean; +} \ No newline at end of file diff --git a/src/time-and-attendance/expenses/dtos/expense-update.dto.ts b/src/time-and-attendance/expenses/dtos/expense-update.dto.ts deleted file mode 100644 index 3dfe9b6..0000000 --- a/src/time-and-attendance/expenses/dtos/expense-update.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { OmitType, PartialType } from "@nestjs/swagger"; -import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; - -export class updateExpenseDto extends PartialType ( - OmitType(ExpenseDto, ['is_approved', 'timesheet_id'] as const) -){} \ No newline at end of file diff --git a/src/time-and-attendance/expenses/services/expense-upsert.service.ts b/src/time-and-attendance/expenses/services/expense-upsert.service.ts index 7f16064..89c2b44 100644 --- a/src/time-and-attendance/expenses/services/expense-upsert.service.ts +++ b/src/time-and-attendance/expenses/services/expense-upsert.service.ts @@ -1,4 +1,4 @@ -import { CreateExpenseResult, UpdateExpensePayload, UpdateExpenseResult, DeleteExpenseResult, NormalizedExpense } from "src/time-and-attendance/utils/type.utils"; +import { CreateExpenseResult, DeleteExpenseResult, NormalizedExpense } from "src/time-and-attendance/utils/type.utils"; import { toDateFromString, toStringFromDate, weekStartSunday } from "src/time-and-attendance/utils/date-time.utils"; import { Injectable, NotFoundException } from "@nestjs/common"; import { expense_select } from "src/time-and-attendance/utils/selects.utils"; @@ -6,6 +6,7 @@ import { PrismaService } from "src/prisma/prisma.service"; import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { ExpenseEntity } from "src/time-and-attendance/expenses/dtos/expense-entity.dto"; @Injectable() @@ -18,39 +19,28 @@ export class ExpenseUpsertService { //_________________________________________________________________ // CREATE //_________________________________________________________________ - async createExpense( dto: ExpenseDto, email: string): Promise { + async createExpense(dto: ExpenseDto, email: string): Promise { try { //fetch employee_id using req.user.email const employee_id = await this.emailResolver.findIdByEmail(email); - //normalize strings and dates - const normed_expense = this.normalizeExpenseDto(dto); + //normalize strings and dates and Parse numbers + const normed_expense = this.normalizeAndParseExpenseDto(dto); - //finds the timesheet using expense.date + //finds the timesheet using expense.date by finding the sunday const start_date = weekStartSunday(normed_expense.date); - - //parse numbers - const parsed_amount = this.parseOptionalNumber(dto.amount, "amount"); - const parsed_mileage = this.parseOptionalNumber(dto.mileage, "mileage"); - const parsed_attachment = this.parseOptionalNumber(dto.attachment, "attachment"); - const timesheet = await this.prisma.timesheets.findFirst({ where: { start_date, employee_id }, select: { id: true, employee_id: true }, }); - if(!timesheet) throw new NotFoundException(`Timesheet with id ${dto.timesheet_id} not found`); - + if (!timesheet) throw new NotFoundException(`Timesheet with id ${dto.timesheet_id} not found`); + //create a new expense const expense = await this.prisma.expenses.create({ data: { + ...normed_expense, timesheet_id: timesheet.id, bank_code_id: dto.bank_code_id, - attachment: parsed_attachment, - date: normed_expense.date, - amount: parsed_amount, - mileage: parsed_mileage, - comment: normed_expense.comment, - supervisor_comment: normed_expense.supervisor_comment, is_approved: dto.is_approved, }, //return the newly created expense with id @@ -59,16 +49,12 @@ export class ExpenseUpsertService { //build an object to return to the frontend to display const created: GetExpenseDto = { - id: expense.id, - timesheet_id: expense.timesheet_id, - bank_code_id: expense.bank_code_id, - attachment: expense.attachment ?? undefined, + ...expense, date: toStringFromDate(expense.date), amount: expense.amount?.toNumber(), mileage: expense.mileage?.toNumber(), - comment: expense.comment, + attachment: expense.attachment ?? undefined, supervisor_comment: expense.supervisor_comment ?? undefined, - is_approved: expense.is_approved, }; return { ok: true, data: created } @@ -80,47 +66,39 @@ export class ExpenseUpsertService { //_________________________________________________________________ // UPDATE //_________________________________________________________________ - async updateExpense({id, dto}: UpdateExpensePayload): Promise { + async updateExpense(dto: ExpenseDto): Promise { try { - //checks for modifications - const data: Record = {}; - if (dto.date !== undefined) data.date = toDateFromString(dto.date); - if (dto.comment !== undefined) data.comment = this.truncate280(dto.comment); - if (dto.attachment !== undefined) data.attachment = this.parseOptionalNumber(dto.attachment, "attachment"); - if (dto.amount !== undefined) data.amount = this.parseOptionalNumber(dto.amount, "amount"); - if (dto.mileage !== undefined) data.mileage = this.parseOptionalNumber(dto.mileage, "mileage"); - if (dto.bank_code_id !== undefined) data.bank_code_id = dto.bank_code_id; - if (dto.supervisor_comment !== undefined) { - data.supervisor_comment = dto.supervisor_comment?.trim() - ? this.truncate280(dto.supervisor_comment.trim()) - : null; - } - //return an error if no fields needs an update - if(!Object.keys(data).length) { - return { ok: false, id, error: new Error("Nothing to update")}; - } + //normalize string , date format and parse numbers + const normed_expense = this.normalizeAndParseExpenseDto(dto); + //checks for modifications + const data: ExpenseEntity = { + ...normed_expense, + id: dto.id, + timesheet_id: dto.timesheet_id, + bank_code_id: dto.bank_code_id, + is_approved: dto.is_approved, + }; + + //push updates and get updated datas const expense = await this.prisma.expenses.update({ - where: { id }, + where: { id: dto.id, timesheet_id: dto.timesheet_id }, data, select: expense_select, }); + //build an object to return to the frontend const updated: GetExpenseDto = { - id: expense.id, - timesheet_id: expense.timesheet_id, - bank_code_id: expense.bank_code_id, - attachment: expense.attachment ?? undefined, + ...expense, date: toStringFromDate(expense.date), amount: expense.amount?.toNumber(), mileage: expense.mileage?.toNumber(), - comment: expense.comment, + attachment: expense.attachment ?? undefined, supervisor_comment: expense.supervisor_comment ?? undefined, - is_approved: expense.is_approved, }; - return { ok: true, id: expense.id, data: updated }; + return updated; } catch (error) { - return { ok: false, id: id, error: error} + return error; } } //_________________________________________________________________ @@ -133,9 +111,9 @@ export class ExpenseUpsertService { where: { id: expense_id }, select: { id: true }, }); - if(!expense) throw new NotFoundException(`Expense with id: ${expense_id} not found`); - - await tx.expenses.delete({ where: { id: expense_id }}); + if (!expense) throw new NotFoundException(`Expense with id: ${expense_id} not found`); + + await tx.expenses.delete({ where: { id: expense_id } }); return { success: true }; }); return { ok: true, id: expense_id }; @@ -148,14 +126,22 @@ export class ExpenseUpsertService { // LOCAL HELPERS //_________________________________________________________________ //makes sure that comments are the right length the date is of Date type - private normalizeExpenseDto(dto: ExpenseDto): NormalizedExpense { + private normalizeAndParseExpenseDto(dto: ExpenseDto): NormalizedExpense { + const parsed_attachment = this.parseOptionalNumber(dto.attachment, "attachment"); + const parsed_mileage = this.parseOptionalNumber(dto.mileage, "mileage"); + const parsed_amount = this.parseOptionalNumber(dto.amount, "amount"); + const comment = this.truncate280(dto.comment); + const supervisor_comment = dto.supervisor_comment && dto.supervisor_comment.trim() + ? this.truncate280(dto.supervisor_comment.trim()) : undefined; const date = toDateFromString(dto.date); - const comment = this.truncate280(dto.comment); - const supervisor_comment = - dto.supervisor_comment && dto.supervisor_comment.trim() - ? this.truncate280(dto.supervisor_comment.trim()) - : undefined; - return { date, comment, supervisor_comment }; + return { + date, + comment, + supervisor_comment, + parsed_amount, + parsed_attachment, + parsed_mileage + }; } //makes sure that a string cannot exceed 280 chars diff --git a/src/time-and-attendance/time-tracker/shifts/dtos/shift-payload.dto.ts b/src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto.ts similarity index 100% rename from src/time-and-attendance/time-tracker/shifts/dtos/shift-payload.dto.ts rename to src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto.ts diff --git a/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts b/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts index 510c5c3..7a857b2 100644 --- a/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts +++ b/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts @@ -6,7 +6,7 @@ import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-ty import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; import { PrismaService } from "src/prisma/prisma.service"; import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto"; -import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-payload.dto"; +import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto"; import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; @Injectable() diff --git a/src/time-and-attendance/utils/type.utils.ts b/src/time-and-attendance/utils/type.utils.ts index 2d2ef35..b38ecc8 100644 --- a/src/time-and-attendance/utils/type.utils.ts +++ b/src/time-and-attendance/utils/type.utils.ts @@ -1,9 +1,8 @@ import { Prisma, PrismaClient } from "@prisma/client"; import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto"; -import { updateExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-update.dto"; import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto"; -import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-payload.dto"; +import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto"; import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; @@ -24,26 +23,38 @@ export type TotalExpenses = { mileage: number; }; -export type Normalized = { date: Date; start_time: Date; end_time: Date; bank_code_id: number}; +export type Normalized = { + date: Date; + start_time: Date; + end_time: Date; + bank_code_id: number; +}; export type CreateShiftResult = { ok: true; data: GetShiftDto } | { ok: false; error: any }; export type UpdateShiftResult = { ok: true; id: number; data: GetShiftDto } | { ok: false; id: number; error: any }; +export type NormedOk = { + index: number; + dto: ShiftEntity; + normed: Normalized; + timesheet_id: number; +}; export type DeletePresetResult = { ok: true; id: number; } | { ok: false; id: number; error: any }; export type CreatePresetResult = { ok: true; } | { ok: false; error: any }; export type UpdatePresetResult = { ok: true; id: number; data: SchedulePresetsDto } | { ok: false; id: number; error: any }; -export type NormalizedExpense = { date: Date; comment: string; supervisor_comment?: string; }; +export type NormalizedExpense = { + date: Date; + comment: string; + supervisor_comment?: string; + parsed_amount?: number; + parsed_mileage?: number; + parsed_attachment?: number; +}; export type CreateExpenseResult = { ok: true; data: GetExpenseDto } | { ok: false; error: any }; -export type UpdateExpensePayload = { id: number; dto: updateExpenseDto }; -export type UpdateExpenseResult = { ok: true; id: number; data: GetExpenseDto } | { ok: false; id: number; error: any }; export type DeleteExpenseResult = { ok: true; id: number; } | { ok: false; id: number; error: any }; - -export type NormedOk = { index: number; dto: ShiftEntity; normed: Normalized, timesheet_id: number }; - - export type ShiftResponse = { week_day: string; sort_order: number;