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 { 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 { export class ExpensePayloadDto {
@IsString() @IsString()
type!: string; type!: string;
@ValidateIf(o => (o.type ?? '').toUpperCase() !== 'MILEAGE') @ValidateIf(o => (o.type ?? '').toUpperCase() !== 'MILEAGE')
@IsDefined()
@IsNumber() @IsNumber()
@Min(0) @Min(0)
amount!: number; amount?: number;
@ValidateIf(o => (o.type ?? '').toUpperCase() === 'MILEAGE') @ValidateIf(o => (o.type ?? '').toUpperCase() === 'MILEAGE')
@IsDefined()
@IsNumber() @IsNumber()
@Min(0) @Min(0)
mileage!: number; mileage?: number;
@IsString() @IsString()
@MaxLength(280) @MaxLength(280)

View File

@ -72,7 +72,15 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
orderBy: [{ date: 'asc' }, { id: 'asc' }], 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: { const normalizePayload = async (payload: {
@ -85,19 +93,39 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
type: string; type: string;
bank_code_id: number; bank_code_id: number;
amount: Prisma.Decimal; amount: Prisma.Decimal;
mileage: number | null;
comment: string; comment: string;
attachment: string | null; attachment: string | null;
}> => { }> => {
const type = this.normalizeType(payload.type); const type = this.normalizeType(payload.type);
const comment = this.assertAndTrimComment(payload.comment); const comment = this.assertAndTrimComment(payload.comment);
const attachment = payload.attachment?.trim()?.length ? payload.attachment.trim() : null; const attachment = payload.attachment?.trim()?.length ? payload.attachment.trim() : null;
const { id: bank_code_id, modifier } = await this.lookupBankCodeOrThrow(tx, type); 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 { return {
type, type,
bank_code_id, bank_code_id,
amount, amount,
mileage,
comment, comment,
attachment attachment
}; };
@ -106,6 +134,7 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
const findExactOld = async (norm: { const findExactOld = async (norm: {
bank_code_id: number; bank_code_id: number;
amount: Prisma.Decimal; amount: Prisma.Decimal;
mileage: number | null;
comment: string; comment: string;
attachment: string | null; attachment: string | null;
}) => { }) => {
@ -117,6 +146,7 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
amount: norm.amount, amount: norm.amount,
comment: norm.comment, comment: norm.comment,
attachment: norm.attachment, attachment: norm.attachment,
...(norm.mileage !== null ? { mileage: norm.mileage } : { mileage: null }),
}, },
select: { id: true }, select: { id: true },
}); });
@ -145,7 +175,7 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
date: dateOnly, date: dateOnly,
bank_code_id: new_exp.bank_code_id, bank_code_id: new_exp.bank_code_id,
amount: new_exp.amount, amount: new_exp.amount,
mileage: null, mileage: new_exp.mileage,
comment: new_exp.comment, comment: new_exp.comment,
attachment: new_exp.attachment, attachment: new_exp.attachment,
is_approved: false, is_approved: false,
@ -153,7 +183,7 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
}); });
action = 'created'; action = 'created';
} }
//-------------------- UPDATE --------------------
else if(old_expense && new_expense) { else if(old_expense && new_expense) {
const oldNorm = await normalizePayload(old_expense); const oldNorm = await normalizePayload(old_expense);
const existing = await findExactOld(oldNorm); const existing = await findExactOld(oldNorm);
@ -170,7 +200,7 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
data: { data: {
bank_code_id: new_exp.bank_code_id, bank_code_id: new_exp.bank_code_id,
amount: new_exp.amount, amount: new_exp.amount,
mileage: null, mileage: new_exp.mileage,
comment: new_exp.comment, comment: new_exp.comment,
attachment: new_exp.attachment, attachment: new_exp.attachment,
}, },
@ -188,7 +218,7 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
} }
//helpers imported from utils and repos. //-------------------- helpers --------------------
private readonly normalizeType = (type: string): string => private readonly normalizeType = (type: string): string =>
normalizeTypeUtil(type); normalizeTypeUtil(type);
@ -210,7 +240,10 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
private readonly computeAmountDecimal = ( private readonly computeAmountDecimal = (
type: string, type: string,
payload: { amount?: number; mileage?: number;}, payload: {
amount?: number;
mileage?: number;
},
modifier: number, modifier: number,
): Prisma.Decimal => { ): Prisma.Decimal => {
if(type === 'MILEAGE') { if(type === 'MILEAGE') {
@ -224,6 +257,7 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
private readonly mapDbToDayResponse = (row: { private readonly mapDbToDayResponse = (row: {
date: Date; date: Date;
amount: Prisma.Decimal | number | string; amount: Prisma.Decimal | number | string;
mileage: Prisma.Decimal | number | string;
comment: string; comment: string;
is_approved: boolean; is_approved: boolean;
bank_code: { type: string } | null; bank_code: { type: string } | null;

View File

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