feat(expense): link expense with attachment.
This commit is contained in:
parent
f8f4ad5a83
commit
77f065f37f
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `attachement` on the `expenses` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `attachement` on the `expenses_archive` table. All the data in the column will be lost.
|
||||||
|
- Made the column `comment` on table `expenses` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."expenses" DROP COLUMN "attachement",
|
||||||
|
ADD COLUMN "attachment" INTEGER,
|
||||||
|
ADD COLUMN "mileage" DECIMAL(65,30),
|
||||||
|
ALTER COLUMN "comment" SET NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."expenses_archive" DROP COLUMN "attachement",
|
||||||
|
ADD COLUMN "attachment" INTEGER,
|
||||||
|
ADD COLUMN "mileage" DECIMAL(65,30),
|
||||||
|
ALTER COLUMN "amount" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."expenses" ADD CONSTRAINT "expenses_attachment_fkey" FOREIGN KEY ("attachment") REFERENCES "public"."attachments"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "public"."expenses_archive" ADD CONSTRAINT "expenses_archive_attachment_fkey" FOREIGN KEY ("attachment") REFERENCES "public"."attachments"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
@ -225,9 +225,10 @@ model Expenses {
|
||||||
bank_code BankCodes @relation("ExpenseBankCodes", fields: [bank_code_id], references: [id])
|
bank_code BankCodes @relation("ExpenseBankCodes", fields: [bank_code_id], references: [id])
|
||||||
bank_code_id Int
|
bank_code_id Int
|
||||||
date DateTime @db.Date
|
date DateTime @db.Date
|
||||||
amount Decimal @db.Money
|
amount Decimal @db.Money
|
||||||
mileage Decimal?
|
mileage Decimal?
|
||||||
attachment String?
|
attachment Int?
|
||||||
|
attachment_record Attachments? @relation("ExpenseAttachment", fields: [attachment], references: [id], onDelete: SetNull)
|
||||||
comment String
|
comment String
|
||||||
is_approved Boolean @default(false)
|
is_approved Boolean @default(false)
|
||||||
supervisor_comment String?
|
supervisor_comment String?
|
||||||
|
|
@ -247,7 +248,8 @@ model ExpensesArchive {
|
||||||
date DateTime @db.Date
|
date DateTime @db.Date
|
||||||
amount Decimal? @db.Money
|
amount Decimal? @db.Money
|
||||||
mileage Decimal?
|
mileage Decimal?
|
||||||
attachment String?
|
attachment Int?
|
||||||
|
attachment_record Attachments? @relation("ExpenseArchiveAttachment", fields: [attachment], references: [id], onDelete: SetNull)
|
||||||
comment String?
|
comment String?
|
||||||
is_approved Boolean
|
is_approved Boolean
|
||||||
supervisor_comment String?
|
supervisor_comment String?
|
||||||
|
|
@ -298,6 +300,9 @@ model Attachments {
|
||||||
created_by String
|
created_by String
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
|
|
||||||
|
expenses Expenses[] @relation("ExpenseAttachment")
|
||||||
|
expenses_archive ExpensesArchive[] @relation("ExpenseArchiveAttachment")
|
||||||
|
|
||||||
@@index([owner_type, owner_id, created_at])
|
@@index([owner_type, owner_id, created_at])
|
||||||
@@index([sha256])
|
@@index([sha256])
|
||||||
@@map("attachments")
|
@@map("attachments")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
import { Transform, Type } from "class-transformer";
|
import { Transform, Type } from "class-transformer";
|
||||||
import { IsNumber, IsOptional, IsString, MaxLength, Min, ValidateIf, ValidateNested } from "class-validator";
|
import {
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
Matches,
|
||||||
|
MaxLength,
|
||||||
|
Min,
|
||||||
|
ValidateIf,
|
||||||
|
ValidateNested
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
export class ExpensePayloadDto {
|
export class ExpensePayloadDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|
@ -21,7 +30,17 @@ export class ExpensePayloadDto {
|
||||||
comment!: string;
|
comment!: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
if (value === null || value === undefined || value === '') return undefined;
|
||||||
|
if (typeof value === 'number') return value.toString();
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length ? trimmed : undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@Matches(/^\d+$/)
|
||||||
@MaxLength(255)
|
@MaxLength(255)
|
||||||
attachment?: string;
|
attachment?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
|
||||||
import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
||||||
import { Expenses, Prisma } from "@prisma/client";
|
import { Expenses, Prisma } from "@prisma/client";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
@ -7,8 +6,21 @@ import { BankCodesRepo } from "../repos/bank-codes.repo";
|
||||||
import { TimesheetsRepo } from "../repos/timesheets.repo";
|
import { TimesheetsRepo } from "../repos/timesheets.repo";
|
||||||
import { EmployeesRepo } from "../repos/employee.repo";
|
import { EmployeesRepo } from "../repos/employee.repo";
|
||||||
import { toDateOnlyUTC } from "src/modules/shifts/helpers/shifts-date-time-helpers";
|
import { toDateOnlyUTC } from "src/modules/shifts/helpers/shifts-date-time-helpers";
|
||||||
import { assertAndTrimComment, computeMileageAmount, mapDbExpenseToDayResponse, normalizeType as normalizeTypeUtil } from "../utils/expenses.utils";
|
import {
|
||||||
import { DayExpenseResponse, UpsertAction } from "../types and interfaces/expenses.types.interfaces";
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
DayExpenseResponse,
|
||||||
|
UpsertAction
|
||||||
|
} from "../types and interfaces/expenses.types.interfaces";
|
||||||
|
import {
|
||||||
|
assertAndTrimComment,
|
||||||
|
computeMileageAmount,
|
||||||
|
mapDbExpenseToDayResponse,
|
||||||
|
normalizeType as normalizeTypeUtil
|
||||||
|
} from "../utils/expenses.utils";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
|
|
@ -88,18 +100,18 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
amount?: number;
|
amount?: number;
|
||||||
mileage?: number;
|
mileage?: number;
|
||||||
comment: string;
|
comment: string;
|
||||||
attachment?: string;
|
attachment?: string | number;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
type: string;
|
type: string;
|
||||||
bank_code_id: number;
|
bank_code_id: number;
|
||||||
amount: Prisma.Decimal;
|
amount: Prisma.Decimal;
|
||||||
mileage: number | null;
|
mileage: number | null;
|
||||||
comment: string;
|
comment: string;
|
||||||
attachment: string | null;
|
attachment: number | 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 = this.parseAttachmentId(payload.attachment);
|
||||||
|
|
||||||
const { id: bank_code_id, modifier } = await this.lookupBankCodeOrThrow(tx, type);
|
const { id: bank_code_id, modifier } = await this.lookupBankCodeOrThrow(tx, type);
|
||||||
let amount = this.computeAmountDecimal(type, payload, modifier);
|
let amount = this.computeAmountDecimal(type, payload, modifier);
|
||||||
|
|
@ -121,6 +133,16 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
amount = new Prisma.Decimal(payload.amount);
|
amount = new Prisma.Decimal(payload.amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (attachment !== null) {
|
||||||
|
const attachmentRow = await tx.attachments.findUnique({
|
||||||
|
where: { id: attachment },
|
||||||
|
select: { status: true },
|
||||||
|
});
|
||||||
|
if (!attachmentRow || attachmentRow.status !== 'ACTIVE') {
|
||||||
|
throw new BadRequestException('Attachment not found or inactive');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
bank_code_id,
|
bank_code_id,
|
||||||
|
|
@ -136,7 +158,7 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
amount: Prisma.Decimal;
|
amount: Prisma.Decimal;
|
||||||
mileage: number | null;
|
mileage: number | null;
|
||||||
comment: string;
|
comment: string;
|
||||||
attachment: string | null;
|
attachment: number | null;
|
||||||
}) => {
|
}) => {
|
||||||
return tx.expenses.findFirst({
|
return tx.expenses.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
|
@ -225,6 +247,33 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
private readonly assertAndTrimComment = (comment: string): string =>
|
private readonly assertAndTrimComment = (comment: string): string =>
|
||||||
assertAndTrimComment(comment);
|
assertAndTrimComment(comment);
|
||||||
|
|
||||||
|
private readonly parseAttachmentId = (value: unknown): number | null => {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
if (!Number.isInteger(value) || value <= 0) {
|
||||||
|
throw new BadRequestException('Invalid attachment id');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed.length) return null;
|
||||||
|
if (!/^\d+$/.test(trimmed)) throw new BadRequestException('Invalid attachment id');
|
||||||
|
|
||||||
|
const parsed = Number(trimmed);
|
||||||
|
if (parsed <= 0) throw new BadRequestException('Invalid attachment id');
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
throw new BadRequestException('Invalid attachment id');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
private readonly resolveEmployeeIdByEmail = async (email: string): Promise<number> =>
|
private readonly resolveEmployeeIdByEmail = async (email: string): Promise<number> =>
|
||||||
this.employeesRepo.findIdByEmail(email);
|
this.employeesRepo.findIdByEmail(email);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user