import { toDateFromString, toStringFromDate } from "../helpers/expenses-date-time-helpers"; import { Injectable, NotFoundException } from "@nestjs/common"; import { updateExpenseDto } from "../dtos/update-expense.dto"; import { GetExpenseDto } from "../dtos/get-expense.dto"; import { PrismaService } from "src/prisma/prisma.service"; import { ExpenseDto } from "../dtos/expense.dto"; type Normalized = { date: Date; comment: string; supervisor_comment?: string; }; export type CreateResult = { ok: true; data: GetExpenseDto } | { ok: false; error: any }; export type UpdatePayload = { id: number; dto: updateExpenseDto }; export type UpdateResult = { ok: true; id: number; data: GetExpenseDto } | { ok: false; id: number; error: any }; export type DeleteResult = { ok: true; id: number; } | { ok: false; id: number; error: any }; @Injectable() export class ExpenseUpsertService { constructor(private readonly prisma: PrismaService) { } //_________________________________________________________________ // CREATE //_________________________________________________________________ async createExpense(timesheet_id: number, dto: ExpenseDto): Promise { 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: { id: true, timesheet_id: true, bank_code_id: true, attachment: true, date: true, amount: true, mileage: true, comment: true, supervisor_comment: true, is_approved: true, }, }); //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}: UpdatePayload): Promise { try { //checks for modifications const data: Record = {}; 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: { id: true, timesheet_id: true, bank_code_id: true, attachment: true, date: true, amount: true, mileage: true, comment: true, supervisor_comment: true, is_approved: true, }, }); 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 { 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): Normalized { 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; }; }