From 4b08d12845317440d1808e99c4795a85c89360b9 Mon Sep 17 00:00:00 2001 From: Methapon Metanipat Date: Tue, 3 Sep 2024 14:06:02 +0700 Subject: [PATCH] refactor!: remove product type --- .../migration.sql | 41 +++ prisma/schema.prisma | 38 +-- src/controllers/product-service-controller.ts | 13 +- src/controllers/product/group-controller.ts | 27 +- src/controllers/product/product-controller.ts | 54 ++-- src/controllers/product/type-controller.ts | 240 ------------------ src/controllers/service/service-controller.ts | 36 +-- 7 files changed, 105 insertions(+), 344 deletions(-) create mode 100644 prisma/migrations/20240903070519_drop_relation/migration.sql delete mode 100644 src/controllers/product/type-controller.ts diff --git a/prisma/migrations/20240903070519_drop_relation/migration.sql b/prisma/migrations/20240903070519_drop_relation/migration.sql new file mode 100644 index 0000000..97ceef0 --- /dev/null +++ b/prisma/migrations/20240903070519_drop_relation/migration.sql @@ -0,0 +1,41 @@ +/* + Warnings: + + - You are about to drop the column `productTypeId` on the `Product` table. All the data in the column will be lost. + - You are about to drop the column `productTypeId` on the `Service` table. All the data in the column will be lost. + - You are about to drop the `ProductType` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "Product" DROP CONSTRAINT "Product_productTypeId_fkey"; + +-- DropForeignKey +ALTER TABLE "ProductType" DROP CONSTRAINT "ProductType_createdByUserId_fkey"; + +-- DropForeignKey +ALTER TABLE "ProductType" DROP CONSTRAINT "ProductType_productGroupId_fkey"; + +-- DropForeignKey +ALTER TABLE "ProductType" DROP CONSTRAINT "ProductType_updatedByUserId_fkey"; + +-- DropForeignKey +ALTER TABLE "Service" DROP CONSTRAINT "Service_productTypeId_fkey"; + +-- AlterTable +ALTER TABLE "Product" DROP COLUMN "productTypeId", +ADD COLUMN "expenseType" TEXT, +ADD COLUMN "productGroupId" TEXT, +ADD COLUMN "vatIncluded" BOOLEAN; + +-- AlterTable +ALTER TABLE "Service" DROP COLUMN "productTypeId", +ADD COLUMN "productGroupId" TEXT; + +-- DropTable +DROP TABLE "ProductType"; + +-- AddForeignKey +ALTER TABLE "Product" ADD CONSTRAINT "Product_productGroupId_fkey" FOREIGN KEY ("productGroupId") REFERENCES "ProductGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Service" ADD CONSTRAINT "Service_productGroupId_fkey" FOREIGN KEY ("productGroupId") REFERENCES "ProductGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c0e9644..87340c1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -390,8 +390,6 @@ model User { workProductUpdated WorkProduct[] @relation("WorkProductUpdatedByUser") productGroupCreated ProductGroup[] @relation("ProductGroupCreatedByUser") productGroupUpdated ProductGroup[] @relation("ProductGroupUpdatedByUser") - productTypeCreated ProductType[] @relation("ProductTypeCreatedByUser") - productTypeUpdated ProductType[] @relation("ProductTypeUpdatedByUser") productCreated Product[] @relation("ProductCreatedByUser") productUpdated Product[] @relation("ProductUpdatedByUser") quotationCreated Quotation[] @relation("QuotationCreatedByUser") @@ -671,32 +669,8 @@ model ProductGroup { updatedBy User? @relation(name: "ProductGroupUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull) updatedByUserId String? - type ProductType[] -} - -model ProductType { - id String @id @default(cuid()) - - code String - name String - detail String - remark String - - status Status @default(CREATED) - statusOrder Int @default(0) - - createdAt DateTime @default(now()) - createdBy User? @relation(name: "ProductTypeCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull) - createdByUserId String? - updatedAt DateTime @updatedAt - updatedBy User? @relation(name: "ProductTypeUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull) - updatedByUserId String? - - productGroup ProductGroup @relation(fields: [productGroupId], references: [id], onDelete: Cascade) - productGroupId String - - product Product[] service Service[] + product Product[] } model Product { @@ -709,14 +683,16 @@ model Product { price Float agentPrice Float serviceCharge Float + vatIncluded Boolean? + expenseType String? status Status @default(CREATED) statusOrder Int @default(0) remark String? - productType ProductType? @relation(fields: [productTypeId], references: [id], onDelete: SetNull) - productTypeId String? + productGroup ProductGroup? @relation(fields: [productGroupId], references: [id], onDelete: SetNull) + productGroupId String? registeredBranchId String? registeredBranch Branch? @relation(fields: [registeredBranchId], references: [id]) @@ -746,8 +722,8 @@ model Service { work Work[] quotationService QuotationService[] - productType ProductType? @relation(fields: [productTypeId], references: [id], onDelete: SetNull) - productTypeId String? + productGroup ProductGroup? @relation(fields: [productGroupId], references: [id], onDelete: SetNull) + productGroupId String? registeredBranchId String? registeredBranch Branch? @relation(fields: [registeredBranchId], references: [id]) diff --git a/src/controllers/product-service-controller.ts b/src/controllers/product-service-controller.ts index 7a361e1..2b5b421 100644 --- a/src/controllers/product-service-controller.ts +++ b/src/controllers/product-service-controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, Query, Route, Security } from "tsoa"; import prisma from "../db"; import { Prisma, Product, Service } from "@prisma/client"; +import { sql } from "kysely"; @Route("/api/v1/product-service") export class ProductServiceController extends Controller { @@ -9,7 +10,7 @@ export class ProductServiceController extends Controller { async getProductService( @Query() status?: "ACTIVE" | "INACTIVE", @Query() query = "", - @Query() productTypeId?: string, + @Query() productGroupId?: string, @Query() page: number = 1, @Query() pageSize: number = 30, @Query() registeredBranchId?: string, @@ -30,7 +31,7 @@ export class ProductServiceController extends Controller { "remark", "status", "statusOrder", - "productTypeId", + "productGroupId", "registeredBranchId", "createdByUserId", "createdAt", @@ -53,7 +54,7 @@ export class ProductServiceController extends Controller { sql`'-'`.as("remark"), "status", "statusOrder", - "productTypeId", + "productGroupId", "registeredBranchId", "createdByUserId", "createdAt", @@ -67,15 +68,15 @@ export class ProductServiceController extends Controller { .where((eb) => { const condStatus = status ? eb("status", "=", status) : undefined; const condQuery = query ? eb("name", "like", `%${query}%`) : undefined; - const condProductTypeId = productTypeId - ? eb("productTypeId", "=", productTypeId) + const condProductGroupId = productGroupId + ? eb("productGroupId", "=", productGroupId) : undefined; const condRegisteredBranchId = registeredBranchId ? eb("registeredBranchId", "=", registeredBranchId).or("registeredBranchId", "is", null) : undefined; return eb.and( - [condStatus, condQuery, condProductTypeId, condRegisteredBranchId].filter((v) => !!v), + [condStatus, condQuery, condProductGroupId, condRegisteredBranchId].filter((v) => !!v), ); }); diff --git a/src/controllers/product/group-controller.ts b/src/controllers/product/group-controller.ts index 8a453bc..991dacb 100644 --- a/src/controllers/product/group-controller.ts +++ b/src/controllers/product/group-controller.ts @@ -70,7 +70,8 @@ export class ProductGroup extends Controller { include: { _count: { select: { - type: true, + service: true, + product: true, }, }, createdBy: true, @@ -84,30 +85,8 @@ export class ProductGroup extends Controller { prisma.productGroup.count({ where }), ]); - const statsDeep = await prisma.productType.findMany({ - include: { - _count: { select: { product: true, service: true } }, - }, - where: { - productGroupId: { in: result.map((v) => v.id) }, - }, - }); - return { - result: result.map((v) => ({ - ...v, - _count: { - ...v._count, - product: statsDeep.reduce( - (a, c) => (c.productGroupId === v.id ? a + c._count.product : a), - 0, - ), - service: statsDeep.reduce( - (a, c) => (c.productGroupId === v.id ? a + c._count.service : a), - 0, - ), - }, - })), + result, page, pageSize, total, diff --git a/src/controllers/product/product-controller.ts b/src/controllers/product/product-controller.ts index cd309ca..48db17b 100644 --- a/src/controllers/product/product-controller.ts +++ b/src/controllers/product/product-controller.ts @@ -58,7 +58,9 @@ type ProductCreate = { price: number; agentPrice: number; serviceCharge: number; - productTypeId: string; + vatIncluded?: boolean; + expenseType?: number; + productGroupId: string; remark?: string; registeredBranchId?: string; @@ -73,7 +75,9 @@ type ProductUpdate = { agentPrice?: number; serviceCharge?: number; remark?: string; - productTypeId?: string; + vatIncluded?: boolean; + expenseType?: number; + productGroupId?: string; registeredBranchId?: string; }; @@ -92,15 +96,15 @@ function globalAllow(roles?: string[]) { @Tags("Product") export class ProductController extends Controller { @Get("stats") - async getProductStats(@Query() productTypeId?: string) { - return await prisma.product.count({ where: { productTypeId } }); + async getProductStats(@Query() productGroupId?: string) { + return await prisma.product.count({ where: { productGroupId } }); } @Get() @Security("keycloak") async getProduct( @Query() status?: Status, - @Query() productTypeId?: string, + @Query() productGroupId?: string, @Query() query: string = "", @Query() page: number = 1, @Query() pageSize: number = 30, @@ -116,8 +120,8 @@ export class ProductController extends Controller { const where = { OR: [ - { name: { contains: query }, productTypeId, ...filterStatus(status) }, - { detail: { contains: query }, productTypeId, ...filterStatus(status) }, + { name: { contains: query }, productGroupId, ...filterStatus(status) }, + { detail: { contains: query }, productGroupId, ...filterStatus(status) }, ], AND: registeredBranchId ? { @@ -191,13 +195,13 @@ export class ProductController extends Controller { @Post() @Security("keycloak", MANAGE_ROLES) async createProduct(@Request() req: RequestWithUser, @Body() body: ProductCreate) { - const [productType, branch] = await prisma.$transaction([ - prisma.productType.findFirst({ + const [productGroup, branch] = await prisma.$transaction([ + prisma.productGroup.findFirst({ include: { createdBy: true, updatedBy: true, }, - where: { id: body.productTypeId }, + where: { id: body.productGroupId }, }), prisma.branch.findFirst({ include: { user: { where: { userId: req.user.sub } } }, @@ -213,11 +217,11 @@ export class ProductController extends Controller { ); } - if (!productType) { + if (!productGroup) { throw new HttpError( HttpStatus.BAD_REQUEST, - "Product Type cannot be found.", - "relationProductTypeNotFound", + "Product Group cannot be found.", + "relationProductGroupNotFound", ); } @@ -260,13 +264,13 @@ export class ProductController extends Controller { }, ); - if (productType.status === "CREATED") { - await prisma.productType.update({ + if (productGroup.status === "CREATED") { + await prisma.productGroup.update({ include: { createdBy: true, updatedBy: true, }, - where: { id: body.productTypeId }, + where: { id: body.productGroupId }, data: { status: Status.ACTIVE }, }); } @@ -294,7 +298,7 @@ export class ProductController extends Controller { @Body() body: ProductUpdate, @Path() productId: string, ) { - const [product, productType, branch] = await prisma.$transaction([ + const [product, productGroup, branch] = await prisma.$transaction([ prisma.product.findUnique({ include: { registeredBranch: { @@ -305,12 +309,12 @@ export class ProductController extends Controller { }, where: { id: productId }, }), - prisma.productType.findFirst({ + prisma.productGroup.findFirst({ include: { createdBy: true, updatedBy: true, }, - where: { id: body.productTypeId }, + where: { id: body.productGroupId }, }), prisma.branch.findFirst({ where: { id: body.registeredBranchId } }), ]); @@ -327,11 +331,11 @@ export class ProductController extends Controller { ); } - if (!productType) { + if (!productGroup) { throw new HttpError( HttpStatus.BAD_REQUEST, - "Product Type cannot be found.", - "relationProductTypeNotFound", + "Product Group cannot be found.", + "relationProductGroupNotFound", ); } @@ -352,9 +356,9 @@ export class ProductController extends Controller { where: { id: productId }, }); - if (productType.status === "CREATED") { - await prisma.productType.updateMany({ - where: { id: body.productTypeId, status: Status.CREATED }, + if (productGroup.status === "CREATED") { + await prisma.productGroup.updateMany({ + where: { id: body.productGroupId, status: Status.CREATED }, data: { status: Status.ACTIVE }, }); } diff --git a/src/controllers/product/type-controller.ts b/src/controllers/product/type-controller.ts deleted file mode 100644 index b86ca39..0000000 --- a/src/controllers/product/type-controller.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { - Body, - Controller, - Delete, - Get, - Put, - Path, - Post, - Query, - Request, - Route, - Security, - Tags, -} from "tsoa"; -import { Prisma, Status } from "@prisma/client"; - -import prisma from "../../db"; -import { RequestWithUser } from "../../interfaces/user"; -import HttpError from "../../interfaces/http-error"; -import HttpStatus from "../../interfaces/http-status"; - -type ProductTypeCreate = { - productGroupId: string; - name: string; - detail: string; - remark: string; - status?: Status; -}; - -type ProductTypeUpdate = { - productGroupId?: string; - name?: string; - detail?: string; - remark?: string; - status?: "ACTIVE" | "INACTIVE"; -}; - -@Route("api/v1/product-type") -@Tags("Product Type") -export class ProductType extends Controller { - @Get("stats") - async getProductTypeStats() { - return await prisma.productType.count(); - } - - @Get() - @Security("keycloak") - async getProductType( - @Query() query: string = "", - @Query() productGroupId?: string, - @Query() status?: Status, - @Query() page: number = 1, - @Query() pageSize: number = 30, - ) { - const filterStatus = (val?: Status) => { - if (!val) return {}; - - return val !== Status.CREATED && val !== Status.ACTIVE - ? { status: val } - : { OR: [{ status: Status.CREATED }, { status: Status.ACTIVE }] }; - }; - const where = { - AND: { productGroupId }, - OR: [ - { name: { contains: query }, ...filterStatus(status) }, - { detail: { contains: query }, ...filterStatus(status) }, - ], - } satisfies Prisma.ProductTypeWhereInput; - const [result, total] = await prisma.$transaction([ - prisma.productType.findMany({ - include: { - _count: { - select: { - product: true, - service: true, - }, - }, - createdBy: true, - updatedBy: true, - }, - orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }], - where, - take: pageSize, - skip: (page - 1) * pageSize, - }), - prisma.productType.count({ where }), - ]); - - return { result, page, pageSize, total }; - } - - @Get("{typeId}") - @Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"]) - async getProductTypeById(@Path() typeId: string) { - const record = await prisma.productType.findFirst({ - where: { id: typeId }, - }); - - if (!record) - throw new HttpError( - HttpStatus.NOT_FOUND, - "Product type cannot be found.", - "productTypeNotFound", - ); - - return record; - } - - @Post() - @Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"]) - async createProductType(@Request() req: RequestWithUser, @Body() body: ProductTypeCreate) { - const productGroup = await prisma.productGroup.findFirst({ - where: { id: body.productGroupId }, - }); - - if (!productGroup) { - throw new HttpError( - HttpStatus.BAD_REQUEST, - "Product group associated cannot be found.", - "productGroupAssociatedBadReq", - ); - } - - const record = await prisma.$transaction( - async (tx) => { - const last = await tx.runningNo.upsert({ - where: { - key: `PRODTYP_T${productGroup.code}`, - }, - create: { - key: `PRODTYP_T${productGroup.code}`, - value: 1, - }, - update: { value: { increment: 1 } }, - }); - - return await tx.productType.create({ - include: { - createdBy: true, - updatedBy: true, - }, - data: { - ...body, - statusOrder: +(body.status === "INACTIVE"), - code: `T${productGroup.code}${last.value.toString().padStart(2, "0")}`, - createdByUserId: req.user.sub, - updatedByUserId: req.user.sub, - }, - }); - }, - { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }, - ); - - if (productGroup.status === "CREATED") { - await prisma.productGroup.update({ - where: { id: body.productGroupId }, - data: { status: Status.ACTIVE }, - }); - } - - this.setStatus(HttpStatus.CREATED); - - return record; - } - - @Put("{typeId}") - @Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"]) - async editProductType( - @Request() req: RequestWithUser, - @Body() body: ProductTypeUpdate, - @Path() typeId: string, - ) { - const productGroup = await prisma.productGroup.findFirst({ - where: { id: body.productGroupId }, - }); - if (body.productGroupId && !productGroup) { - throw new HttpError( - HttpStatus.BAD_REQUEST, - "Product group cannot be found.", - "productGroupBadReq", - ); - } - - if (!(await prisma.productType.findUnique({ where: { id: typeId } }))) { - throw new HttpError( - HttpStatus.NOT_FOUND, - "Product type cannot be found.", - "productTypeNotFound", - ); - } - - const record = await prisma.productType.update({ - include: { - createdBy: true, - updatedBy: true, - }, - data: { - ...body, - statusOrder: +(body.status === "INACTIVE"), - updatedByUserId: req.user.sub, - }, - where: { id: typeId }, - }); - - if (body.productGroupId && productGroup?.status === "CREATED") { - await prisma.productGroup.updateMany({ - where: { id: body.productGroupId, status: Status.CREATED }, - data: { status: Status.ACTIVE }, - }); - } - - return record; - } - - @Delete("{typeId}") - @Security("keycloak", ["system", "head_of_admin", "admin", "branch_accountant", "accountant"]) - async deleteProductType(@Path() typeId: string) { - const record = await prisma.productType.findFirst({ where: { id: typeId } }); - - if (!record) { - throw new HttpError( - HttpStatus.NOT_FOUND, - "Product type cannot be found.", - "productTypeNotFound", - ); - } - - if (record.status !== Status.CREATED) { - throw new HttpError(HttpStatus.FORBIDDEN, "Product type is in used.", "productTypeInUsed"); - } - - return await prisma.productType.delete({ - include: { - createdBy: true, - updatedBy: true, - }, - where: { id: typeId }, - }); - } -} diff --git a/src/controllers/service/service-controller.ts b/src/controllers/service/service-controller.ts index 49d217e..3818b64 100644 --- a/src/controllers/service/service-controller.ts +++ b/src/controllers/service/service-controller.ts @@ -48,7 +48,7 @@ type ServiceCreate = { productId: string[]; attributes?: { [key: string]: any }; }[]; - productTypeId: string; + productGroupId: string; registeredBranchId?: string; }; @@ -64,7 +64,7 @@ type ServiceUpdate = { productId: string[]; attributes?: { [key: string]: any }; }[]; - productTypeId?: string; + productGroupId?: string; registeredBranchId?: string; }; @@ -92,7 +92,7 @@ export class ServiceController extends Controller { @Query() page: number = 1, @Query() pageSize: number = 30, @Query() status?: Status, - @Query() productTypeId?: string, + @Query() productGroupId?: string, @Query() registeredBranchId?: string, @Query() fullDetail?: boolean, ) { @@ -106,8 +106,8 @@ export class ServiceController extends Controller { const where = { OR: [ - { name: { contains: query }, productTypeId, ...filterStatus(status) }, - { detail: { contains: query }, productTypeId, ...filterStatus(status) }, + { name: { contains: query }, productGroupId, ...filterStatus(status) }, + { detail: { contains: query }, productGroupId, ...filterStatus(status) }, ], AND: registeredBranchId ? { @@ -233,15 +233,15 @@ export class ServiceController extends Controller { @Post() @Security("keycloak", MANAGE_ROLES) async createService(@Request() req: RequestWithUser, @Body() body: ServiceCreate) { - const { work, productTypeId, ...payload } = body; + const { work, productGroupId, ...payload } = body; - const [productType, branch] = await prisma.$transaction([ - prisma.productType.findFirst({ + const [productGroup, branch] = await prisma.$transaction([ + prisma.productGroup.findFirst({ include: { createdBy: true, updatedBy: true, }, - where: { id: body.productTypeId }, + where: { id: body.productGroupId }, }), prisma.branch.findFirst({ include: { user: { where: { userId: req.user.sub } } }, @@ -257,11 +257,11 @@ export class ServiceController extends Controller { ); } - if (!productType) { + if (!productGroup) { throw new HttpError( HttpStatus.BAD_REQUEST, "Product Type cannot be found.", - "relationProductTypeNotFound", + "relationproductGroupNotFound", ); } @@ -301,7 +301,7 @@ export class ServiceController extends Controller { }, data: { ...payload, - productTypeId, + productGroupId, statusOrder: +(body.status === "INACTIVE"), code: `${body.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`, work: { @@ -350,9 +350,9 @@ export class ServiceController extends Controller { if (!(await prisma.service.findUnique({ where: { id: serviceId } }))) { throw new HttpError(HttpStatus.NOT_FOUND, "Service cannot be found.", "serviceNotFound"); } - const { work, productTypeId, ...payload } = body; + const { work, productGroupId, ...payload } = body; - const [service, productType, branch] = await prisma.$transaction([ + const [service, productGroup, branch] = await prisma.$transaction([ prisma.service.findUnique({ include: { registeredBranch: { @@ -363,12 +363,12 @@ export class ServiceController extends Controller { }, where: { id: serviceId }, }), - prisma.productType.findFirst({ + prisma.productGroup.findFirst({ include: { createdBy: true, updatedBy: true, }, - where: { id: body.productTypeId }, + where: { id: body.productGroupId }, }), prisma.branch.findFirst({ where: { id: body.registeredBranchId } }), ]); @@ -385,11 +385,11 @@ export class ServiceController extends Controller { ); } - if (!productType) { + if (!productGroup) { throw new HttpError( HttpStatus.BAD_REQUEST, "Product Type cannot be found.", - "relationProductTypeNotFound", + "relationproductGroupNotFound", ); }