173 lines
8.0 KiB
TypeScript
173 lines
8.0 KiB
TypeScript
import { CreateExpenseResult, UpdateExpensePayload, UpdateExpenseResult, DeleteExpenseResult, NormalizedExpense } from "src/time-and-attendance/utils/type.utils";
|
|
import { toDateFromString, toStringFromDate, weekStartSunday } 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 { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto";
|
|
import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto";
|
|
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
|
|
|
|
|
@Injectable()
|
|
export class ExpenseUpsertService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly emailResolver: EmailToIdResolver,
|
|
) { }
|
|
|
|
//_________________________________________________________________
|
|
// CREATE
|
|
//_________________________________________________________________
|
|
async createExpense( dto: ExpenseDto, email: string): Promise<CreateExpenseResult> {
|
|
try {
|
|
//fetch employee_id using req.user.email
|
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
|
|
|
//normalize strings and dates
|
|
const normed_expense = this.normalizeExpenseDto(dto);
|
|
|
|
//finds the timesheet using expense.date
|
|
const start_date = weekStartSunday(normed_expense.date);
|
|
|
|
//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");
|
|
|
|
const timesheet = await this.prisma.timesheets.findFirst({
|
|
where: { start_date, employee_id },
|
|
select: { id: true, employee_id: true },
|
|
});
|
|
if(!timesheet) throw new NotFoundException(`Timesheet with id ${dto.timesheet_id} not found`);
|
|
|
|
//create a new expense
|
|
const expense = await this.prisma.expenses.create({
|
|
data: {
|
|
timesheet_id: 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;
|
|
};
|
|
} |