fix(expenses): ajusted mileage logic

This commit is contained in:
Matthieu Haineault 2025-10-01 10:02:52 -04:00
parent 3b4dd9ddb5
commit f8f4ad5a83
3 changed files with 87 additions and 51 deletions

View File

@ -1,21 +1,19 @@
import { Transform, Type } from "class-transformer";
import { IsDefined, IsNumber, IsOptional, IsString, maxLength, MaxLength, Min, ValidateIf, ValidateNested } from "class-validator";
import { IsNumber, IsOptional, IsString, MaxLength, Min, ValidateIf, ValidateNested } from "class-validator";
export class ExpensePayloadDto {
@IsString()
type!: string;
@ValidateIf(o => (o.type ?? '').toUpperCase() !== 'MILEAGE')
@IsDefined()
@IsNumber()
@Min(0)
amount!: number;
amount?: number;
@ValidateIf(o => (o.type ?? '').toUpperCase() === 'MILEAGE')
@IsDefined()
@IsNumber()
@Min(0)
mileage!: number;
mileage?: number;
@IsString()
@MaxLength(280)

View File

@ -72,32 +72,60 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
orderBy: [{ date: 'asc' }, { id: 'asc' }],
});
return rows.map(this.mapDbToDayResponse);
return rows.map((r) =>
this.mapDbToDayResponse({
date: r.date,
amount: r.amount ?? 0,
mileage: r.mileage ?? 0,
comment: r.comment,
is_approved: r.is_approved,
bank_code: r.bank_code,
}));
};
const normalizePayload = async (payload: {
type: string;
amount?: number;
mileage?: number;
comment: string;
type: string;
amount?: number;
mileage?: number;
comment: string;
attachment?: string;
}): Promise<{
type: string;
type: string;
bank_code_id: number;
amount: Prisma.Decimal;
comment: string;
attachment: string | null;
amount: Prisma.Decimal;
mileage: number | null;
comment: string;
attachment: string | null;
}> => {
const type = this.normalizeType(payload.type);
const comment = this.assertAndTrimComment(payload.comment);
const type = this.normalizeType(payload.type);
const comment = this.assertAndTrimComment(payload.comment);
const attachment = payload.attachment?.trim()?.length ? payload.attachment.trim() : null;
const { id: bank_code_id, modifier } = await this.lookupBankCodeOrThrow(tx, type);
const amount = this.computeAmountDecimal(type, payload, modifier);
let amount = this.computeAmountDecimal(type, payload, modifier);
let mileage: number | null = null;
if (type === 'MILEAGE') {
mileage = Number(payload.mileage ?? 0);
if (!(mileage > 0)) {
throw new BadRequestException('Mileage required and must be > 0 for type MILEAGE');
}
const amountNumber = computeMileageAmount(mileage, modifier);
amount = new Prisma.Decimal(amountNumber);
} else {
if (!(typeof payload.amount === 'number' && payload.amount >= 0)) {
throw new BadRequestException('Amount required for non-MILEAGE expense');
}
amount = new Prisma.Decimal(payload.amount);
}
return {
type,
bank_code_id,
amount,
mileage,
comment,
attachment
};
@ -105,18 +133,20 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
const findExactOld = async (norm: {
bank_code_id: number;
amount: Prisma.Decimal;
comment: string;
attachment: string | null;
amount: Prisma.Decimal;
mileage: number | null;
comment: string;
attachment: string | null;
}) => {
return tx.expenses.findFirst({
where: {
timesheet_id: timesheet_id,
date: dateOnly,
date: dateOnly,
bank_code_id: norm.bank_code_id,
amount: norm.amount,
comment: norm.comment,
attachment: norm.attachment,
amount: norm.amount,
comment: norm.comment,
attachment: norm.attachment,
...(norm.mileage !== null ? { mileage: norm.mileage } : { mileage: null }),
},
select: { id: true },
});
@ -136,24 +166,24 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
await tx.expenses.delete({where: { id: existing.id } });
action = 'deleted';
}
//-------------------- CREATE --------------------
//-------------------- CREATE --------------------
else if (!old_expense && new_expense) {
const new_exp = await normalizePayload(new_expense);
await tx.expenses.create({
data: {
timesheet_id: timesheet_id,
date: dateOnly,
date: dateOnly,
bank_code_id: new_exp.bank_code_id,
amount: new_exp.amount,
mileage: null,
comment: new_exp.comment,
attachment: new_exp.attachment,
is_approved: false,
amount: new_exp.amount,
mileage: new_exp.mileage,
comment: new_exp.comment,
attachment: new_exp.attachment,
is_approved: false,
},
});
action = 'created';
}
//-------------------- UPDATE --------------------
else if(old_expense && new_expense) {
const oldNorm = await normalizePayload(old_expense);
const existing = await findExactOld(oldNorm);
@ -169,10 +199,10 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
where: { id: existing.id },
data: {
bank_code_id: new_exp.bank_code_id,
amount: new_exp.amount,
mileage: null,
comment: new_exp.comment,
attachment: new_exp.attachment,
amount: new_exp.amount,
mileage: new_exp.mileage,
comment: new_exp.comment,
attachment: new_exp.attachment,
},
});
action = 'updated';
@ -188,7 +218,7 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
}
//helpers imported from utils and repos.
//-------------------- helpers --------------------
private readonly normalizeType = (type: string): string =>
normalizeTypeUtil(type);
@ -210,7 +240,10 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
private readonly computeAmountDecimal = (
type: string,
payload: { amount?: number; mileage?: number;},
payload: {
amount?: number;
mileage?: number;
},
modifier: number,
): Prisma.Decimal => {
if(type === 'MILEAGE') {
@ -222,11 +255,12 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
};
private readonly mapDbToDayResponse = (row: {
date: Date;
amount: Prisma.Decimal | number | string;
comment: string;
date: Date;
amount: Prisma.Decimal | number | string;
mileage: Prisma.Decimal | number | string;
comment: string;
is_approved: boolean;
bank_code: { type: string } | null;
bank_code: { type: string } | null;
}): DayExpenseResponse => mapDbExpenseToDayResponse(row);

View File

@ -1,5 +1,6 @@
import { BadRequestException } from "@nestjs/common";
import { DayExpenseResponse } from "../types and interfaces/expenses.types.interfaces";
import { Prisma } from "@prisma/client";
//uppercase and trim for validation
export function normalizeType(type: string): string {
@ -48,18 +49,21 @@ export function toNumberSafe(value: DecimalLike): number {
//map of a row for DayExpenseResponse
export function mapDbExpenseToDayResponse(row: {
date: Date;
amount: DecimalLike;
comment: string;
date: Date;
amount: Prisma.Decimal | number | string | null;
mileage?: Prisma.Decimal | number | string | null;
comment: string;
is_approved: boolean;
bank_code?: { type?: string | null } | null;
bank_code?: { type?: string | null } | null;
}): DayExpenseResponse {
const yyyyMmDd = row.date.toISOString().slice(0,10);
const toNum = (value: any)=> (value == null ? 0 : Number(value));
return {
date: yyyyMmDd,
type: normalizeType(row.bank_code?.type ?? 'UNKNOWN'),
amount: toNumberSafe(row.amount),
comment: row.comment,
date: yyyyMmDd,
type: normalizeType(row.bank_code?.type ?? 'UNKNOWN'),
amount: toNum(row.amount),
comment: row.comment,
is_approved: row.is_approved,
...(row.mileage !== null ? { mileage: toNum(row.mileage) }: {}),
};
}