From 91fe8843b3e493aaccddb9cdfcd4ffef7444b237 Mon Sep 17 00:00:00 2001 From: Nic D Date: Tue, 10 Mar 2026 16:44:08 -0400 Subject: [PATCH] fix(expense): Fix issue where expenses were not properly calculating amount Also add better permission handling when creating shifts or expenses for other users through timesheet-approval module --- .../services/expense-create.service.ts | 24 +++++++++--- .../services/expense-delete.service.ts | 3 ++ .../services/expense-update.service.ts | 39 ++++++++++++------- .../exports/services/csv-exports.service.ts | 6 ++- .../pay-period/dtos/pay-period-event.dto.ts | 1 + .../schedule-presets-apply.service.ts | 2 + .../shifts/services/shifts-create.service.ts | 3 +- .../shifts/services/shifts-delete.service.ts | 4 +- .../shifts/services/shifts-update.service.ts | 3 +- .../utils/selects.utils.ts | 3 +- 10 files changed, 64 insertions(+), 24 deletions(-) diff --git a/src/time-and-attendance/expenses/services/expense-create.service.ts b/src/time-and-attendance/expenses/services/expense-create.service.ts index 4da0de3..78a66d2 100644 --- a/src/time-and-attendance/expenses/services/expense-create.service.ts +++ b/src/time-and-attendance/expenses/services/expense-create.service.ts @@ -22,13 +22,26 @@ export class ExpenseCreateService { // CREATE //_________________________________________________________________ async createExpense( - dto: ExpenseDto, - email: string, - employee_email?: string + dto: ExpenseDto, + requesterEmail: string, + targetEmail?: string ): Promise> { try { - const accountEmail = employee_email ?? email; - + // check if requester has access to timesheet_approval + if (!!targetEmail && requesterEmail !== targetEmail) { + const requester = await this.prisma.users.findFirst({ + where: { email: requesterEmail, }, + select: { user_module_access: true, } + }) + + if (!requester) return { success: false, error: 'INVALID_USER' } + if (!requester.user_module_access?.timesheets_approval) + return { success: false, error: 'ACCESS_DENIED' } + + } + + const accountEmail = targetEmail ?? requesterEmail; + //fetch employee_id using req.user.email const employee_id = await this.emailResolver.findIdByEmail(accountEmail); if (!employee_id.success) return { success: false, error: employee_id.error }; @@ -77,6 +90,7 @@ export class ExpenseCreateService { employee_email: accountEmail, event_type: 'expense', action: 'create', + date: created.date, }); return { success: true, data: created }; diff --git a/src/time-and-attendance/expenses/services/expense-delete.service.ts b/src/time-and-attendance/expenses/services/expense-delete.service.ts index 4c1a8c1..47cb998 100644 --- a/src/time-and-attendance/expenses/services/expense-delete.service.ts +++ b/src/time-and-attendance/expenses/services/expense-delete.service.ts @@ -2,6 +2,7 @@ import { Injectable } from "@nestjs/common"; import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service"; import { Result } from "src/common/errors/result-error.factory"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; +import { toStringFromDate } from "src/common/utils/date-utils"; import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service"; @Injectable() @@ -25,6 +26,7 @@ export class ExpenseDeleteService { const expense = await this.prisma.expenses.findUnique({ where: { id: expense_id }, select: { + date: true, timesheet: { select: { employee_id: true, @@ -52,6 +54,7 @@ export class ExpenseDeleteService { employee_email: email, event_type: 'expense', action: 'delete', + date: toStringFromDate(expense.date) }); return { success: true, data: expense_id }; diff --git a/src/time-and-attendance/expenses/services/expense-update.service.ts b/src/time-and-attendance/expenses/services/expense-update.service.ts index 6149e77..6320771 100644 --- a/src/time-and-attendance/expenses/services/expense-update.service.ts +++ b/src/time-and-attendance/expenses/services/expense-update.service.ts @@ -20,38 +20,51 @@ export class ExpenseUpdateService { async updateExpense( dto: ExpenseDto, - email: string, - employee_email?: string + requesterEmail: string, + targetEmail?: string ): Promise> { try { - const account_email = employee_email ?? email; - //fetch employee_id using req.user.email - const employee_id = await this.emailResolver.findIdByEmail(account_email); + // check if requester has access to timesheet_approval + if (!!targetEmail && requesterEmail !== targetEmail) { + const requester = await this.prisma.users.findFirst({ + where: { email: requesterEmail, }, + select: { user_module_access: true, } + }) + + if (!requester) return { success: false, error: 'INVALID_USER' } + if (!requester.user_module_access?.timesheets_approval) + return { success: false, error: 'ACCESS_DENIED' } + + } + + const accountEmail = targetEmail ?? requesterEmail; + + const employee_id = await this.emailResolver.findIdByEmail(accountEmail); if (!employee_id.success) return { success: false, error: employee_id.error }; - //normalize string , date format and parse numbers + const normed_expense = await normalizeAndParseExpenseDto(dto); if (!normed_expense.success) return { success: false, error: normed_expense.error } const type = await this.typeResolver.findBankCodeIDByType(dto.type); if (!type.success) return { success: false, error: 'INVALID_EXPENSE_TYPE' } - //added timesheet_id modification check according to the new date + const new_timesheet_start_date = weekStartSunday(toDateFromString(dto.date)); const timesheet = await this.prisma.timesheets.findFirst({ where: { start_date: new_timesheet_start_date, employee_id: employee_id.data }, select: timesheet_select, }); + if (!timesheet) return { success: false, error: `TIMESHEET_NOT_FOUND` } - //checks for modifications const data = { ...normed_expense.data, bank_code_id: type.data, is_approved: dto.is_approved, }; + if (!data) return { success: false, error: `INVALID_EXPENSE` } - //push updates and get updated datas const expense = await this.prisma.expenses.update({ where: { id: dto.id, timesheet_id: timesheet.id }, data, @@ -59,7 +72,6 @@ export class ExpenseUpdateService { }); if (!expense) return { success: false, error: 'INVALID_EXPENSE' } - //build an object to return to the frontend const updated: ExpenseDto = { ...expense, type: expense.bank_code.type, @@ -70,12 +82,13 @@ export class ExpenseUpdateService { }; // notify timesheet-approval observers of changes, but only if it came - // from timesheet and not timesheet-approval (no employee_email) - if (!employee_email) { + // from timesheet and not timesheet-approval (no targetEmail) + if (!targetEmail) { this.payPeriodEventService.emit({ - employee_email: email, + employee_email: accountEmail, event_type: 'expense', action: 'update', + date: updated.date, }); } diff --git a/src/time-and-attendance/exports/services/csv-exports.service.ts b/src/time-and-attendance/exports/services/csv-exports.service.ts index b9abf04..bba4779 100644 --- a/src/time-and-attendance/exports/services/csv-exports.service.ts +++ b/src/time-and-attendance/exports/services/csv-exports.service.ts @@ -79,7 +79,7 @@ export class CsvExportService { const week = computeWeekNumber(start, shift.date); const type_transaction = shift.bank_code.bank_code.charAt(0); const code = Number(shift.bank_code.bank_code.slice(1,)); - const isPTO = PTO_SHIFT_CODES.includes(shift.bank_code.bank_code) + const isPTO = PTO_SHIFT_CODES.includes(shift.bank_code.bank_code); return { timesheet_id: shift.timesheet.id, @@ -118,6 +118,8 @@ export class CsvExportService { const type_transaction = expense.bank_code.bank_code.charAt(0); const code = Number(expense.bank_code.bank_code.slice(1,)) const week = computeWeekNumber(start, expense.date); + const amount = expense.bank_code.bank_code === 'G503' ? expense.mileage : expense.amount; + const adjustedAmount = Number(amount) * expense.bank_code.modifier; rows.push({ timesheet_id: expense.timesheet.id, @@ -129,7 +131,7 @@ export class CsvExportService { code: code, quantite_hre: undefined, taux_horaire: undefined, - montant: Number(expense.amount), + montant: adjustedAmount, semaine_no: week, division_no: undefined, service_no: undefined, diff --git a/src/time-and-attendance/pay-period/dtos/pay-period-event.dto.ts b/src/time-and-attendance/pay-period/dtos/pay-period-event.dto.ts index 89faff2..58847b1 100644 --- a/src/time-and-attendance/pay-period/dtos/pay-period-event.dto.ts +++ b/src/time-and-attendance/pay-period/dtos/pay-period-event.dto.ts @@ -2,4 +2,5 @@ export class PayPeriodEvent { employee_email: string; event_type: 'expense' | 'shift' | 'preset'; action: 'create' | 'update' | 'delete'; + date: string; } \ No newline at end of file diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts index 770add9..42cf75e 100644 --- a/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts @@ -87,6 +87,7 @@ export class SchedulePresetsApplyService { employee_email: user_email, event_type: 'preset', action: 'create', + date: created_shifts[0].date }) } @@ -142,6 +143,7 @@ export class SchedulePresetsApplyService { employee_email: user_email, event_type: 'preset', action: 'create', + date }) } diff --git a/src/time-and-attendance/shifts/services/shifts-create.service.ts b/src/time-and-attendance/shifts/services/shifts-create.service.ts index b4c92b1..00ee082 100644 --- a/src/time-and-attendance/shifts/services/shifts-create.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-create.service.ts @@ -65,7 +65,8 @@ export class ShiftsCreateService { this.payPeriodEventService.emit({ employee_email: email, event_type: 'shift', - action: 'create' + action: 'create', + date: shifts[0].date, }) } diff --git a/src/time-and-attendance/shifts/services/shifts-delete.service.ts b/src/time-and-attendance/shifts/services/shifts-delete.service.ts index abe3d4a..52c140a 100644 --- a/src/time-and-attendance/shifts/services/shifts-delete.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-delete.service.ts @@ -4,6 +4,7 @@ import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service"; import { PaidTimeOffBankHoursService } from "src/time-and-attendance/paid-time-off/paid-time-off.service"; import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service"; +import { toStringFromDate } from "src/common/utils/date-utils"; @Injectable() export class ShiftsDeleteService { @@ -72,7 +73,8 @@ export class ShiftsDeleteService { this.payPeriodEventService.emit({ employee_email: email, event_type: 'shift', - action: 'delete' + action: 'delete', + date: toStringFromDate(shift.date), }) } diff --git a/src/time-and-attendance/shifts/services/shifts-update.service.ts b/src/time-and-attendance/shifts/services/shifts-update.service.ts index 9569b59..a622ceb 100644 --- a/src/time-and-attendance/shifts/services/shifts-update.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-update.service.ts @@ -63,7 +63,8 @@ export class ShiftsUpdateService { this.payPeriodEventService.emit({ employee_email: email, event_type: 'shift', - action: 'update' + action: 'update', + date: shifts[0].date }) } diff --git a/src/time-and-attendance/utils/selects.utils.ts b/src/time-and-attendance/utils/selects.utils.ts index 1c4e5f3..c06fcf0 100644 --- a/src/time-and-attendance/utils/selects.utils.ts +++ b/src/time-and-attendance/utils/selects.utils.ts @@ -106,7 +106,8 @@ export const select_csv_shift_lines = { export const select_csv_expense_lines = { date: true, amount: true, - bank_code: { select: { bank_code: true } }, + mileage: true, + bank_code: { select: { bank_code: true, modifier: true } }, timesheet: { select: { id: true,