refactor(expenses): ajusted the controller and service to match new session set-up and did some cleaning

This commit is contained in:
Matthieu Haineault 2025-11-07 13:09:32 -05:00
parent eb166dbc46
commit 8dca65d00e
8 changed files with 89 additions and 87 deletions

View File

@ -1,17 +1,16 @@
import { Controller, Post, Param, Body, Patch, Delete, Req, UnauthorizedException } from "@nestjs/common"; 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 { 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 { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto";
import { RolesAllowed } from "src/common/decorators/roles.decorators"; 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') @Controller('expense')
@RolesAllowed(...GLOBAL_CONTROLLER_ROLES)
export class ExpenseController { export class ExpenseController {
constructor( private readonly upsert_service: ExpenseUpsertService ){} constructor( private readonly upsert_service: ExpenseUpsertService ){}
@Post('create') @Post('create')
@RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN)
create( @Req() req, @Body() dto: ExpenseDto): Promise<CreateExpenseResult>{ create( @Req() req, @Body() dto: ExpenseDto): Promise<CreateExpenseResult>{
const email = req.user?.email; const email = req.user?.email;
if(!email) throw new UnauthorizedException('Unauthorized User'); if(!email) throw new UnauthorizedException('Unauthorized User');
@ -19,13 +18,11 @@ export class ExpenseController {
} }
@Patch('update') @Patch('update')
@RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) update(@Body() dto: ExpenseDto): Promise<ExpenseDto>{
update(@Body() body: { update :{ id: number; dto: updateExpenseDto }}): Promise<UpdateExpenseResult>{ return this.upsert_service.updateExpense(dto);
return this.upsert_service.updateExpense(body.update);
} }
@Delete('delete/:expense_id') @Delete('delete/:expense_id')
@RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN)
remove(@Param('expense_id') expense_id: number) { remove(@Param('expense_id') expense_id: number) {
return this.upsert_service.deleteExpense(expense_id); return this.upsert_service.deleteExpense(expense_id);
} }

View File

@ -1,6 +1,7 @@
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength } from "class-validator"; import { IsBoolean, IsInt, IsOptional, IsString, MaxLength } from "class-validator";
export class ExpenseDto { export class ExpenseDto {
@IsInt() @IsOptional() id: number;
@IsInt() bank_code_id!: number; @IsInt() bank_code_id!: number;
@IsInt() timesheet_id!: number; @IsInt() timesheet_id!: number;
@IsInt() @IsOptional() attachment?: number; @IsInt() @IsOptional() attachment?: number;

View File

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

View File

@ -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)
){}

View File

@ -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 { toDateFromString, toStringFromDate, weekStartSunday } from "src/time-and-attendance/utils/date-time.utils";
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
import { expense_select } from "src/time-and-attendance/utils/selects.utils"; 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 { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto";
import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.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 { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
import { ExpenseEntity } from "src/time-and-attendance/expenses/dtos/expense-entity.dto";
@Injectable() @Injectable()
@ -23,17 +24,11 @@ export class ExpenseUpsertService {
//fetch employee_id using req.user.email //fetch employee_id using req.user.email
const employee_id = await this.emailResolver.findIdByEmail(email); const employee_id = await this.emailResolver.findIdByEmail(email);
//normalize strings and dates //normalize strings and dates and Parse numbers
const normed_expense = this.normalizeExpenseDto(dto); 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); 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({ const timesheet = await this.prisma.timesheets.findFirst({
where: { start_date, employee_id }, where: { start_date, employee_id },
select: { id: true, employee_id: true }, select: { id: true, employee_id: true },
@ -43,14 +38,9 @@ export class ExpenseUpsertService {
//create a new expense //create a new expense
const expense = await this.prisma.expenses.create({ const expense = await this.prisma.expenses.create({
data: { data: {
...normed_expense,
timesheet_id: timesheet.id, timesheet_id: timesheet.id,
bank_code_id: dto.bank_code_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, is_approved: dto.is_approved,
}, },
//return the newly created expense with id //return the newly created expense with id
@ -59,16 +49,12 @@ export class ExpenseUpsertService {
//build an object to return to the frontend to display //build an object to return to the frontend to display
const created: GetExpenseDto = { const created: GetExpenseDto = {
id: expense.id, ...expense,
timesheet_id: expense.timesheet_id,
bank_code_id: expense.bank_code_id,
attachment: expense.attachment ?? undefined,
date: toStringFromDate(expense.date), date: toStringFromDate(expense.date),
amount: expense.amount?.toNumber(), amount: expense.amount?.toNumber(),
mileage: expense.mileage?.toNumber(), mileage: expense.mileage?.toNumber(),
comment: expense.comment, attachment: expense.attachment ?? undefined,
supervisor_comment: expense.supervisor_comment ?? undefined, supervisor_comment: expense.supervisor_comment ?? undefined,
is_approved: expense.is_approved,
}; };
return { ok: true, data: created } return { ok: true, data: created }
@ -80,47 +66,39 @@ export class ExpenseUpsertService {
//_________________________________________________________________ //_________________________________________________________________
// UPDATE // UPDATE
//_________________________________________________________________ //_________________________________________________________________
async updateExpense({id, dto}: UpdateExpensePayload): Promise<UpdateExpenseResult> { async updateExpense(dto: ExpenseDto): Promise<ExpenseDto> {
try { try {
//checks for modifications //normalize string , date format and parse numbers
const data: Record<string, unknown> = {}; const normed_expense = this.normalizeAndParseExpenseDto(dto);
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")};
}
//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({ const expense = await this.prisma.expenses.update({
where: { id }, where: { id: dto.id, timesheet_id: dto.timesheet_id },
data, data,
select: expense_select, select: expense_select,
}); });
//build an object to return to the frontend
const updated: GetExpenseDto = { const updated: GetExpenseDto = {
id: expense.id, ...expense,
timesheet_id: expense.timesheet_id,
bank_code_id: expense.bank_code_id,
attachment: expense.attachment ?? undefined,
date: toStringFromDate(expense.date), date: toStringFromDate(expense.date),
amount: expense.amount?.toNumber(), amount: expense.amount?.toNumber(),
mileage: expense.mileage?.toNumber(), mileage: expense.mileage?.toNumber(),
comment: expense.comment, attachment: expense.attachment ?? undefined,
supervisor_comment: expense.supervisor_comment ?? undefined, supervisor_comment: expense.supervisor_comment ?? undefined,
is_approved: expense.is_approved,
}; };
return { ok: true, id: expense.id, data: updated }; return updated;
} catch (error) { } catch (error) {
return { ok: false, id: id, error: error} return error;
} }
} }
//_________________________________________________________________ //_________________________________________________________________
@ -148,14 +126,22 @@ export class ExpenseUpsertService {
// LOCAL HELPERS // LOCAL HELPERS
//_________________________________________________________________ //_________________________________________________________________
//makes sure that comments are the right length the date is of Date type //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 date = toDateFromString(dto.date); 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 comment = this.truncate280(dto.comment);
const supervisor_comment = const supervisor_comment = dto.supervisor_comment && dto.supervisor_comment.trim()
dto.supervisor_comment && dto.supervisor_comment.trim() ? this.truncate280(dto.supervisor_comment.trim()) : undefined;
? this.truncate280(dto.supervisor_comment.trim()) const date = toDateFromString(dto.date);
: undefined; return {
return { date, comment, supervisor_comment }; date,
comment,
supervisor_comment,
parsed_amount,
parsed_attachment,
parsed_mileage
};
} }
//makes sure that a string cannot exceed 280 chars //makes sure that a string cannot exceed 280 chars

View File

@ -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 { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.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 { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
@Injectable() @Injectable()

View File

@ -1,9 +1,8 @@
import { Prisma, PrismaClient } from "@prisma/client"; import { Prisma, PrismaClient } from "@prisma/client";
import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto"; 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 { 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 { 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"; import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
@ -24,26 +23,38 @@ export type TotalExpenses = {
mileage: number; 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 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 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 DeletePresetResult = { ok: true; id: number; } | { ok: false; id: number; error: any };
export type CreatePresetResult = { ok: true; } | { ok: false; 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 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 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 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 = { export type ShiftResponse = {
week_day: string; week_day: string;
sort_order: number; sort_order: number;