import { Body, Controller, Delete, Get, Head, Path, Put, Query, Request, Route, Security, Tags, } from "tsoa"; 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"; import flowAccount from "../services/flowaccount"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; const MANAGE_ROLES = ["system", "head_of_admin", "admin", "head_of_accountant", "accountant"]; function globalAllow(user: RequestWithUser["user"]) { const allowList = ["system", "head_of_admin", "head_of_accountant"]; 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: { installments: true, quotation: true, createdBy: true, }, }, }, orderBy: { createdAt: "asc" }, }), prisma.payment.count({ where }), ]); return { result, page, pageSize, total }; } @Get("{paymentId}") @Security("keycloak") async getPayment(@Path() paymentId: string) { const record = await prisma.payment.findFirst({ where: { id: paymentId }, include: { invoice: { include: { installments: true, quotation: true, 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"); if (record.paymentStatus === "PaymentSuccess") return record; return await prisma.$transaction(async (tx) => { const current = new Date(); const year = `${current.getFullYear()}`.slice(-2).padStart(2, "0"); const month = `${current.getMonth() + 1}`.padStart(2, "0"); const lastReceipt = body.paymentStatus === "PaymentSuccess" && record.paymentStatus !== "PaymentSuccess" ? await tx.runningNo.upsert({ where: { key: `RECEIPT_${year}${month}`, }, create: { key: `RECEIPT_${year}${month}`, value: 1, }, update: { value: { increment: 1 } }, }) : null; const quotation = record.invoice.quotation; const payment = await tx.payment.update({ where: { id: paymentId, invoice: { quotationId: quotation.id } }, data: { ...body, code: lastReceipt ? `RE${year}${month}${lastReceipt.value.toString().padStart(6, "0")}` : undefined, }, }); const paymentSum = await tx.payment.aggregate({ _sum: { amount: true }, where: { invoice: { quotationId: quotation.id, payment: { paymentStatus: "PaymentSuccess" }, }, }, }); await tx.quotation.update({ where: { id: quotation.id }, data: { quotationStatus: (paymentSum._sum.amount || 0) >= quotation.finalPrice ? "PaymentSuccess" : "PaymentInProcess", requestData: await (async () => { if ( body.paymentStatus === "PaymentSuccess" && (paymentSum._sum.amount || 0) - payment.amount <= 0 ) { const lastRequest = await tx.runningNo.upsert({ where: { key: `REQUEST_${year}${month}`, }, create: { key: `REQUEST_${year}${month}`, value: quotation.worker.length, }, update: { value: { increment: quotation.worker.length } }, }); return { create: quotation.worker.flatMap((v, i) => { const productEmployee = quotation.productServiceList.flatMap((item) => item.worker.findIndex((w) => w.employeeId === v.employeeId) !== -1 ? { productServiceId: item.id } : [], ); if (productEmployee.length <= 0) return []; return { code: `TR${year}${month}${(lastRequest.value - quotation.worker.length + i + 1).toString().padStart(6, "0")}`, employeeId: v.employeeId, requestWork: { create: quotation.productServiceList.flatMap((item) => item.worker.findIndex((w) => w.employeeId === v.employeeId) !== -1 ? { productServiceId: item.id } : [], ), }, }; }), }; } })(), }, }); return payment; }); } } @Route("api/v1/payment/{paymentId}/attachment") @Tags("Payment") export class PaymentFileController 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() @Security("keycloak") 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 data = await prisma.payment.findUnique({ where: { id: paymentId }, include: { invoice: true }, }); if (!data) throw notFoundError("Payment"); return req.res?.redirect( await getPresigned( "head", fileLocation.quotation.payment(data.invoice.quotationId, paymentId, name), ), ); } @Get("{name}") async getAttachment( @Request() req: RequestWithUser, @Path() paymentId: string, @Path() name: string, ) { const data = await prisma.payment.findUnique({ where: { id: paymentId }, include: { invoice: true }, }); if (!data) throw notFoundError("Payment"); return req.res?.redirect( await getFile(fileLocation.quotation.payment(data.invoice.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)); } } @Route("api/v1/payment/{paymentId}/flow-account") @Tags("Payment") export class FlowAccountController extends Controller { @Get() async getDocument(@Path() paymentId: string) { const payment = await prisma.payment.findFirst({ where: { id: paymentId, }, include: { invoice: true }, }); if (!payment) throw notFoundError("Payment"); if (payment.paymentStatus !== PaymentStatus.PaymentSuccess) throw new HttpError( HttpStatus.PRECONDITION_FAILED, "Payment not success", "paymentNotSuccess", ); if (!payment.invoice.flowAccountRecordId) { const result = await flowAccount.issueInvoice(payment.invoice.id); const documentId = result?.body?.data?.documentId; if (!documentId) { throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, "FlowAccount error", "flowAccountError", ); } await prisma.invoice.update({ where: { id: payment.invoice.id }, data: { flowAccountRecordId: String(documentId) }, }); if (documentId) { return flowAccount.getInvoiceDocument(documentId); } else { throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, "Failed to issue invoice/receipt document.", "InvoiceReceiptIssueFailed", ); } } else { return flowAccount.getInvoiceDocument(payment.invoice.flowAccountRecordId); } } }