targo-backend/src/time-and-attendance/modules/expenses/services/expense-upsert.service.ts
2025-10-30 13:57:16 -04:00

158 lines
7.3 KiB
TypeScript

import { CreateExpenseResult, UpdateExpensePayload, UpdateExpenseResult, DeleteExpenseResult, NormalizedExpense } from "src/time-and-attendance/utils/type.utils";
import { toDateFromString, toStringFromDate } 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";
import { PrismaService } from "src/prisma/prisma.service";
import { GetExpenseDto } from "src/time-and-attendance/modules/expenses/dtos/expense-get.dto";
import { ExpenseDto } from "src/time-and-attendance/modules/expenses/dtos/expense-create.dto";
@Injectable()
export class ExpenseUpsertService {
constructor(private readonly prisma: PrismaService) { }
//_________________________________________________________________
// CREATE
//_________________________________________________________________
async createExpense(timesheet_id: number, dto: ExpenseDto): Promise<CreateExpenseResult> {
try {
//normalize strings and dates
const normed_expense = this.normalizeExpenseDto(dto);
//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");
//create a new expense
const expense = await this.prisma.expenses.create({
data: {
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
select: expense_select,
});
//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,
date: toStringFromDate(expense.date),
amount: expense.amount?.toNumber(),
mileage: expense.mileage?.toNumber(),
comment: expense.comment,
supervisor_comment: expense.supervisor_comment ?? undefined,
is_approved: expense.is_approved,
};
return { ok: true, data: created }
} catch (error) {
return { ok: false, error: error }
}
}
//_________________________________________________________________
// UPDATE
//_________________________________________________________________
async updateExpense({id, dto}: UpdateExpensePayload): Promise<UpdateExpenseResult> {
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")};
}
const expense = await this.prisma.expenses.update({
where: { id },
data,
select: expense_select,
});
const updated: GetExpenseDto = {
id: expense.id,
timesheet_id: expense.timesheet_id,
bank_code_id: expense.bank_code_id,
attachment: expense.attachment ?? undefined,
date: toStringFromDate(expense.date),
amount: expense.amount?.toNumber(),
mileage: expense.mileage?.toNumber(),
comment: expense.comment,
supervisor_comment: expense.supervisor_comment ?? undefined,
is_approved: expense.is_approved,
};
return { ok: true, id: expense.id, data: updated };
} catch (error) {
return { ok: false, id: id, error: error}
}
}
//_________________________________________________________________
// DELETE
//_________________________________________________________________
async deleteExpense(expense_id: number): Promise<DeleteExpenseResult> {
try {
await this.prisma.$transaction(async (tx) => {
const expense = await tx.expenses.findUnique({
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 }});
return { success: true };
});
return { ok: true, id: expense_id };
} catch (error) {
return { ok: false, id: expense_id, error };
}
}
//_________________________________________________________________
// 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);
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 };
}
//makes sure that a string cannot exceed 280 chars
private truncate280 = (input: string): string => {
return input.length > 280 ? input.slice(0, 280) : input;
}
//makes sure that the type of data of numeric values is valid
private parseOptionalNumber = (value: unknown, field: string) => {
if (value == null) return undefined;
const parsed = Number(value);
if (Number.isNaN(parsed)) throw new Error(`Invalid value : ${value} for ${field}`);
return parsed;
};
}