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 { 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<CreateExpenseResult>{
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<UpdateExpenseResult>{
return this.upsert_service.updateExpense(body.update);
update(@Body() dto: ExpenseDto): Promise<ExpenseDto>{
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);
}

View File

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

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 { 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()
@ -23,17 +24,11 @@ export class ExpenseUpsertService {
//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 },
@ -43,14 +38,9 @@ export class ExpenseUpsertService {
//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<UpdateExpenseResult> {
async updateExpense(dto: ExpenseDto): Promise<ExpenseDto> {
try {
//checks for modifications
const data: Record<string, unknown> = {};
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;
}
}
//_________________________________________________________________
@ -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 {
const date = toDateFromString(dto.date);
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;
return { date, comment, supervisor_comment };
const supervisor_comment = dto.supervisor_comment && dto.supervisor_comment.trim()
? this.truncate280(dto.supervisor_comment.trim()) : undefined;
const date = toDateFromString(dto.date);
return {
date,
comment,
supervisor_comment,
parsed_amount,
parsed_attachment,
parsed_mileage
};
}
//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 { 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()

View File

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