import { CustomerType, Prisma, Status } from "@prisma/client"; import { Body, Controller, Delete, Get, Path, Post, Put, Query, Request, Route, Security, Tags, } from "tsoa"; import { RequestWithUser } from "../interfaces/user"; import prisma from "../db"; import minio, { deleteFolder } from "../services/minio"; import HttpStatus from "../interfaces/http-status"; import HttpError from "../interfaces/http-error"; import { isSystem } from "../utils/keycloak"; import { branchRelationPermInclude, createPermCheck, createPermCondition, } from "../services/permission"; import { filterStatus } from "../services/prisma"; import { fileLocation, listFile } from "../utils/minio"; if (!process.env.MINIO_BUCKET) { throw Error("Require MinIO bucket."); } const MINIO_BUCKET = process.env.MINIO_BUCKET; const MANAGE_ROLES = [ "system", "head_of_admin", "admin", "head_of_account", "account", "head_of_sale", "sale", ]; function globalAllow(user: RequestWithUser["user"]) { const allowList = ["system", "head_of_admin", "admin", "head_of_account", "head_of_sale"]; return allowList.some((v) => user.roles?.includes(v)); } const permissionCond = createPermCondition(globalAllow); const permissionCheck = createPermCheck(globalAllow); export type CustomerCreate = { registeredBranchId?: string; status?: Status; customerType: CustomerType; namePrefix: string; firstName: string; firstNameEN?: string; lastName: string; lastNameEN?: string; gender: string; birthDate: Date; }; export type CustomerUpdate = { registeredBranchId?: string; status?: "ACTIVE" | "INACTIVE"; customerType?: CustomerType; namePrefix?: string; firstName?: string; firstNameEN?: string; lastName?: string; lastNameEN?: string; gender?: string; birthDate?: Date; }; @Route("api/v1/customer") @Tags("Customer") export class CustomerController extends Controller { @Get("type-stats") @Security("keycloak") async stat(@Request() req: RequestWithUser) { const list = await prisma.customer.groupBy({ by: "customerType", _count: true, where: { registeredBranch: isSystem(req.user) ? undefined : { OR: permissionCond(req.user) }, }, }); return list.reduce>( (a, c) => { a[c.customerType] = c._count; return a; }, { CORP: 0, PERS: 0, }, ); } @Get() @Security("keycloak") async list( @Request() req: RequestWithUser, @Query() customerType?: CustomerType, @Query() query: string = "", @Query() status?: Status, @Query() page: number = 1, @Query() pageSize: number = 30, @Query() includeBranch: boolean = false, ) { const where = { OR: [ { namePrefix: { contains: query } }, { firstName: { contains: query } }, { firstNameEN: { contains: query } }, ], AND: { customerType, ...filterStatus(status), registeredBranch: isSystem(req.user) ? undefined : { OR: permissionCond(req.user) }, }, } satisfies Prisma.CustomerWhereInput; const [result, total] = await prisma.$transaction([ prisma.customer.findMany({ include: { _count: true, branch: includeBranch ? { include: { province: true, district: true, subDistrict: true, }, orderBy: { createdAt: "asc" }, } : undefined, createdBy: true, updatedBy: true, }, orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }], where, take: pageSize, skip: (page - 1) * pageSize, }), prisma.customer.count({ where }), ]); return { result, page, pageSize, total }; } @Get("{customerId}") @Security("keycloak") async getById(@Path() customerId: string) { const [record, countEmployee] = await prisma.$transaction([ prisma.customer.findFirst({ include: { branch: { include: { province: true, district: true, subDistrict: true, }, orderBy: { createdAt: "asc" }, }, createdBy: true, updatedBy: true, }, where: { id: customerId }, }), prisma.employee.count({ where: { customerBranch: { customerId } } }), ]); if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "customerNotFound"); } return Object.assign(record, { _count: { employee: countEmployee } }); } @Post() @Security("keycloak", MANAGE_ROLES) async create(@Request() req: RequestWithUser, @Body() body: CustomerCreate) { // NOTE: handle empty string if (!body.registeredBranchId) { body.registeredBranchId = undefined; } const [branch] = await prisma.$transaction([ prisma.branch.findFirst({ where: { id: body.registeredBranchId }, include: branchRelationPermInclude(req.user), }), ]); if (!!body.registeredBranchId && !branch) { throw new HttpError( HttpStatus.BAD_REQUEST, "Branch cannot be found.", "relationBranchNotFound", ); } if (body.registeredBranchId !== undefined && branch) { await permissionCheck(req.user, branch); } const record = await prisma.$transaction( async (tx) => { await tx.branch.updateMany({ where: { id: body.registeredBranchId, status: "CREATED", }, data: { status: "INACTIVE", statusOrder: 1, }, }); return await tx.customer.create({ include: { branch: { include: { province: true, district: true, subDistrict: true, }, }, createdBy: true, updatedBy: true, }, data: { ...body, statusOrder: +(body.status === "INACTIVE"), createdByUserId: req.user.sub, updatedByUserId: req.user.sub, }, }); }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }, ); this.setStatus(HttpStatus.CREATED); return record; } @Put("{customerId}") @Security("keycloak", MANAGE_ROLES) async editById( @Path() customerId: string, @Request() req: RequestWithUser, @Body() body: CustomerUpdate, ) { if (body.registeredBranchId === "") { body.registeredBranchId = undefined; } const customer = await prisma.customer.findUnique({ where: { id: customerId }, include: { registeredBranch: { include: branchRelationPermInclude(req.user), }, }, }); if (!customer) { throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "customerNotFound"); } const [branch] = await prisma.$transaction([ prisma.branch.findFirst({ where: { id: body.registeredBranchId }, include: branchRelationPermInclude(req.user), }), ]); if (!!body.registeredBranchId && !branch) { throw new HttpError( HttpStatus.BAD_REQUEST, "Branch cannot be found.", "relationBranchNotFound", ); } if (customer.registeredBranch) { await permissionCheck(req.user, customer.registeredBranch); } if (body.registeredBranchId !== undefined && branch) { await permissionCheck(req.user, branch); } const record = await prisma.$transaction(async (tx) => { return await tx.customer.update({ include: { branch: { include: { province: true, district: true, subDistrict: true, }, }, createdBy: true, updatedBy: true, }, where: { id: customerId }, data: { ...body, statusOrder: +(body.status === "INACTIVE"), updatedByUserId: req.user.sub, }, }); }); return record; } @Delete("{customerId}") @Security("keycloak", MANAGE_ROLES) async deleteById(@Path() customerId: string, @Request() req: RequestWithUser) { const record = await prisma.customer.findFirst({ where: { id: customerId }, include: { registeredBranch: { include: branchRelationPermInclude(req.user), }, }, }); if (!record) { throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found.", "customerNotFound"); } await permissionCheck(req.user, record.registeredBranch); if (record.status !== Status.CREATED) { throw new HttpError(HttpStatus.FORBIDDEN, "Customer is in used.", "customerInUsed"); } return await prisma.customer .delete({ where: { id: customerId } }) .then( async (data) => await deleteFolder(MINIO_BUCKET, `customer/${customerId}`).then(() => data), ); } } @Route("api/v1/customer/{customerId}/image") @Tags("Customer") export class CustomerImageController extends Controller { @Get() @Security("keycloak") async listImage(@Path() customerId: string) { const customer = await prisma.customer.findUnique({ where: { id: customerId }, }); if (!customer) { throw new HttpError(HttpStatus.NOT_FOUND, "Customer cannot be found", "customerNotFound"); } return await listFile(fileLocation.customer.img(customerId)); } @Get("{name}") async getImage( @Request() req: RequestWithUser, @Path() customerId: string, @Path() name: string, ) { return req.res?.redirect( await minio.presignedGetObject( MINIO_BUCKET, fileLocation.customer.img(customerId, name), 12 * 60 * 60, ), ); } @Put("{name}") @Security("keycloak") async putImage( @Request() req: RequestWithUser, @Path() customerId: string, @Path() name: string, ) { const customer = await prisma.customer.findUnique({ where: { id: customerId }, include: { registeredBranch: { include: branchRelationPermInclude(req.user), }, }, }); if (!customer) { throw new HttpError(HttpStatus.NOT_FOUND, "Image cannot be found", "imageNotFound"); } console.log(customer.registeredBranch); await permissionCheck(req.user, customer.registeredBranch); return req.res?.redirect( await minio.presignedPutObject( MINIO_BUCKET, fileLocation.customer.img(customerId, name), 12 * 60 * 60, ), ); } @Delete("{name}") @Security("keycloak") async deleteImage( @Request() req: RequestWithUser, @Path() customerId: string, @Path() name: string, ) { const customer = await prisma.customer.findUnique({ where: { id: customerId }, include: { registeredBranch: { include: branchRelationPermInclude(req.user), }, }, }); if (!customer) { throw new HttpError(HttpStatus.NOT_FOUND, "Image cannot be found", "imageNotFound"); } await permissionCheck(req.user, customer.registeredBranch); await minio.removeObject(MINIO_BUCKET, fileLocation.customer.img(customerId, name), { forceDelete: true, }); } }