import { Prisma, Status } from "@prisma/client"; import { Body, Controller, Delete, Get, Head, Path, Post, Put, Query, Request, Route, Security, Tags, } from "tsoa"; import { RequestWithUser } from "../interfaces/user"; import prisma from "../db"; import HttpStatus from "../interfaces/http-status"; import { isSystem } from "../utils/keycloak"; import { branchRelationPermInclude, createPermCheck, createPermCondition, } from "../services/permission"; import { filterStatus } from "../services/prisma"; import { connectOrDisconnect, connectOrNot, queryOrNot, whereAddressQuery, } from "../utils/relation"; import { isUsedError, notFoundError, relationError } from "../utils/error"; import { deleteFile, deleteFolder, fileLocation, getFile, getPresigned, listFile, setFile, } from "../utils/minio"; 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", "head_of_account", "head_of_sale"]; return allowList.some((v) => user.roles?.includes(v)); } const permissionCondCompany = createPermCondition((_) => true); const permissionCond = createPermCondition(globalAllow); const permissionCheck = createPermCheck(globalAllow); export type CustomerBranchCreate = { customerId: string; // NOTE: About (Natural Person) citizenId?: string; namePrefix?: string; firstName?: string; firstNameEN?: string; lastName?: string; lastNameEN?: string; gender?: string; birthDate?: Date; // NOTE: About (Legal Entity) legalPersonNo?: string; registerName?: string; registerNameEN?: string; registerDate?: Date; authorizedCapital?: string; authorizedName?: string; authorizedNameEN?: string; customerName?: string; telephoneNo: string; status?: Status; homeCode: string; employmentOffice: string; employmentOfficeEN: string; address: string; addressEN: string; soi?: string | null; soiEN?: string | null; moo?: string | null; mooEN?: string | null; street?: string | null; streetEN?: string | null; email: string; contactTel: string; officeTel: string; contactName: string; agent: string; businessType: string; jobPosition: string; jobDescription: string; payDate: string; payDateEN: string; wageRate: number; wageRateText: string; subDistrictId?: string | null; districtId?: string | null; provinceId?: string | null; }; export type CustomerBranchUpdate = { customerId: string; // NOTE: About (Natural Person) citizenId?: string; namePrefix?: string; firstName?: string; firstNameEN?: string; lastName?: string; lastNameEN?: string; gender?: string; birthDate?: Date; // NOTE: About (Legal Entity) legalPersonNo?: string; registerName?: string; registerNameEN?: string; registerDate?: Date; authorizedCapital?: string; authorizedName?: string; authorizedNameEN?: string; customerName?: string; telephoneNo: string; status?: Status; homeCode?: string; employmentOffice?: string; employmentOfficeEN?: string; address?: string; addressEN?: string; soi?: string | null; soiEN?: string | null; moo?: string | null; mooEN?: string | null; street?: string | null; streetEN?: string | null; email?: string; contactTel?: string; officeTel?: string; contactName?: string; agent?: string; businessType?: string; jobPosition?: string; jobDescription?: string; payDate?: string; payDateEN?: string; wageRate?: number; wageRateText?: string; subDistrictId?: string | null; districtId?: string | null; provinceId?: string | null; }; @Route("api/v1/customer-branch") @Tags("Customer Branch") export class CustomerBranchController extends Controller { @Get() @Security("keycloak") async list( @Request() req: RequestWithUser, @Query() zipCode?: string, @Query() company?: boolean, @Query() customerId?: string, @Query() registeredBranchId?: string, @Query() status?: Status, @Query() includeCustomer?: boolean, @Query() query: string = "", @Query() page: number = 1, @Query() pageSize: number = 30, ) { const where = { OR: queryOrNot(query, [ { registerName: { contains: query } }, { registerNameEN: { contains: query } }, { email: { contains: query } }, { code: { contains: query } }, { firstName: { contains: query } }, { firstNameEN: { contains: query } }, { lastName: { contains: query } }, { lastNameEN: { contains: query } }, ...whereAddressQuery(query), ]), AND: { customer: isSystem(req.user) ? { registeredBranchId } : { registeredBranch: { AND: { id: registeredBranchId }, OR: company ? permissionCondCompany(req.user) : permissionCond(req.user), }, }, customerId, subDistrict: zipCode ? { zipCode } : undefined, ...filterStatus(status), }, } satisfies Prisma.CustomerBranchWhereInput; const [result, total] = await prisma.$transaction([ prisma.customerBranch.findMany({ orderBy: [{ code: "asc" }, { statusOrder: "asc" }, { createdAt: "asc" }], include: { customer: includeCustomer, province: true, district: true, subDistrict: true, createdBy: true, updatedBy: true, _count: true, }, where, take: pageSize, skip: (page - 1) * pageSize, }), prisma.customerBranch.count({ where }), ]); return { result, page, pageSize, total }; } @Get("{branchId}") @Security("keycloak") async getById(@Path() branchId: string) { const record = await prisma.customerBranch.findFirst({ include: { customer: true, province: true, district: true, subDistrict: true, createdBy: true, updatedBy: true, }, where: { id: branchId }, }); if (!record) throw notFoundError("Branch"); return record; } @Get("{branchId}/employee") @Security("keycloak") async listEmployee( @Path() branchId: string, @Query() zipCode?: string, @Query() gender?: string, @Query() status?: Status, @Query() query: string = "", @Query() passport?: boolean, @Query() visa?: boolean, @Query() page: number = 1, @Query() pageSize: number = 30, ) { const where = { OR: queryOrNot(query, [ { firstName: { contains: query } }, { firstNameEN: { contains: query } }, { lastName: { contains: query } }, { lastNameEN: { contains: query } }, ...whereAddressQuery(query), ]), AND: { ...filterStatus(status), customerBranchId: branchId, subDistrict: zipCode ? { zipCode } : undefined, gender, }, } satisfies Prisma.EmployeeWhereInput; const [result, total] = await prisma.$transaction([ prisma.employee.findMany({ orderBy: { createdAt: "asc" }, include: { province: true, district: true, subDistrict: true, employeePassport: passport, employeeVisa: visa, createdBy: true, updatedBy: true, }, where, take: pageSize, skip: (page - 1) * pageSize, }), prisma.employee.count({ where }), ]); return { result, page, pageSize, total, }; } @Post() @Security("keycloak", MANAGE_ROLES) async create(@Request() req: RequestWithUser, @Body() body: CustomerBranchCreate) { const [province, district, subDistrict, customer] = await prisma.$transaction([ prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), prisma.district.findFirst({ where: { id: body.districtId || undefined } }), prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), prisma.customer.findFirst({ where: { id: body.customerId || undefined }, include: { registeredBranch: { include: branchRelationPermInclude(req.user), }, branch: { take: 1, orderBy: { createdAt: "asc" }, }, }, }), ]); if (body.provinceId && !province) throw relationError("Province"); if (body.districtId && !district) throw relationError("District"); if (body.subDistrictId && !subDistrict) throw relationError("SubDistrict"); if (!customer) throw relationError("Customer"); let company = await permissionCheck(req.user, customer.registeredBranch).then( (v) => (v.headOffice || v).code, ); const { provinceId, districtId, subDistrictId, customerId, ...rest } = body; const record = await prisma.$transaction( async (tx) => { const headoffice = customer.branch.at(0); const headofficeCode = headoffice?.code.slice(0, -3); let runningKey = ""; if (headofficeCode) { runningKey = `CUSTOMER_BRANCH_${company}_${headofficeCode}`; } else if ("citizenId" in body) { runningKey = `CUSTOMER_BRANCH_${company}_${body.citizenId}`; } else { runningKey = `CUSTOMER_BRANCH_${company}_${body.legalPersonNo}`; } const last = await tx.runningNo.upsert({ where: { key: runningKey }, create: { key: runningKey, value: 1, }, update: { value: { increment: 1 } }, }); if (headoffice) { await tx.customerBranch.updateMany({ where: { id: headoffice.id, status: "CREATED", }, data: { status: "ACTIVE" }, }); } return await tx.customerBranch.create({ include: { province: true, district: true, subDistrict: true, createdBy: true, updatedBy: true, }, data: { ...rest, code: `${runningKey.replace(`CUSTOMER_BRANCH_${company}_`, "")}-${`${last.value - 1}`.padStart(2, "0")}`, codeCustomer: runningKey.replace(`CUSTOMER_BRANCH_${company}_`, ""), customer: { connect: { id: customerId } }, province: { connect: provinceId ? { id: provinceId } : undefined }, district: { connect: districtId ? { id: districtId } : undefined }, subDistrict: { connect: subDistrictId ? { id: subDistrictId } : undefined }, createdBy: { connect: { id: req.user.sub } }, updatedBy: { connect: { id: req.user.sub } }, }, }); }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }, ); this.setStatus(HttpStatus.CREATED); return record; } @Put("{branchId}") @Security("keycloak", MANAGE_ROLES) async editById( @Request() req: RequestWithUser, @Body() body: CustomerBranchUpdate, @Path() branchId: string, ) { const branch = await prisma.customerBranch.findUnique({ where: { id: branchId }, include: { customer: { include: { registeredBranch: { include: branchRelationPermInclude(req.user), }, }, }, }, }); if (!branch) throw notFoundError("Customer Branch"); await permissionCheck(req.user, branch.customer.registeredBranch); if (!body.customerId) body.customerId = branch.customerId; if (body.provinceId || body.districtId || body.subDistrictId || body.customerId) { const [province, district, subDistrict, customer] = await prisma.$transaction([ prisma.province.findFirst({ where: { id: body.provinceId || undefined } }), prisma.district.findFirst({ where: { id: body.districtId || undefined } }), prisma.subDistrict.findFirst({ where: { id: body.subDistrictId || undefined } }), prisma.customer.findFirst({ where: { id: body.customerId || undefined }, include: { registeredBranch: { include: branchRelationPermInclude(req.user), }, }, }), ]); if (body.provinceId && !province) throw relationError("Province"); if (body.districtId && !district) throw relationError("District"); if (body.subDistrictId && !subDistrict) throw relationError("SubDistrict"); if (!customer) throw relationError("Customer"); await permissionCheck(req.user, customer.registeredBranch); } const { provinceId, districtId, subDistrictId, customerId, ...rest } = body; return await prisma.customerBranch.update({ where: { id: branchId }, include: { province: true, district: true, subDistrict: true, createdBy: true, updatedBy: true, }, data: { ...rest, statusOrder: +(rest.status === "INACTIVE"), customer: connectOrNot(customerId), province: connectOrDisconnect(provinceId), district: connectOrDisconnect(districtId), subDistrict: connectOrDisconnect(subDistrictId), updatedBy: { connect: { id: req.user.sub } }, }, }); } @Delete("{branchId}") @Security("keycloak", MANAGE_ROLES) async delete(@Request() req: RequestWithUser, @Path() branchId: string) { const record = await prisma.customerBranch.findFirst({ where: { id: branchId }, include: { customer: { include: { registeredBranch: { include: branchRelationPermInclude(req.user), }, }, }, }, }); if (!record) throw notFoundError("Customer Branch"); await permissionCheck(req.user, record.customer.registeredBranch); if (record.status !== Status.CREATED) throw isUsedError("Customer Branch"); return await prisma.$transaction(async (tx) => { if (record.code.endsWith("00")) { await Promise.all([ tx.customer.delete({ where: { id: record.customerId }, }), tx.runningNo.delete({ where: { key: record.code.slice(0, -3) }, }), ]); } return await prisma.customerBranch .delete({ include: { createdBy: true, updatedBy: true }, where: { id: branchId }, }) .then((v) => Promise.all([ deleteFolder(fileLocation.customerBranch.attachment(branchId)), deleteFolder(fileLocation.customerBranch.citizen(branchId)), deleteFolder(fileLocation.customerBranch.powerOfAttorney(branchId)), deleteFolder(fileLocation.customerBranch.vatRegistration(branchId)), deleteFolder(fileLocation.customerBranch.houseRegistration(branchId)), deleteFolder(fileLocation.customerBranch.commercialRegistration(branchId)), ]).then(() => v), ); }); } } @Route("api/v1/customer-branch/{branchId}") export class CustomerBranchFileController extends Controller { private async checkPermission(user: RequestWithUser["user"], id: string) { const data = await prisma.customerBranch.findFirst({ where: { id }, include: { customer: { include: { registeredBranch: { include: branchRelationPermInclude(user), }, }, }, }, }); if (!data) throw notFoundError("Customer Branch"); await permissionCheck(user, data.customer.registeredBranch); } @Get("attachment") @Security("keycloak") @Tags("Customer Branch") async listAttachment(@Request() req: RequestWithUser, @Path() branchId: string) { await this.checkPermission(req.user, branchId); return await listFile(fileLocation.customerBranch.attachment(branchId)); } @Get("attachment/{name}") @Security("keycloak") @Tags("Customer Branch") async getAttachment(@Path() branchId: string, @Path() name: string) { return await getFile(fileLocation.customerBranch.attachment(branchId, name)); } @Head("attachment/{name}") @Security("keycloak") @Tags("Customer Branch") async headAttachment(@Path() branchId: string, @Path() name: string) { return await getPresigned("head", fileLocation.customerBranch.attachment(branchId, name)); } @Put("attachment/{name}") @Security("keycloak") @Tags("Customer Branch") async putAttachment( @Request() req: RequestWithUser, @Path() branchId: string, @Path() name: string, ) { await this.checkPermission(req.user, branchId); return await setFile(fileLocation.customerBranch.attachment(branchId, name)); } @Delete("attachment/{name}") @Security("keycloak") @Tags("Customer Branch") async delAttachment( @Request() req: RequestWithUser, @Path() branchId: string, @Path() name: string, ) { await this.checkPermission(req.user, branchId); return await deleteFile(fileLocation.customerBranch.attachment(branchId, name)); } @Get("file-citizen") @Security("keycloak") @Tags("Customer Branch Citizen") async listCitizen(@Request() req: RequestWithUser, @Path() branchId: string) { await this.checkPermission(req.user, branchId); return await listFile(fileLocation.customerBranch.attachment(branchId)); } @Get("file-citizen/{id}") @Security("keycloak") @Tags("Customer Branch Citizen") async getCitizen(@Path() branchId: string, @Path() id: string) { return await getFile(fileLocation.customerBranch.attachment(branchId, id)); } @Put("file-citizen/{id}") @Security("keycloak") @Tags("Customer Branch Citizen") async putCitizen(@Request() req: RequestWithUser, @Path() branchId: string, @Path() id: string) { await this.checkPermission(req.user, branchId); return req.res?.redirect(await setFile(fileLocation.customerBranch.attachment(branchId, id))); } @Delete("file-citizen/{id}") @Security("keycloak") @Tags("Customer Branch Citizen") async delCitizen(@Request() req: RequestWithUser, @Path() branchId: string, @Path() id: string) { await this.checkPermission(req.user, branchId); return await deleteFile(fileLocation.customerBranch.citizen(branchId, id)); } @Get("file-power-of-attorney") @Security("keycloak") @Tags("Customer Branch Power of Attorney") async listPoa(@Request() req: RequestWithUser, @Path() branchId: string) { await this.checkPermission(req.user, branchId); return await listFile(fileLocation.customerBranch.powerOfAttorney(branchId)); } @Get("file-power-of-attorney/{id}") @Security("keycloak") @Tags("Customer Branch Power of Attorney") async getPoa(@Path() branchId: string, @Path() id: string) { return await getFile(fileLocation.customerBranch.powerOfAttorney(branchId, id)); } @Put("file-power-of-attorney/{id}") @Security("keycloak") @Tags("Customer Branch Power of Attorney") async putPoa(@Request() req: RequestWithUser, @Path() branchId: string, @Path() id: string) { await this.checkPermission(req.user, branchId); return req.res?.redirect( await setFile(fileLocation.customerBranch.powerOfAttorney(branchId, id)), ); } @Delete("file-power-of-attorney/{id}") @Security("keycloak") @Tags("Customer Branch Power of Attorney") async delPoa(@Request() req: RequestWithUser, @Path() branchId: string, @Path() id: string) { await this.checkPermission(req.user, branchId); return await deleteFile(fileLocation.customerBranch.powerOfAttorney(branchId, id)); } @Get("file-house-registration") @Tags("Customer Branch House Registration") @Security("keycloak") async listHouseRegis(@Request() req: RequestWithUser, @Path() branchId: string) { await this.checkPermission(req.user, branchId); return await listFile(fileLocation.customerBranch.houseRegistration(branchId)); } @Get("file-house-registration/{id}") @Security("keycloak") @Tags("Customer Branch House Registration") async getHouseRegis(@Path() branchId: string, @Path() id: string) { return await getFile(fileLocation.customerBranch.houseRegistration(branchId, id)); } @Put("file-house-registration/{id}") @Security("keycloak") @Tags("Customer Branch House Registration") async putHouseRegis( @Request() req: RequestWithUser, @Path() branchId: string, @Path() id: string, ) { await this.checkPermission(req.user, branchId); return req.res?.redirect( await setFile(fileLocation.customerBranch.houseRegistration(branchId, id)), ); } @Delete("file-house-registration/{id}") @Security("keycloak") @Tags("Customer Branch House Registration") async delHouseRegis( @Request() req: RequestWithUser, @Path() branchId: string, @Path() id: string, ) { await this.checkPermission(req.user, branchId); return await deleteFile(fileLocation.customerBranch.houseRegistration(branchId, id)); } @Get("file-commercial-registration") @Security("keycloak") @Tags("Customer Branch Commercial Registration") async listCommercialRegis(@Request() req: RequestWithUser, @Path() branchId: string) { await this.checkPermission(req.user, branchId); return await listFile(fileLocation.customerBranch.commercialRegistration(branchId)); } @Get("file-commercial-registration/{id}") @Security("keycloak") @Tags("Customer Branch Commercial Registration") async getCommercialRegis(@Path() branchId: string, @Path() id: string) { return await getFile(fileLocation.customerBranch.commercialRegistration(branchId, id)); } @Put("file-commercial-registration/{id}") @Security("keycloak") @Tags("Customer Branch Commercial Registration") async putCommercialRegis( @Request() req: RequestWithUser, @Path() branchId: string, @Path() id: string, ) { await this.checkPermission(req.user, branchId); return req.res?.redirect( await setFile(fileLocation.customerBranch.commercialRegistration(branchId, id)), ); } @Delete("file-commercial-registration/{id}") @Security("keycloak") @Tags("Customer Branch Commercial Registration") async delCommercialRegis( @Request() req: RequestWithUser, @Path() branchId: string, @Path() id: string, ) { await this.checkPermission(req.user, branchId); return await deleteFile(fileLocation.customerBranch.commercialRegistration(branchId, id)); } @Get("file-vat-registration") @Security("keycloak") @Tags("Customer Branch Vat Registration") async listVatRegis(@Request() req: RequestWithUser, @Path() branchId: string) { await this.checkPermission(req.user, branchId); return await listFile(fileLocation.customerBranch.vatRegistration(branchId)); } @Get("file-vat-registration/{id}") @Security("keycloak") @Tags("Customer Branch Vat Registration") async getVatRegis(@Path() branchId: string, @Path() id: string) { return await getFile(fileLocation.customerBranch.vatRegistration(branchId, id)); } @Put("file-vat-registration/{id}") @Security("keycloak") @Tags("Customer Branch Vat Registration") async putVatRegis(@Request() req: RequestWithUser, @Path() branchId: string, @Path() id: string) { await this.checkPermission(req.user, branchId); return req.res?.redirect( await setFile(fileLocation.customerBranch.vatRegistration(branchId, id)), ); } @Delete("file-vat-registration/{id}") @Security("keycloak") @Tags("Customer Branch Vat Registration") async delVatRegis(@Request() req: RequestWithUser, @Path() branchId: string, @Path() id: string) { await this.checkPermission(req.user, branchId); return await deleteFile(fileLocation.customerBranch.vatRegistration(branchId, id)); } }