refactor!: payment sys
This commit is contained in:
parent
3cbc157028
commit
02e17fcde4
7 changed files with 675 additions and 279 deletions
|
|
@ -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;
|
||||||
|
|
@ -423,6 +423,8 @@ model User {
|
||||||
quotationUpdated Quotation[] @relation("QuotationUpdatedByUser")
|
quotationUpdated Quotation[] @relation("QuotationUpdatedByUser")
|
||||||
flowCreated WorkflowTemplate[] @relation("FlowCreatedByUser")
|
flowCreated WorkflowTemplate[] @relation("FlowCreatedByUser")
|
||||||
flowUpdated WorkflowTemplate[] @relation("FlowUpdatedByUser")
|
flowUpdated WorkflowTemplate[] @relation("FlowUpdatedByUser")
|
||||||
|
invoiceCreated Invoice[]
|
||||||
|
paymentCreated Payment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CustomerType {
|
enum CustomerType {
|
||||||
|
|
@ -1106,8 +1108,7 @@ model Quotation {
|
||||||
status Status @default(CREATED)
|
status Status @default(CREATED)
|
||||||
statusOrder Int @default(0)
|
statusOrder Int @default(0)
|
||||||
|
|
||||||
quotationStatus QuotationStatus @default(PaymentPending)
|
quotationStatus QuotationStatus @default(PaymentPending)
|
||||||
quotationPaymentData QuotationPayment[]
|
|
||||||
|
|
||||||
remark String?
|
remark String?
|
||||||
|
|
||||||
|
|
@ -1152,26 +1153,8 @@ model Quotation {
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
updatedBy User? @relation(name: "QuotationUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
updatedBy User? @relation(name: "QuotationUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||||
updatedByUserId String?
|
updatedByUserId String?
|
||||||
}
|
|
||||||
|
|
||||||
enum PaymentStatus {
|
invoice Invoice[]
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model QuotationPaySplit {
|
model QuotationPaySplit {
|
||||||
|
|
@ -1221,6 +1204,9 @@ model QuotationProductServiceList {
|
||||||
|
|
||||||
worker QuotationProductServiceWorker[]
|
worker QuotationProductServiceWorker[]
|
||||||
requestWork RequestWork[]
|
requestWork RequestWork[]
|
||||||
|
|
||||||
|
invoice Invoice? @relation(fields: [invoiceId], references: [id])
|
||||||
|
invoiceId String?
|
||||||
}
|
}
|
||||||
|
|
||||||
model QuotationProductServiceWorker {
|
model QuotationProductServiceWorker {
|
||||||
|
|
@ -1233,6 +1219,46 @@ model QuotationProductServiceWorker {
|
||||||
@@id([productServiceId, employeeId])
|
@@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 {
|
model RequestData {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
|
||||||
|
|
|
||||||
233
src/controllers/04-invoice-controller.ts
Normal file
233
src/controllers/04-invoice-controller.ts
Normal file
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/controllers/04-receipt-controller.ts
Normal file
90
src/controllers/04-receipt-controller.ts
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
249
src/controllers/05-payment-controller.ts
Normal file
249
src/controllers/05-payment-controller.ts
Normal file
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -490,20 +490,6 @@ export class QuotationController extends Controller {
|
||||||
data: {
|
data: {
|
||||||
...rest,
|
...rest,
|
||||||
...price,
|
...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"),
|
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")}`,
|
code: `${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${currentDate.toString().padStart(2, "0")}${lastQuotation.value.toString().padStart(4, "0")}`,
|
||||||
worker: {
|
worker: {
|
||||||
|
|
@ -746,24 +732,6 @@ export class QuotationController extends Controller {
|
||||||
data: {
|
data: {
|
||||||
...rest,
|
...rest,
|
||||||
...price,
|
...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"),
|
statusOrder: +(rest.status === "INACTIVE"),
|
||||||
worker:
|
worker:
|
||||||
sortedEmployeeId.length > 0
|
sortedEmployeeId.length > 0
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue