targo-backend/src/time-and-attendance/modules/expenses/services/expense-upsert.service.ts

184 lines
8.2 KiB
TypeScript

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<CreateResult> {
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<UpdateResult> {
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: {
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<DeleteResult> {
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;
};
}