targo-backend/src/time-and-attendance/expenses/services/expense-update.service.ts
Nic D 91fe8843b3 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
2026-03-10 16:44:08 -04:00

103 lines
4.4 KiB
TypeScript

import { weekStartSunday, toStringFromDate, toDateFromString } from "src/common/utils/date-utils";
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
import { expense_select, timesheet_select } from "src/time-and-attendance/utils/selects.utils";
import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";
import { Injectable } from "@nestjs/common";
import { Result } from "src/common/errors/result-error.factory";
import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper";
import { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto";
import { normalizeAndParseExpenseDto } from "src/time-and-attendance/expenses/expense.utils";
import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service";
@Injectable()
export class ExpenseUpdateService {
constructor(
private readonly prisma: PrismaPostgresService,
private readonly emailResolver: EmailToIdResolver,
private readonly typeResolver: BankCodesResolver,
private readonly payPeriodEventService: PayPeriodEventService,
) { }
async updateExpense(
dto: ExpenseDto,
requesterEmail: string,
targetEmail?: string
): Promise<Result<ExpenseDto, string>> {
try {
// 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 };
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' }
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` }
const data = {
...normed_expense.data,
bank_code_id: type.data,
is_approved: dto.is_approved,
};
if (!data) return { success: false, error: `INVALID_EXPENSE` }
const expense = await this.prisma.expenses.update({
where: { id: dto.id, timesheet_id: timesheet.id },
data,
select: expense_select,
});
if (!expense) return { success: false, error: 'INVALID_EXPENSE' }
const updated: ExpenseDto = {
...expense,
type: expense.bank_code.type,
date: toStringFromDate(expense.date),
amount: expense.amount?.toNumber(),
mileage: expense.mileage?.toNumber(),
supervisor_comment: expense.supervisor_comment ?? undefined,
};
// notify timesheet-approval observers of changes, but only if it came
// from timesheet and not timesheet-approval (no targetEmail)
if (!targetEmail) {
this.payPeriodEventService.emit({
employee_email: accountEmail,
event_type: 'expense',
action: 'update',
date: updated.date,
});
}
return { success: true, data: updated };
} catch (error) {
console.error(error);
return { success: false, error: 'EXPENSE_NOT_FOUND' };
}
}
}