import { Prisma, Status } from "@prisma/client"; import { Body, Controller, Delete, Get, Head, OperationId, Path, Post, Put, Query, Request, Route, Security, Tags, } from "tsoa"; import prisma from "../db"; import { isUsedError, notFoundError } from "../utils/error"; import { queryOrNot, whereDateQuery } from "../utils/relation"; import { RequestWithUser } from "../interfaces/user"; import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; import { filterStatus } from "../services/prisma"; type InstitutionPayload = { name: string; nameEN: string; code: string; addressEN: string; address: string; soi?: string | null; soiEN?: string | null; moo?: string | null; mooEN?: string | null; street?: string | null; streetEN?: string | null; subDistrictId: string; districtId: string; provinceId: string; selectedImage?: string | null; contactName?: string; contactEmail?: string; contactTel?: string; bank?: { bankName: string; bankBranch: string; accountName: string; accountNumber: string; accountType: string; currentlyUse: boolean; }[]; }; type InstitutionUpdatePayload = { name: string; nameEN: string; code: string; addressEN: string; address: string; soi?: string | null; soiEN?: string | null; moo?: string | null; mooEN?: string | null; street?: string | null; streetEN?: string | null; subDistrictId: string; districtId: string; provinceId: string; selectedImage?: string | null; contactName?: string; contactEmail?: string; contactTel?: string; bank?: { id?: string; bankName: string; bankBranch: string; accountName: string; accountNumber: string; accountType: string; currentlyUse: boolean; }[]; }; @Route("api/v1/institution") @Tags("Institution") export class InstitutionController extends Controller { @Get() @Security("keycloak") @OperationId("getInstitutionList") async getInstitutionList( @Query() query: string = "", @Query() page: number = 1, @Query() pageSize: number = 30, @Query() status?: Status, @Query() activeOnly?: boolean, @Query() group?: string, @Query() startDate?: Date, @Query() endDate?: Date, ) { return this.getInstitutionListByCriteria( query, page, pageSize, status, activeOnly, group, startDate, endDate, ); } @Post("list") @Security("keycloak") @OperationId("getInstitutionListByCriteria") async getInstitutionListByCriteria( @Query() query: string = "", @Query() page: number = 1, @Query() pageSize: number = 30, @Query() status?: Status, @Query() activeOnly?: boolean, @Query() group?: string, @Query() startDate?: Date, @Query() endDate?: Date, @Body() body?: { group?: string[]; }, ) { const where = { ...filterStatus(activeOnly ? Status.ACTIVE : status), group: body?.group ? { in: body.group } : group, OR: queryOrNot(query, [ { name: { contains: query, mode: "insensitive" } }, { code: { contains: query, mode: "insensitive" } }, ]), ...whereDateQuery(startDate, endDate), } satisfies Prisma.InstitutionWhereInput; const [result, total] = await prisma.$transaction([ prisma.institution.findMany({ where, include: { province: true, district: true, subDistrict: true, bank: true, }, orderBy: [{ statusOrder: "asc" }, { code: "asc" }], take: pageSize, skip: (page - 1) * pageSize, }), prisma.institution.count({ where }), ]); return { result, page, pageSize, total }; } @Get("{institutionId}") @Security("keycloak") @OperationId("getInstitution") async getInstitution(@Path() institutionId: string, @Query() group?: string) { return await prisma.institution.findFirst({ include: { province: true, district: true, subDistrict: true, bank: true, }, where: { id: institutionId, group }, }); } @Post() @Security("keycloak") @OperationId("createInstitution") async createInstitution( @Body() body: InstitutionPayload & { status?: Status; }, @Request() req: RequestWithUser, ) { return await prisma.$transaction(async (tx) => { const last = await tx.runningNo.upsert({ where: { key: `INST_${body.code}`, }, create: { key: `INST_${body.code}`, value: 1, }, update: { value: { increment: 1 } }, }); return await tx.institution.create({ include: { bank: true, createdBy: true, updatedBy: true, }, data: { ...body, code: `${body.code}${last.value.toString().padStart(5, "0")}`, group: body.code, bank: { createMany: { data: body.bank ?? [], }, }, createdByUserId: req.user.sub, updatedByUserId: req.user.sub, }, }); }); } @Put("{institutionId}") @Security("keycloak") @OperationId("updateInstitution") async updateInstitution( @Path() institutionId: string, @Body() body: InstitutionUpdatePayload & { status?: "ACTIVE" | "INACTIVE"; }, ) { const { bank } = body; return await prisma.$transaction(async (tx) => { const listDeleted = bank ? await tx.institutionBank.findMany({ where: { id: { not: { in: bank.flatMap((v) => (!!v.id ? v.id : [])) } }, institutionId, }, }) : []; await Promise.all( listDeleted.map((v) => deleteFile(fileLocation.institution.bank(v.institutionId, v.id))), ); return await prisma.institution.update({ include: { bank: true, }, where: { id: institutionId }, data: { ...body, statusOrder: +(body.status === "INACTIVE"), bank: bank ? { deleteMany: listDeleted.length > 0 ? { id: { in: listDeleted.map((v) => v.id) } } : undefined, upsert: bank.map((v) => ({ where: { id: v.id || "" }, create: { ...v, id: undefined }, update: v, })), } : undefined, }, }); }); } @Delete("{institutionId}") @Security("keycloak") @OperationId("deleteInstitution") async deleteInstitution(@Path() institutionId: string) { return await prisma.$transaction(async (tx) => { const record = await tx.institution.findFirst({ where: { id: institutionId }, include: { taskOrder: { take: 1, }, }, }); if (!record) throw notFoundError("Institution"); if (record.status !== "CREATED" || record.taskOrder.length > 0) { throw isUsedError("Institution"); } const data = await tx.institution.delete({ include: { bank: true, }, where: { id: institutionId }, }); await Promise.all([ ...data.bank.map((v) => deleteFile(fileLocation.institution.bank(institutionId, v.id))), ]); return data; }); } } @Route("api/v1/institution/{institutionId}") @Tags("Institution") export class InstitutionFileController extends Controller { private async checkPermission(_user: RequestWithUser["user"], id: string) { const data = await prisma.institution.findUnique({ where: { id }, }); if (!data) throw notFoundError("Institution"); } @Get("image") @Security("keycloak") async listImage(@Request() req: RequestWithUser, @Path() institutionId: string) { await this.checkPermission(req.user, institutionId); return await listFile(fileLocation.institution.img(institutionId)); } @Get("image/{name}") async getImage( @Request() req: RequestWithUser, @Path() institutionId: string, @Path() name: string, ) { return req.res?.redirect(await getFile(fileLocation.institution.img(institutionId, name))); } @Head("image/{name}") async headImage( @Request() req: RequestWithUser, @Path() institutionId: string, @Path() name: string, ) { return req.res?.redirect( await getPresigned("head", fileLocation.institution.img(institutionId, name)), ); } @Put("image/{name}") @Security("keycloak") async putImage( @Request() req: RequestWithUser, @Path() institutionId: string, @Path() name: string, ) { if (!req.headers["content-type"]?.startsWith("image/")) { throw new HttpError(HttpStatus.BAD_REQUEST, "Not a valid image.", "notValidImage"); } await this.checkPermission(req.user, institutionId); return req.res?.redirect(await setFile(fileLocation.institution.img(institutionId, name))); } @Delete("image/{name}") @Security("keycloak") async delImage( @Request() req: RequestWithUser, @Path() institutionId: string, @Path() name: string, ) { await this.checkPermission(req.user, institutionId); return await deleteFile(fileLocation.institution.img(institutionId, name)); } @Get("attachment") @Security("keycloak") async listAttachment(@Request() req: RequestWithUser, @Path() institutionId: string) { await this.checkPermission(req.user, institutionId); return await listFile(fileLocation.institution.attachment(institutionId)); } @Get("attachment/{name}") @Security("keycloak") async getAttachment(@Path() institutionId: string, @Path() name: string) { return await getFile(fileLocation.institution.attachment(institutionId, name)); } @Head("attachment/{name}") @Security("keycloak") async headAttachment(@Path() institutionId: string, @Path() name: string) { return await getPresigned("head", fileLocation.institution.attachment(institutionId, name)); } @Put("attachment/{name}") @Security("keycloak") async putAttachment( @Request() req: RequestWithUser, @Path() institutionId: string, @Path() name: string, ) { await this.checkPermission(req.user, institutionId); return await setFile(fileLocation.institution.attachment(institutionId, name)); } @Delete("attachment/{name}") @Security("keycloak") async delAttachment( @Request() req: RequestWithUser, @Path() institutionId: string, @Path() name: string, ) { await this.checkPermission(req.user, institutionId); return await deleteFile(fileLocation.institution.attachment(institutionId, name)); } @Get("bank-qr/{bankId}") async getBankImage( @Request() req: RequestWithUser, @Path() institutionId: string, @Path() bankId: string, ) { return req.res?.redirect(await getFile(fileLocation.institution.bank(institutionId, bankId))); } @Head("bank-qr/{bankId}") async headBankImage( @Request() req: RequestWithUser, @Path() institutionId: string, @Path() bankId: string, ) { return req.res?.redirect( await getPresigned("head", fileLocation.institution.bank(institutionId, bankId)), ); } @Put("bank-qr/{bankId}") @Security("keycloak") async putBankImage( @Request() req: RequestWithUser, @Path() institutionId: string, @Path() bankId: string, ) { if (!req.headers["content-type"]?.startsWith("image/")) { throw new HttpError(HttpStatus.BAD_REQUEST, "Not a valid image.", "notValidImage"); } await this.checkPermission(req.user, institutionId); return req.res?.redirect(await setFile(fileLocation.institution.bank(institutionId, bankId))); } @Delete("bank-qr/{bankId}") @Security("keycloak") async delBankImage( @Request() req: RequestWithUser, @Path() institutionId: string, @Path() bankId: string, ) { await this.checkPermission(req.user, institutionId); return await deleteFile(fileLocation.institution.bank(institutionId, bankId)); } }