import { Body, Controller, Delete, Get, Path, Post, Put, Query, Request, Route, Security, Tags, } from "tsoa"; import prisma from "../db"; import { RequestWithUser } from "../interfaces/user"; import { branchRelationPermInclude, createPermCheck, createPermCondition, } from "../services/permission"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; import { notFoundError } from "../utils/error"; import { CreditNotePaybackType, CreditNoteStatus, Prisma } from "@prisma/client"; import { queryOrNot } from "../utils/relation"; import { RequestWorkStatus } from "../generated/kysely/types"; const MANAGE_ROLES = [ "system", "head_of_admin", "admin", "head_of_accountant", "accountant", "head_of_sale", "sale", ]; function globalAllow(user: RequestWithUser["user"]) { const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"]; return allowList.some((v) => user.roles?.includes(v)); } // NOTE: permission condition/check in requestWork -> requestData -> quotation -> registeredBranch const permissionCond = createPermCondition(globalAllow); const permissionCondCompany = createPermCondition((_) => true); const permissionCheck = createPermCheck(globalAllow); const permissionCheckCompany = createPermCheck((_) => true); type CreditNoteCreate = { requestWorkId: string[]; quotationId: string; reason?: string; detail?: string; paybackType?: CreditNotePaybackType; paybackBank?: string; paybackAccount?: string; paybackAccountName?: string; }; type CreditNoteUpdate = { requestWorkId: string[]; quotationId: string; reason?: string; detail?: string; paybackType?: CreditNotePaybackType; paybackBank?: string; paybackAccount?: string; paybackAccountName?: string; }; @Route("api/v1/credit-note") @Tags("Credit Note") export class CreditNoteController extends Controller { @Get("stats") @Security("keycloak") async getCreditNoteStats(@Request() req: RequestWithUser, @Query() quotationId?: string) { const where = { requestWork: { some: { request: { quotationId, quotation: { registeredBranch: { OR: permissionCondCompany(req.user) }, }, }, }, }, } satisfies Prisma.CreditNoteWhereInput; return await prisma.creditNote.count({ where }); } @Get() @Security("keycloak") async getCreditNoteList( @Request() req: RequestWithUser, @Query() page: number = 1, @Query() pageSize: number = 30, @Query() query: string = "", @Query() quotationId?: string, ) { return await this.getCreditNoteListByCriteria(req, page, pageSize, query, quotationId); } // NOTE: only when needed or else remove this and implement in getCreditNoteList @Post("list") @Security("keycloak") async getCreditNoteListByCriteria( @Request() req: RequestWithUser, @Query() page: number = 1, @Query() pageSize: number = 30, @Query() query: string = "", @Query() quotationId?: string, @Body() body?: {}, ) { const where = { OR: queryOrNot(query, [ { requestWork: { some: { request: { OR: queryOrNot(query, [ { quotation: { code: { contains: query, mode: "insensitive" } } }, { quotation: { workName: { contains: query } } }, { quotation: { customerBranch: { OR: [ { code: { contains: query, mode: "insensitive" } }, { customerName: { contains: query } }, { firstName: { contains: query } }, { firstNameEN: { contains: query } }, { lastName: { contains: query } }, { lastNameEN: { contains: query } }, ], }, }, employee: { OR: [ { employeePassport: { some: { number: { contains: query } }, }, }, { code: { contains: query, mode: "insensitive" } }, { firstName: { contains: query } }, { firstNameEN: { contains: query } }, { lastName: { contains: query } }, { lastNameEN: { contains: query } }, ], }, }, ]), }, }, }, }, ]), requestWork: { some: { request: { quotationId, quotation: { registeredBranch: { OR: permissionCondCompany(req.user) }, }, }, }, }, } satisfies Prisma.CreditNoteWhereInput; const [result, total] = await prisma.$transaction([ prisma.creditNote.findMany({ where, include: { quotation: { include: { registeredBranch: true, customerBranch: { include: { customer: true, }, }, }, }, requestWork: { include: { request: true }, }, }, }), prisma.creditNote.count({ where }), ]); return { result, page, pageSize, total }; } @Get("{creditNoteId}") @Security("keycloak") async getCreditNote(@Request() req: RequestWithUser, @Path() creditNoteId: string) { const where = { id: creditNoteId, requestWork: { some: { request: { quotation: { registeredBranch: { OR: permissionCondCompany(req.user) }, }, }, }, }, } satisfies Prisma.CreditNoteWhereInput; return prisma.creditNote.findFirst({ where, include: { quotation: true, requestWork: { include: { request: true }, }, }, }); } @Post() @Security("keycloak", MANAGE_ROLES) async createCreditNote(@Request() req: RequestWithUser, @Body() body: CreditNoteCreate) { const requestWork = await prisma.requestWork.findMany({ where: { creditNote: null, request: { quotation: { id: body.quotationId, }, }, stepStatus: { some: { workStatus: "Canceled", }, }, id: { in: body.requestWorkId }, }, include: { stepStatus: true, productService: { include: { product: true, work: { include: { productOnWork: true }, }, }, }, request: { include: { quotation: { include: { registeredBranch: { include: branchRelationPermInclude(req.user) }, }, }, }, }, }, }); if (requestWork.length !== body.requestWorkId.length) { throw new HttpError(HttpStatus.BAD_REQUEST, "Not Match", "reqNotMet"); } await Promise.all( requestWork.map((item) => permissionCheck(req.user, item.request.quotation.registeredBranch)), ); const value = requestWork.reduce((a, c) => { const serviceChargeStepCount = c.productService.work?.productOnWork.find( (v) => v.productId === c.productService.productId, )?.stepCount; const successCount = c.stepStatus.filter((v) => v.workStatus === "Completed").length; if (serviceChargeStepCount && successCount) { return ( a + c.productService.product.price - c.productService.product.serviceCharge * successCount ); } return a + c.productService.product.price; }, 0); this.setStatus(HttpStatus.CREATED); return await prisma.$transaction( async (tx) => { const currentYear = new Date().getFullYear(); const currentMonth = new Date().getMonth() + 1; const last = await tx.runningNo.upsert({ where: { key: `CREDIT_NOTE_${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}`, }, create: { key: `CREDIT_NOTE_${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}`, value: 1, }, update: { value: { increment: 1 } }, }); return await prisma.creditNote.create({ include: { requestWork: { include: { request: true, }, }, quotation: true, }, data: { reason: body.reason, detail: body.detail, paybackType: body.paybackType, paybackBank: body.paybackBank, paybackAccount: body.paybackAccount, paybackAccountName: body.paybackAccountName, code: `CN${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${last.value.toString().padStart(6, "0")}`, value, requestWork: { connect: body.requestWorkId.map((v) => ({ id: v, })), }, quotationId: body.quotationId, }, }); }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }, ); } @Put("{creditNoteId}") @Security("keycloak", MANAGE_ROLES) async updateCreditNote( @Request() req: RequestWithUser, @Path() creditNoteId: string, @Body() body: CreditNoteUpdate, ) { const creditNoteData = await prisma.creditNote.findFirst({ where: { id: creditNoteId }, include: { requestWork: true, quotation: { include: { registeredBranch: { include: branchRelationPermInclude(req.user) }, }, }, }, }); if (!creditNoteData) throw notFoundError("Credit Note"); await permissionCheck(req.user, creditNoteData.quotation.registeredBranch); const requestWork = await prisma.requestWork.findMany({ where: { OR: [{ creditNote: null }, { creditNoteId }], request: { quotation: { id: body.quotationId, }, }, stepStatus: { some: { workStatus: RequestWorkStatus.Canceled, }, }, id: { in: body.requestWorkId }, }, include: { stepStatus: true, productService: { include: { product: true, work: { include: { productOnWork: true }, }, }, }, request: { include: { quotation: { include: { registeredBranch: { include: branchRelationPermInclude(req.user) }, }, }, }, }, }, }); if (requestWork.length !== body.requestWorkId.length) { throw new HttpError(HttpStatus.BAD_REQUEST, "Not Match", "reqNotMet"); } const value = requestWork.reduce((a, c) => { const serviceChargeStepCount = c.productService.work?.productOnWork.find( (v) => v.productId === c.productService.productId, )?.stepCount; const successCount = c.stepStatus.filter((v) => v.workStatus === "Completed").length; if (serviceChargeStepCount && successCount) { return ( a + c.productService.product.price - c.productService.product.serviceCharge * successCount ); } return a + c.productService.product.price; }, 0); const record = await prisma.creditNote.update({ where: { id: creditNoteId }, include: { requestWork: { include: { request: true, }, }, quotation: true, }, data: { reason: body.reason, detail: body.detail, paybackType: body.paybackType, paybackBank: body.paybackBank, paybackAccount: body.paybackAccount, paybackAccountName: body.paybackAccountName, value, requestWork: { disconnect: creditNoteData.requestWork .map((item) => ({ id: item.id, })) .filter((data) => !body.requestWorkId.find((item) => (item = data.id))), connect: body.requestWorkId.map((v) => ({ id: v, })), }, quotationId: body.quotationId, }, }); return record; } @Delete("{creditNoteId}") @Security("keycloak", MANAGE_ROLES) async deleteCreditNote(@Request() req: RequestWithUser, @Path() creditNoteId: string) { const record = await prisma.creditNote.findFirst({ where: { id: creditNoteId, }, include: { quotation: { include: { registeredBranch: { include: branchRelationPermInclude(req.user) }, }, }, }, }); if (!record) throw notFoundError("Credit Note"); await permissionCheck(req.user, record.quotation.registeredBranch); return await prisma.creditNote.delete({ where: { id: creditNoteId } }); } } @Route("api/v1/credit-note/{creditNoteId}") @Tags("Credit Note") export class CreditNoteActionController extends Controller { async #checkPermission(user: RequestWithUser["user"], id: string) { const creditNoteData = await prisma.creditNote.findFirst({ where: { id }, include: { requestWork: true, quotation: { include: { registeredBranch: { include: branchRelationPermInclude(user) }, }, }, }, }); if (!creditNoteData) throw notFoundError("Credit Note"); await permissionCheck(user, creditNoteData.quotation.registeredBranch); return creditNoteData; } @Post("status") @Security("keycloak", MANAGE_ROLES) async updateStatus( @Request() req: RequestWithUser, @Path() creditNoteId: string, @Body() body: CreditNoteStatus, ) { await this.#checkPermission(req.user, creditNoteId); return await prisma.creditNote.update({ where: { id: creditNoteId }, include: { requestWork: { include: { request: true, }, }, quotation: true, }, data: { creditNoteStatus: body, }, }); } }