From 02e17fcde42e584f7d8e9bf52ad5f43b2e158127 Mon Sep 17 00:00:00 2001 From: Methapon Metanipat Date: Fri, 25 Oct 2024 13:58:29 +0700 Subject: [PATCH] refactor!: payment sys --- .../migration.sql | 56 ++++ prisma/schema.prisma | 68 +++-- src/controllers/04-invoice-controller.ts | 233 ++++++++++++++++ src/controllers/04-receipt-controller.ts | 90 +++++++ src/controllers/05-payment-controller.ts | 249 ++++++++++++++++++ src/controllers/05-quotation-controller.ts | 32 --- .../05-quotation-payment-controller.ts | 226 ---------------- 7 files changed, 675 insertions(+), 279 deletions(-) create mode 100644 prisma/migrations/20241025065648_add_invoice_and_payment_system/migration.sql create mode 100644 src/controllers/04-invoice-controller.ts create mode 100644 src/controllers/04-receipt-controller.ts create mode 100644 src/controllers/05-payment-controller.ts delete mode 100644 src/controllers/05-quotation-payment-controller.ts diff --git a/prisma/migrations/20241025065648_add_invoice_and_payment_system/migration.sql b/prisma/migrations/20241025065648_add_invoice_and_payment_system/migration.sql new file mode 100644 index 0000000..2c77973 --- /dev/null +++ b/prisma/migrations/20241025065648_add_invoice_and_payment_system/migration.sql @@ -0,0 +1,56 @@ +/* + Warnings: + + - You are about to drop the `QuotationPayment` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "QuotationPayment" DROP CONSTRAINT "QuotationPayment_quotationId_fkey"; + +-- AlterTable +ALTER TABLE "QuotationProductServiceList" ADD COLUMN "invoiceId" TEXT; + +-- DropTable +DROP TABLE "QuotationPayment"; + +-- CreateTable +CREATE TABLE "Invoice" ( + "id" TEXT NOT NULL, + "quotationId" TEXT NOT NULL, + "amount" DOUBLE PRECISION, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdByUserId" TEXT NOT NULL, + + CONSTRAINT "Invoice_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Payment" ( + "id" TEXT NOT NULL, + "invoiceId" TEXT NOT NULL, + "paymentStatus" "PaymentStatus" NOT NULL, + "amount" DOUBLE PRECISION NOT NULL, + "date" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdByUserId" TEXT, + + CONSTRAINT "Payment_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Payment_invoiceId_key" ON "Payment"("invoiceId"); + +-- AddForeignKey +ALTER TABLE "QuotationProductServiceList" ADD CONSTRAINT "QuotationProductServiceList_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES "Invoice"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Invoice" ADD CONSTRAINT "Invoice_quotationId_fkey" FOREIGN KEY ("quotationId") REFERENCES "Quotation"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Invoice" ADD CONSTRAINT "Invoice_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Payment" ADD CONSTRAINT "Payment_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES "Invoice"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Payment" ADD CONSTRAINT "Payment_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ce5b029..021eff4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -423,6 +423,8 @@ model User { quotationUpdated Quotation[] @relation("QuotationUpdatedByUser") flowCreated WorkflowTemplate[] @relation("FlowCreatedByUser") flowUpdated WorkflowTemplate[] @relation("FlowUpdatedByUser") + invoiceCreated Invoice[] + paymentCreated Payment[] } enum CustomerType { @@ -1106,8 +1108,7 @@ model Quotation { status Status @default(CREATED) statusOrder Int @default(0) - quotationStatus QuotationStatus @default(PaymentPending) - quotationPaymentData QuotationPayment[] + quotationStatus QuotationStatus @default(PaymentPending) remark String? @@ -1152,26 +1153,8 @@ model Quotation { updatedAt DateTime @updatedAt updatedBy User? @relation(name: "QuotationUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull) updatedByUserId String? -} -enum PaymentStatus { - PaymentWait - PaymentInProcess - PaymentRetry - PaymentSuccess -} - -model QuotationPayment { - id String @id @default(cuid()) - - paymentStatus PaymentStatus - - date DateTime - amount Float - remark String? - - quotation Quotation @relation(fields: [quotationId], references: [id], onDelete: Cascade) - quotationId String + invoice Invoice[] } model QuotationPaySplit { @@ -1221,6 +1204,9 @@ model QuotationProductServiceList { worker QuotationProductServiceWorker[] requestWork RequestWork[] + + invoice Invoice? @relation(fields: [invoiceId], references: [id]) + invoiceId String? } model QuotationProductServiceWorker { @@ -1233,6 +1219,46 @@ model QuotationProductServiceWorker { @@id([productServiceId, employeeId]) } +model Invoice { + id String @id @default(cuid()) + + quotation Quotation @relation(fields: [quotationId], references: [id]) + quotationId String + + productServiceList QuotationProductServiceList[] + + amount Float? + + payment Payment? + + createdAt DateTime @default(now()) + createdBy User @relation(fields: [createdByUserId], references: [id]) + createdByUserId String +} + +enum PaymentStatus { + PaymentWait + PaymentInProcess + PaymentRetry + PaymentSuccess +} + +model Payment { + id String @id @default(cuid()) + + invoice Invoice @relation(fields: [invoiceId], references: [id]) + invoiceId String @unique + + paymentStatus PaymentStatus + + amount Float + date DateTime? + + createdAt DateTime @default(now()) + createdBy User? @relation(fields: [createdByUserId], references: [id]) + createdByUserId String? +} + model RequestData { id String @id @default(cuid()) diff --git a/src/controllers/04-invoice-controller.ts b/src/controllers/04-invoice-controller.ts new file mode 100644 index 0000000..c7a74f9 --- /dev/null +++ b/src/controllers/04-invoice-controller.ts @@ -0,0 +1,233 @@ +import { Prisma } from "@prisma/client"; +import { + Body, + Controller, + Delete, + Get, + OperationId, + Path, + Post, + Put, + Query, + Request, + Route, + Security, + Tags, +} from "tsoa"; +import prisma from "../db"; +import { notFoundError } from "../utils/error"; +import { RequestWithUser } from "../interfaces/user"; +import { + branchRelationPermInclude, + createPermCheck, + createPermCondition, +} from "../services/permission"; + +type InvoicePayload = { + quotationId: string; + amount: number; + // NOTE: For individual list that will be include in the quotation + productServiceListId?: string[]; + // NOTE: Will be pulled from quotation + installmentNo?: number[]; +}; + +const MANAGE_ROLES = ["system", "head_of_admin", "admin", "head_of_account", "account"]; + +function globalAllow(user: RequestWithUser["user"]) { + const allowList = ["system", "head_of_admin", "head_of_account"]; + return allowList.some((v) => user.roles?.includes(v)); +} + +const permissionCondCompany = createPermCondition((_) => true); +const permissionCheck = createPermCheck(globalAllow); + +@Route("/api/v1/invoice") +@Tags("Invoice") +export class InvoiceController extends Controller { + @Get() + @OperationId("getInvoiceList") + @Security("keycloak") + async getInvoiceList( + @Request() req: RequestWithUser, + @Query() page: number = 1, + @Query() pageSize: number = 30, + @Query() quotationId?: string, + ) { + const where: Prisma.InvoiceWhereInput = { + quotationId, + quotation: { + registeredBranch: { + OR: permissionCondCompany(req.user), + }, + }, + }; + + const [result, total] = await prisma.$transaction([ + prisma.invoice.findMany({ + where, + include: { + productServiceList: { + include: { + worker: true, + service: true, + work: true, + product: true, + }, + }, + quotation: true, + createdBy: true, + }, + orderBy: { createdAt: "asc" }, + }), + prisma.invoice.count({ where }), + ]); + + return { result, page, pageSize, total }; + } + + @Get("{invoiceId}") + @OperationId("getInvoice") + @Security("keycloak") + async getInvoice(@Path() invoiceId: string) { + const record = await prisma.invoice.findFirst({ + where: { id: invoiceId }, + include: { + productServiceList: { + include: { + worker: true, + service: true, + work: true, + product: true, + }, + }, + quotation: true, + createdBy: true, + }, + orderBy: { createdAt: "asc" }, + }); + + if (!record) throw notFoundError("Invoice"); + + return record; + } + + @Post() + @OperationId("createInvoice") + @Security("keycloak", MANAGE_ROLES) + async createInvoice(@Request() req: RequestWithUser, @Body() body: InvoicePayload) { + const [quotation, productServiceList] = await prisma.$transaction([ + prisma.quotation.findUnique({ + where: { id: body.quotationId }, + include: { registeredBranch: { include: branchRelationPermInclude(req.user) } }, + }), + prisma.quotationProductServiceList.findMany({ + where: { + OR: [ + { id: { in: body.productServiceListId }, invoiceId: null }, + { installmentNo: { in: body.installmentNo }, invoiceId: null }, + ], + }, + }), + ]); + + if (!quotation) throw notFoundError("Quotation"); + await permissionCheck(req.user, quotation.registeredBranch); + + return await prisma.invoice.create({ + data: { + productServiceList: { connect: productServiceList.map((v) => ({ id: v.id })) }, + quotationId: body.quotationId, + amount: body.amount, + payment: { + create: { + paymentStatus: "PaymentWait", + amount: body.amount, + }, + }, + createdByUserId: req.user.sub, + }, + }); + } + + @Put("{invoiceId}") + @OperationId("updateInvoice") + @Security("keycloak", MANAGE_ROLES) + async updateInvoice( + @Request() req: RequestWithUser, + @Body() body: InvoicePayload, + @Path() invoiceId: string, + ) { + const [record, quotation, productServiceList] = await prisma.$transaction([ + prisma.invoice.findUnique({ + where: { id: invoiceId }, + include: { + productServiceList: { + where: { + id: { notIn: body.productServiceListId }, + installmentNo: { notIn: body.installmentNo }, + }, + }, + }, + }), + prisma.quotation.findUnique({ + where: { id: body.quotationId }, + include: { registeredBranch: { include: branchRelationPermInclude(req.user) } }, + }), + prisma.quotationProductServiceList.findMany({ + where: { + OR: [ + { id: { in: body.productServiceListId }, invoiceId: null }, + { installmentNo: { in: body.installmentNo }, invoiceId: null }, + ], + }, + }), + ]); + + if (!record) throw notFoundError("Invoice"); + if (!quotation) throw notFoundError("Quotation"); + await permissionCheck(req.user, quotation.registeredBranch); + + return await prisma.$transaction(async (tx) => { + return await tx.invoice.update({ + where: { id: invoiceId }, + data: { + productServiceList: { + disconnect: record.productServiceList.map((v) => ({ id: v.id })), + connect: productServiceList.map((v) => ({ id: v.id })), + }, + }, + }); + }); + } + + @Delete("{invoiceId}") + @OperationId("deleteInvoice") + @Security("keycloak", MANAGE_ROLES) + async deleteInvoice(@Request() req: RequestWithUser, @Path() invoiceId: string) { + return await prisma.$transaction(async (tx) => { + const record = await tx.invoice.delete({ + where: { id: invoiceId }, + include: { + productServiceList: { + include: { + worker: true, + service: true, + work: true, + product: true, + }, + }, + quotation: { + include: { registeredBranch: { include: branchRelationPermInclude(req.user) } }, + }, + createdBy: true, + }, + }); + + if (!record) throw notFoundError("Invoice"); + await permissionCheck(req.user, record.quotation.registeredBranch); + + return record; + }); + } +} diff --git a/src/controllers/04-receipt-controller.ts b/src/controllers/04-receipt-controller.ts new file mode 100644 index 0000000..678b2c7 --- /dev/null +++ b/src/controllers/04-receipt-controller.ts @@ -0,0 +1,90 @@ +import { Controller, Get, OperationId, Path, Query, Request, Route, Security, Tags } from "tsoa"; +import prisma from "../db"; +import { Prisma } from "@prisma/client"; +import { notFoundError } from "../utils/error"; +import { RequestWithUser } from "../interfaces/user"; +import { createPermCondition } from "../services/permission"; + +const permissionCondCompany = createPermCondition((_) => true); + +@Route("/api/v1/receipt") +@Tags("Receipt") +export class ReceiptController extends Controller { + @Get() + @OperationId("getReceiptList") + @Security("keycloak") + async getReceiptList( + @Request() req: RequestWithUser, + @Query() page: number = 1, + @Query() pageSize: number = 30, + @Query() quotationId?: string, + ) { + const where: Prisma.PaymentWhereInput = { + paymentStatus: "PaymentSuccess", + invoice: { + quotationId, + quotation: { + registeredBranch: { + OR: permissionCondCompany(req.user), + }, + }, + }, + }; + + const [result, total] = await prisma.$transaction([ + prisma.payment.findMany({ + where, + include: { + invoice: { + include: { + productServiceList: { + include: { + worker: true, + service: true, + work: true, + product: true, + }, + }, + quotation: true, + createdBy: true, + }, + }, + }, + orderBy: { createdAt: "asc" }, + }), + prisma.payment.count({ where }), + ]); + + return { result, page, pageSize, total }; + } + + @Get("{receiptId}") + @OperationId("getReceipt") + @Security("keycloak") + async getReceipt(@Path() receiptId: string) { + const record = await prisma.payment.findFirst({ + where: { id: receiptId }, + include: { + invoice: { + include: { + productServiceList: { + include: { + worker: true, + service: true, + work: true, + product: true, + }, + }, + quotation: true, + createdBy: true, + }, + }, + }, + orderBy: { createdAt: "asc" }, + }); + + if (!record) throw notFoundError("Receipt"); + + return record; + } +} diff --git a/src/controllers/05-payment-controller.ts b/src/controllers/05-payment-controller.ts new file mode 100644 index 0000000..ac992c4 --- /dev/null +++ b/src/controllers/05-payment-controller.ts @@ -0,0 +1,249 @@ +import { + Body, + Controller, + Delete, + Get, + Head, + Path, + Put, + Query, + Request, + Route, + Security, + Tags, +} from "tsoa"; +import express from "express"; +import { PaymentStatus, Prisma } from "@prisma/client"; +import prisma from "../db"; +import { notFoundError } from "../utils/error"; +import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio"; +import { RequestWithUser } from "../interfaces/user"; +import { + branchRelationPermInclude, + createPermCheck, + createPermCondition, +} from "../services/permission"; + +const MANAGE_ROLES = ["system", "head_of_admin", "admin", "head_of_account", "account"]; + +function globalAllow(user: RequestWithUser["user"]) { + const allowList = ["system", "head_of_admin", "head_of_account"]; + return allowList.some((v) => user.roles?.includes(v)); +} + +const permissionCondCompany = createPermCondition((_) => true); +const permissionCheck = createPermCheck(globalAllow); + +@Tags("Payment") +@Route("api/v1/payment") +export class QuotationPayment extends Controller { + @Get() + @Security("keycloak") + async getPaymentList( + @Request() req: RequestWithUser, + @Query() page: number = 1, + @Query() pageSize: number = 30, + @Query() quotationId?: string, + ) { + const where: Prisma.PaymentWhereInput = { + invoice: { + quotationId, + quotation: { + registeredBranch: { + OR: permissionCondCompany(req.user), + }, + }, + }, + }; + + const [result, total] = await prisma.$transaction([ + prisma.payment.findMany({ + where, + include: { + invoice: { + include: { + productServiceList: { + include: { + worker: true, + service: true, + work: true, + product: true, + }, + }, + quotation: true, + createdBy: true, + }, + }, + }, + orderBy: { createdAt: "asc" }, + }), + prisma.payment.count({ where }), + ]); + + return { result, page, pageSize, total }; + } + + @Get("{quotationId}") + @Security("keycloak") + async getPayment(@Query() quotationId: string) { + const record = await prisma.payment.findFirst({ + where: { invoice: { quotationId } }, + include: { createdBy: true }, + }); + return record; + } + + @Put("{paymentId}") + @Security("keycloak", MANAGE_ROLES) + async updatePayment( + @Path() paymentId: string, + @Body() body: { amount?: number; date?: Date; paymentStatus?: PaymentStatus }, + ) { + const record = await prisma.payment.findUnique({ + where: { id: paymentId }, + include: { + invoice: { + include: { + quotation: { + include: { + _count: { + select: { paySplit: true }, + }, + worker: true, + productServiceList: { + include: { + worker: true, + work: true, + service: true, + product: true, + }, + }, + }, + }, + }, + }, + }, + }); + + if (!record) throw notFoundError("Payment"); + + return await prisma.$transaction(async (tx) => { + const quotation = record.invoice.quotation; + + const payment = await tx.payment.update({ + where: { id: paymentId, invoice: { quotationId: quotation.id } }, + data: body, + }); + + const paymentSum = await prisma.payment.aggregate({ + _sum: { amount: true }, + where: { invoice: { quotationId: quotation.id } }, + }); + + await tx.quotation.update({ + where: { id: quotation.id }, + data: { + quotationStatus: + paymentSum._sum.amount || 0 >= quotation.finalPrice + ? "PaymentSuccess" + : "PaymentInProcess", + requestData: + quotation.quotationStatus === "PaymentPending" + ? { + create: quotation.worker.map((v) => ({ + employeeId: v.employeeId, + requestWork: { + create: quotation.productServiceList.flatMap((item) => + item.worker.findIndex((w) => w.employeeId === v.employeeId) !== -1 + ? { productServiceId: item.id } + : [], + ), + }, + })), + } + : undefined, + }, + }); + + return payment; + }); + } +} + +@Route("api/v1/payment/{paymentId}/attachment") +@Tags("Payment") +export class PaymentController extends Controller { + private async checkPermission(user: RequestWithUser["user"], id: string) { + const data = await prisma.payment.findUnique({ + include: { + invoice: { + include: { + quotation: { + include: { + registeredBranch: { + include: branchRelationPermInclude(user), + }, + }, + }, + }, + }, + }, + where: { id }, + }); + if (!data) throw notFoundError("Payment"); + await permissionCheck(user, data.invoice.quotation.registeredBranch); + return { paymentId: id, quotationId: data.invoice.quotationId }; + } + + @Get() + async listAttachment(@Request() req: RequestWithUser, @Path() paymentId: string) { + const { quotationId } = await this.checkPermission(req.user, paymentId); + return await listFile(fileLocation.quotation.payment(quotationId, paymentId)); + } + + @Head("{name}") + async headAttachment( + @Request() req: RequestWithUser, + @Path() paymentId: string, + @Path() name: string, + ) { + const { quotationId } = await this.checkPermission(req.user, paymentId); + return req.res?.redirect( + await getPresigned("head", fileLocation.quotation.payment(quotationId, paymentId, name)), + ); + } + + @Get("{name}") + async getAttachment( + @Request() req: RequestWithUser, + @Path() paymentId: string, + @Path() name: string, + ) { + const { quotationId } = await this.checkPermission(req.user, paymentId); + return req.res?.redirect( + await getFile(fileLocation.quotation.payment(quotationId, paymentId, name)), + ); + } + + @Put("{name}") + @Security("keycloak", MANAGE_ROLES) + async putAttachment( + @Request() req: RequestWithUser, + @Path() paymentId: string, + @Path() name: string, + ) { + const { quotationId } = await this.checkPermission(req.user, paymentId); + return await setFile(fileLocation.quotation.payment(quotationId, paymentId, name)); + } + + @Delete("{name}") + @Security("keycloak", MANAGE_ROLES) + async deleteAttachment( + @Request() req: RequestWithUser, + @Path() paymentId: string, + @Path() name: string, + ) { + const { quotationId } = await this.checkPermission(req.user, paymentId); + return await deleteFile(fileLocation.quotation.payment(quotationId, paymentId, name)); + } +} diff --git a/src/controllers/05-quotation-controller.ts b/src/controllers/05-quotation-controller.ts index 300481d..7da2675 100644 --- a/src/controllers/05-quotation-controller.ts +++ b/src/controllers/05-quotation-controller.ts @@ -490,20 +490,6 @@ export class QuotationController extends Controller { data: { ...rest, ...price, - quotationPaymentData: { - create: - rest.payCondition === "BillSplit" || rest.payCondition === "Split" - ? rest.paySplit?.map((v) => ({ - paymentStatus: "PaymentWait", - amount: v.amount, - date: v.date, - })) - : { - paymentStatus: "PaymentWait", - amount: price.finalPrice, - date: new Date(), - }, - }, statusOrder: +(rest.status === "INACTIVE"), code: `${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${currentDate.toString().padStart(2, "0")}${lastQuotation.value.toString().padStart(4, "0")}`, worker: { @@ -746,24 +732,6 @@ export class QuotationController extends Controller { data: { ...rest, ...price, - quotationPaymentData: - price && record.quotationStatus === "PaymentPending" - ? { - deleteMany: {}, - create: - rest.payCondition === "BillSplit" || rest.payCondition === "Split" - ? rest.paySplit?.map((v) => ({ - paymentStatus: "PaymentWait", - amount: v.amount, - date: v.date, - })) - : { - paymentStatus: "PaymentWait", - amount: price.finalPrice, - date: new Date(), - }, - } - : undefined, statusOrder: +(rest.status === "INACTIVE"), worker: sortedEmployeeId.length > 0 diff --git a/src/controllers/05-quotation-payment-controller.ts b/src/controllers/05-quotation-payment-controller.ts deleted file mode 100644 index dcb425e..0000000 --- a/src/controllers/05-quotation-payment-controller.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { - Body, - Controller, - Delete, - Get, - Head, - Path, - Post, - Put, - Query, - Request, - Route, - Tags, -} from "tsoa"; -import express from "express"; -import { PaymentStatus } from "@prisma/client"; -import prisma from "../db"; -import { notFoundError } from "../utils/error"; -import HttpError from "../interfaces/http-error"; -import HttpStatus from "../interfaces/http-status"; -import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio"; - -@Tags("Payment") -@Route("api/v1/quotation/{quotationId}/payment") -export class QuotationPayment extends Controller { - @Get() - async getPayment(@Path() quotationId: string) { - const record = await prisma.quotation.findFirst({ - where: { id: quotationId }, - include: { - quotationPaymentData: { - orderBy: { date: "asc" }, - }, - createdBy: true, - updatedBy: true, - }, - }); - - return record; - } - - @Post() - async addPayment( - @Path() quotationId: string, - @Body() body: { amount: number; date: Date; remark: string; paymentStatus?: PaymentStatus }, - ) { - const record = await prisma.quotation.findUnique({ - where: { id: quotationId }, - }); - - if (!record) throw notFoundError("Quotation"); - - if (!body.paymentStatus && record.quotationStatus !== "PaymentPending") { - // NOTE: The quotation must be in waiting for payment or waiting for payment confirmation (re-submit payment) - throw new HttpError( - HttpStatus.PRECONDITION_FAILED, - "Cannot submit payment info of this quotation", - "quotationStatusWrong", - ); - } - - return await prisma.quotation.update({ - where: { id: quotationId }, - include: { quotationPaymentData: true }, - data: { - quotationStatus: "PaymentInProcess", - quotationPaymentData: { - create: { - paymentStatus: "PaymentWait", - ...body, - }, - }, - }, - }); - } - - @Put("{paymentId}") - async updatePayment( - @Path() quotationId: string, - @Path() paymentId: string, - @Body() body: { amount?: number; date?: Date; remark?: string; paymentStatus?: PaymentStatus }, - ) { - const record = await prisma.quotationPayment.findUnique({ - where: { id: paymentId, quotationId }, - }); - - if (!record) throw notFoundError("Quotation Payment"); - - return await prisma.quotationPayment.update({ - where: { id: paymentId, quotationId }, - data: body, - }); - } - - @Get("{paymentId}/attachment") - async listPaymentFile(@Path() quotationId: string, @Path() paymentId: string) { - return await listFile(fileLocation.quotation.payment(quotationId, paymentId)); - } - - @Head("{paymentId}/attachment/{name}") - async headPaymentFile( - @Request() req: express.Request, - @Path() quotationId: string, - @Path() paymentId: string, - @Path() name: string, - ) { - return req.res?.redirect( - await getPresigned("head", fileLocation.quotation.payment(quotationId, paymentId, name)), - ); - } - - @Get("{paymentId}/attachment/{name}") - async getPaymentFile( - @Request() req: express.Request, - @Path() quotationId: string, - @Path() paymentId: string, - @Path() name: string, - ) { - return req.res?.redirect( - await getFile(fileLocation.quotation.payment(quotationId, paymentId, name)), - ); - } - - @Put("{paymentId}/attachment/{name}") - async uploadPayment( - @Path() quotationId: string, - @Path() paymentId: string, - @Path() name: string, - ) { - const record = await prisma.quotation.findUnique({ - where: { id: quotationId }, - }); - - if (!record) throw notFoundError("Quotation"); - - if (record.quotationStatus !== "PaymentPending") { - // NOTE: The quotation must be in waiting for payment or waiting for payment confirmation (re-submit payment) - throw new HttpError( - HttpStatus.PRECONDITION_FAILED, - "Cannot submit payment info of this quotation", - "quotationStatusWrong", - ); - } - - return await setFile(fileLocation.quotation.payment(quotationId, paymentId, name)); - } - - @Delete("{paymentId}/attachment/{name}") - async deletePayment( - @Path() quotationId: string, - @Path() paymentId: string, - @Path() name: string, - ) { - const record = await prisma.quotation.findUnique({ - where: { id: quotationId }, - }); - - if (!record) throw notFoundError("Quotation"); - - return await deleteFile(fileLocation.quotation.payment(quotationId, paymentId, name)); - } - - @Post("confirm") - async confirmPayment(@Path() quotationId: string, @Query() paymentId?: string) { - const record = await prisma.quotation.findUnique({ - include: { - _count: { - select: { - quotationPaymentData: true, - paySplit: true, - }, - }, - worker: true, - quotationPaymentData: true, - productServiceList: { - include: { - worker: true, - work: true, - service: true, - product: true, - }, - }, - }, - where: { id: quotationId }, - }); - - if (!record) throw notFoundError("Quotation"); - - await prisma.$transaction(async (tx) => { - await tx.quotation.update({ - where: { id: quotationId }, - data: { - quotationStatus: - record.payCondition === "Full" || - record.payCondition === "BillFull" || - record._count.paySplit === record._count.quotationPaymentData - ? "PaymentSuccess" - : undefined, - quotationPaymentData: { - update: paymentId - ? { - where: { id: paymentId }, - data: { paymentStatus: "PaymentSuccess" }, - } - : undefined, - }, - requestData: - record.quotationStatus === "PaymentPending" - ? { - create: record.worker.map((v) => ({ - employeeId: v.employeeId, - requestWork: { - create: record.productServiceList.flatMap((item) => - item.worker.findIndex((w) => w.employeeId === v.employeeId) !== -1 - ? { productServiceId: item.id } - : [], - ), - }, - })), - } - : undefined, - }, - }); - }); - } -}