From 20b7e56a0dee6981dc016866a9607f770cb44932 Mon Sep 17 00:00:00 2001 From: Methapon Metanipat Date: Tue, 15 Oct 2024 13:29:40 +0700 Subject: [PATCH] feat: add payment for split --- .../migration.sql | 37 ++++++ prisma/schema.prisma | 24 +++- .../05-quotation-payment-controller.ts | 117 ++++++++++++++---- src/utils/minio.ts | 3 +- 4 files changed, 151 insertions(+), 30 deletions(-) create mode 100644 prisma/migrations/20241015062910_update_payment_data/migration.sql diff --git a/prisma/migrations/20241015062910_update_payment_data/migration.sql b/prisma/migrations/20241015062910_update_payment_data/migration.sql new file mode 100644 index 0000000..f6b69e3 --- /dev/null +++ b/prisma/migrations/20241015062910_update_payment_data/migration.sql @@ -0,0 +1,37 @@ +/* + Warnings: + + - The values [PaymentWait] on the enum `QuotationStatus` will be removed. If these variants are still used in the database, this will fail. + +*/ +-- CreateEnum +CREATE TYPE "PaymentStatus" AS ENUM ('PaymentWait', 'PaymentSuccess'); + +-- AlterEnum +BEGIN; +CREATE TYPE "QuotationStatus_new" AS ENUM ('PaymentPending', 'PaymentInProcess', 'PaymentSuccess', 'ProcessComplete', 'Canceled'); +ALTER TABLE "Quotation" ALTER COLUMN "quotationStatus" DROP DEFAULT; +ALTER TABLE "Quotation" ALTER COLUMN "quotationStatus" TYPE "QuotationStatus_new" USING ("quotationStatus"::text::"QuotationStatus_new"); +ALTER TYPE "QuotationStatus" RENAME TO "QuotationStatus_old"; +ALTER TYPE "QuotationStatus_new" RENAME TO "QuotationStatus"; +DROP TYPE "QuotationStatus_old"; +ALTER TABLE "Quotation" ALTER COLUMN "quotationStatus" SET DEFAULT 'PaymentPending'; +COMMIT; + +-- AlterTable +ALTER TABLE "Quotation" ALTER COLUMN "quotationStatus" SET DEFAULT 'PaymentPending'; + +-- CreateTable +CREATE TABLE "QuotationPayment" ( + "id" TEXT NOT NULL, + "paymentStatus" "PaymentStatus" NOT NULL, + "date" TIMESTAMP(3) NOT NULL, + "amount" DOUBLE PRECISION NOT NULL, + "remark" TEXT, + "quotationId" TEXT NOT NULL, + + CONSTRAINT "QuotationPayment_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "QuotationPayment" ADD CONSTRAINT "QuotationPayment_quotationId_fkey" FOREIGN KEY ("quotationId") REFERENCES "Quotation"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0d842a6..239aa77 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1074,9 +1074,10 @@ model WorkProduct { enum QuotationStatus { PaymentPending - PaymentWait + PaymentInProcess // For Installments / Split Payment PaymentSuccess ProcessComplete + Canceled } enum PayCondition { @@ -1095,7 +1096,8 @@ model Quotation { status Status @default(CREATED) statusOrder Int @default(0) - quotationStatus QuotationStatus @default(PaymentWait) + quotationStatus QuotationStatus @default(PaymentPending) + quotationPaymentData QuotationPayment[] code String @@ -1137,6 +1139,24 @@ model Quotation { updatedByUserId String? } +enum PaymentStatus { + PaymentWait + PaymentSuccess +} + +model QuotationPayment { + id String @id @default(cuid()) + + paymentStatus PaymentStatus + + date DateTime + amount Float + remark String? + + quotation Quotation @relation(fields: [quotationId], references: [id]) + quotationId String +} + model QuotationPaySplit { id String @id @default(cuid()) diff --git a/src/controllers/05-quotation-payment-controller.ts b/src/controllers/05-quotation-payment-controller.ts index 1918438..a2acb78 100644 --- a/src/controllers/05-quotation-payment-controller.ts +++ b/src/controllers/05-quotation-payment-controller.ts @@ -1,21 +1,37 @@ -import { Controller, Path, Post, Put, Route, Tags } from "tsoa"; +import { Body, Controller, Get, Path, Post, Put, Query, Request, Route, Tags } from "tsoa"; +import express from "express"; import prisma from "../db"; import { notFoundError } from "../utils/error"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; +import { fileLocation, getFile, setFile } from "../utils/minio"; @Tags("Quotation") @Route("api/v1/quotation/{quotationId}/payment") export class QuotationPayment extends Controller { - @Post("submit") - async submitPayment(@Path("quotationId") id: string) { + @Get("quotationId") + async getPayment(@Path() quotationId: string) { + const record = await prisma.quotation.findFirst({ + where: { id: quotationId }, + include: { + quotationPaymentData: true, + createdBy: true, + updatedBy: true, + }, + }); + + return record; + } + + @Post() + async addPayment(@Path() quotationId: string, @Body() body: { amount: number; date: Date }) { const record = await prisma.quotation.findUnique({ - where: { id }, + where: { id: quotationId }, }); if (!record) throw notFoundError("Quotation"); - if (record.quotationStatus !== "PaymentPending" && record.quotationStatus !== "PaymentWait") { + 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, @@ -24,21 +40,43 @@ export class QuotationPayment extends Controller { ); } - await prisma.quotation.update({ - where: { id }, - data: { quotationStatus: "PaymentWait" }, + return await prisma.quotation.update({ + where: { id: quotationId }, + include: { quotationPaymentData: true }, + data: { + quotationStatus: "PaymentInProcess", + quotationPaymentData: { + create: { + paymentStatus: "PaymentWait", + ...body, + }, + }, + }, }); } - @Put("file") - async uploadPayment(@Path("quotationId") id: string) { + @Get("{paymentId}/file") + async getPaymentFile( + @Request() req: express.Request, + @Path() quotationId: string, + @Path() paymentId: string, + ) { + return req.res?.redirect(await getFile(fileLocation.quotation.payment(quotationId, paymentId))); + } + + @Put("{paymentId}/file") + async uploadPayment( + @Request() req: express.Request, + @Path() quotationId: string, + @Path() paymentId: string, + ) { const record = await prisma.quotation.findUnique({ - where: { id }, + where: { id: quotationId }, }); if (!record) throw notFoundError("Quotation"); - if (record.quotationStatus !== "PaymentPending" && record.quotationStatus !== "PaymentWait") { + 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, @@ -46,13 +84,22 @@ export class QuotationPayment extends Controller { "quotationStatusWrong", ); } + + return req.res?.redirect(await setFile(fileLocation.quotation.payment(quotationId, paymentId))); } @Post("confirm") - async confirmPayment(@Path("quotationId") id: string) { + 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, @@ -62,28 +109,44 @@ export class QuotationPayment extends Controller { }, }, }, - where: { id }, + where: { id: quotationId }, }); if (!record) throw notFoundError("Quotation"); await prisma.$transaction(async (tx) => { await tx.quotation.update({ - where: { id }, + where: { id: quotationId }, data: { - quotationStatus: "PaymentSuccess", - requestData: { - 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 } - : [], - ), - }, - })), + 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, }, }); }); diff --git a/src/utils/minio.ts b/src/utils/minio.ts index 2fdcf1f..8fc89bd 100644 --- a/src/utils/minio.ts +++ b/src/utils/minio.ts @@ -91,7 +91,8 @@ export const fileLocation = { img: (serviceId: string, name?: string) => `service/img-${serviceId}/${name || ""}`, }, quotation: { - payment: (quotationId: string) => `quotation/payment-${quotationId}`, + payment: (quotationId: string, paymentId: string) => + `quotation/payment-${quotationId}/${paymentId || ""}`, }, request: { attachment: (requestId: string, name?: string) =>