From ba04c54c5bba9fa11d742463e2e163b94f7ad2f9 Mon Sep 17 00:00:00 2001 From: rahat0078 Date: Mon, 13 Apr 2026 00:35:51 +0600 Subject: [PATCH] feat(support): complete full CRUD for support module --- .../migration.sql | 30 ++++ .../migration.sql | 16 ++ prisma/schema/account.schema.prisma | 2 + prisma/schema/support.prisma | 43 ++++++ src/app/modules/support/support.controller.ts | 99 +++++++++++++ src/app/modules/support/support.route.ts | 30 ++++ src/app/modules/support/support.service.ts | 139 ++++++++++++++++++ src/app/modules/support/support.swagger.ts | 114 ++++++++++++++ src/app/modules/support/support.validation.ts | 33 +++++ src/routes.ts | 2 + src/swaggerOptions.ts | 2 + 11 files changed, 510 insertions(+) create mode 100644 prisma/migrations/20260412105751_add_support_schema/migration.sql create mode 100644 prisma/migrations/20260412151425_update_support_model/migration.sql create mode 100644 prisma/schema/support.prisma create mode 100644 src/app/modules/support/support.controller.ts create mode 100644 src/app/modules/support/support.route.ts create mode 100644 src/app/modules/support/support.service.ts create mode 100644 src/app/modules/support/support.swagger.ts create mode 100644 src/app/modules/support/support.validation.ts diff --git a/prisma/migrations/20260412105751_add_support_schema/migration.sql b/prisma/migrations/20260412105751_add_support_schema/migration.sql new file mode 100644 index 0000000..4d3ed33 --- /dev/null +++ b/prisma/migrations/20260412105751_add_support_schema/migration.sql @@ -0,0 +1,30 @@ +-- CreateEnum +CREATE TYPE "T_SupportType" AS ENUM ('TECHNICAL', 'BILLING', 'DOMAIN', 'TEMPLATE', 'PAYMENT', 'ACCOUNT', 'FEATURE_REQUEST', 'BUG', 'OTHER'); + +-- CreateEnum +CREATE TYPE "T_SupportStatus" AS ENUM ('OPEN', 'IN_PROGRESS', 'RESOLVED', 'CLOSED'); + +-- CreateTable +CREATE TABLE "Support" ( + "id" TEXT NOT NULL, + "issueName" TEXT NOT NULL, + "description" TEXT NOT NULL, + "type" "T_SupportType" NOT NULL, + "status" "T_SupportStatus" NOT NULL DEFAULT 'OPEN', + "resolvedBy" TEXT, + "resolvedAt" TIMESTAMP(3), + "storeAccountId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Support_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "Support_storeAccountId_idx" ON "Support"("storeAccountId"); + +-- CreateIndex +CREATE INDEX "Support_storeAccountId_status_idx" ON "Support"("storeAccountId", "status"); + +-- AddForeignKey +ALTER TABLE "Support" ADD CONSTRAINT "Support_storeAccountId_fkey" FOREIGN KEY ("storeAccountId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20260412151425_update_support_model/migration.sql b/prisma/migrations/20260412151425_update_support_model/migration.sql new file mode 100644 index 0000000..a6ab9da --- /dev/null +++ b/prisma/migrations/20260412151425_update_support_model/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - The values [CLOSED] on the enum `T_SupportStatus` will be removed. If these variants are still used in the database, this will fail. + +*/ +-- AlterEnum +BEGIN; +CREATE TYPE "T_SupportStatus_new" AS ENUM ('OPEN', 'IN_PROGRESS', 'RESOLVED', 'REJECTED'); +ALTER TABLE "public"."Support" ALTER COLUMN "status" DROP DEFAULT; +ALTER TABLE "Support" ALTER COLUMN "status" TYPE "T_SupportStatus_new" USING ("status"::text::"T_SupportStatus_new"); +ALTER TYPE "T_SupportStatus" RENAME TO "T_SupportStatus_old"; +ALTER TYPE "T_SupportStatus_new" RENAME TO "T_SupportStatus"; +DROP TYPE "public"."T_SupportStatus_old"; +ALTER TABLE "Support" ALTER COLUMN "status" SET DEFAULT 'OPEN'; +COMMIT; diff --git a/prisma/schema/account.schema.prisma b/prisma/schema/account.schema.prisma index fbd9f48..959bd65 100644 --- a/prisma/schema/account.schema.prisma +++ b/prisma/schema/account.schema.prisma @@ -18,5 +18,7 @@ model Account { createdAt DateTime @default(now()) updatedAt DateTime @default(now()) + supports Support[] + profile Profile? } diff --git a/prisma/schema/support.prisma b/prisma/schema/support.prisma new file mode 100644 index 0000000..012e04d --- /dev/null +++ b/prisma/schema/support.prisma @@ -0,0 +1,43 @@ +enum T_SupportType { + TECHNICAL + BILLING + DOMAIN + TEMPLATE + PAYMENT + ACCOUNT + FEATURE_REQUEST + BUG + OTHER +} + +enum T_SupportStatus { + OPEN + IN_PROGRESS + RESOLVED + REJECTED +} + + + +model Support { + id String @id @default(uuid()) + + issueName String + description String + + type T_SupportType + status T_SupportStatus @default(OPEN) + + resolvedBy String? + resolvedAt DateTime? + + storeAccountId String + + storeAccount Account @relation(fields: [storeAccountId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([storeAccountId]) + @@index([storeAccountId, status]) +} \ No newline at end of file diff --git a/src/app/modules/support/support.controller.ts b/src/app/modules/support/support.controller.ts new file mode 100644 index 0000000..25faf3f --- /dev/null +++ b/src/app/modules/support/support.controller.ts @@ -0,0 +1,99 @@ + +import { Request, Response } from "express"; +import catchAsync from "../../utils/catch_async"; +import manageResponse from "../../utils/manage_response"; +import { support_service } from "./support.service"; + + + +const createSupport = catchAsync(async (req: Request, res: Response) => { + const id = req?.user?.accountId; + const data = { + ...req.body, + storeAccountId: id as string + } + + const result = await support_service.createSupportIntoDB(data); + manageResponse(res, { + success: true, + statusCode: 200, + message: "support created successfully.", + data: result, + meta: {}, + }); +}); + + +const getAllSupport = catchAsync(async (req: Request, res: Response) => { + const role = req?.user?.role; + const id = req?.user?.accountId; + const search = req?.query?.search; + const type = req?.query?.type; + const status = req?.query?.status; + + const result = await support_service.getAllSupportFromDB(id as string, role as string, search as string, type as string, status as string); + manageResponse(res, { + success: true, + statusCode: 200, + message: "All support fetched successfully.", + data: result, + meta: {}, + }); +}); + +const get_single_support = catchAsync(async (req, res) => { + const {id} = req.params; + const userId = req?.user?.accountId; + const role = req?.user?.role + + + const result = await support_service.getSingleSupportFromDB(id as string, userId as string, role as string); + manageResponse(res, { + success: true, + statusCode: 200, + message: "Single support fetched successfully.", + data: result, + meta: {}, + }); +}); + + +const update_support = catchAsync(async (req, res) => { + const {id} = req.params; + const userId = req?.user?.accountId; + const role = req?.user?.role; + const data = req.body + + + const result = await support_service.updateSupportIntoDB(id as string, userId as string, role as string, data); + manageResponse(res, { + success: true, + statusCode: 200, + message: "support updated successfully.", + data: result, + meta: {}, + }); +}); + +const delete_support = catchAsync(async (req, res) => { + const {id} = req.params; + const userId = req?.user?.accountId; + const role = req?.user?.role + + const result = await support_service.deleteSupportFromDB(id as string, userId as string, role as string); + manageResponse(res, { + success: true, + statusCode: 200, + message: "support deleted successfully.", + data: result, + meta: {}, + }); +}); + +export const support_controller = { + createSupport, + getAllSupport, + get_single_support, + update_support, + delete_support, +}; diff --git a/src/app/modules/support/support.route.ts b/src/app/modules/support/support.route.ts new file mode 100644 index 0000000..bf447e1 --- /dev/null +++ b/src/app/modules/support/support.route.ts @@ -0,0 +1,30 @@ + +import { Router } from "express"; +import RequestValidator from "../../middlewares/request_validator"; +import { support_controller } from "./support.controller"; +import { support_validations } from "./support.validation"; +import auth from "../../middlewares/auth"; + +const router = Router(); + +router.get("/", auth("ADMIN", "USER"), support_controller.getAllSupport); + +router.post( + "/", + auth("ADMIN", "USER"), + RequestValidator(support_validations.create_support), + support_controller.createSupport, +); + +router.get("/:id", auth("ADMIN", "USER"), support_controller.get_single_support); +router.patch( + "/:id", + auth("ADMIN", "USER"), + RequestValidator(support_validations.update_support), + support_controller.update_support, +); + +router.delete("/:id", auth("ADMIN", "USER"), support_controller.delete_support); + +export default router; + \ No newline at end of file diff --git a/src/app/modules/support/support.service.ts b/src/app/modules/support/support.service.ts new file mode 100644 index 0000000..c970fbe --- /dev/null +++ b/src/app/modules/support/support.service.ts @@ -0,0 +1,139 @@ +import { prisma } from "../../lib/prisma"; +import { Prisma } from "../../../../prisma/generated/prisma/client"; +import { AppError } from "../../utils/app_error"; + +const createSupportIntoDB = async (payload: any) => { + const result = await prisma.support.create({ data: payload }); + return result; +}; + +const getAllSupportFromDB = async ( + user_id: string, + role: string, + search?: string, + type?: string, + status?: string, +) => { + const andCondition: Prisma.SupportWhereInput[] = []; + + if (search) { + andCondition.push({ + OR: [ + { + issueName: { + contains: search, + mode: "insensitive", + }, + }, + { + description: { + contains: search, + mode: "insensitive", + }, + }, + ], + }); + } + if (type) { + andCondition.push({ + type: type as any, + }); + } + + if (status) { + andCondition.push({ + status: status as any, + }); + } + + if (role !== "ADMIN") { + andCondition.push({ + storeAccountId: user_id, + }); + } + + const whereCondition: Prisma.SupportWhereInput = + andCondition.length > 0 ? { AND: andCondition } : {}; + + const result = await prisma.support.findMany({ + where: whereCondition, + orderBy: { + createdAt: "desc", + }, + }); + + return result; +}; + +const getSingleSupportFromDB = async ( + id: string, + userId: string, + role: string, +) => { + const support = await prisma.support.findUnique({ + where: { id }, + }); + if (!support) { + throw new AppError("Support not found", 404); + } + + if (role !== "ADMIN" && support.storeAccountId !== userId) { + throw new AppError("You are not authorized", 403); + } + return support; +}; + +const updateSupportIntoDB = async ( + id: string, + userId: string, + role: string, + payload: any, +) => { + const support = await prisma.support.findUnique({ + where: { id }, + }); + + if (!support) { + throw new AppError("Support not found", 404); + } + + if (role !== "ADMIN" && support.storeAccountId !== userId) { + throw new AppError("You are not authorized", 403); + } + + const result = await prisma.support.update({ + where: { id }, + data: payload, + }); + + return result; +}; + +const deleteSupportFromDB = async ( + id: string, + userId: string, + role: string, +) => { + const support = await prisma.support.findUnique({ + where: { id }, + }); + if (!support) { + throw new AppError("Support not found", 404); + } + if (role !== "ADMIN" && support.storeAccountId !== userId) { + throw new AppError("You are not authorized", 403); + } + + const result = await prisma.support.delete({ + where: {id} + }) + return result; +}; + +export const support_service = { + createSupportIntoDB, + getAllSupportFromDB, + getSingleSupportFromDB, + updateSupportIntoDB, + deleteSupportFromDB, +}; diff --git a/src/app/modules/support/support.swagger.ts b/src/app/modules/support/support.swagger.ts new file mode 100644 index 0000000..a4fe40d --- /dev/null +++ b/src/app/modules/support/support.swagger.ts @@ -0,0 +1,114 @@ + + export const supportSwaggerDocs = { + "/api/support": { + post: { + tags: ["support"], + summary: "Create new support", + description: "", + requestBody: { + required: true, + content: { + "application/json": { + example: JSON.stringify({ + "issueName": "Your issue name", + "description": "Issue description", + "type": "Issue Type" + }), // put your request body + }, + }, + }, + responses: { + 201: { description: "support created successfully" }, + 500: { description: "Validation error or internal server error" }, + }, + }, + get: { + tags: ["support"], + summary: "Get all support", + description: "", + parameters: [ + { + name: "page", + in: "query", + required: false, + schema: { type: "number" }, + }, + { + name: "limit", + in: "query", + required: false, + schema: { type: "number" }, + }, + ], + responses: { + 200: { description: "support fetched successfully" }, + 401: { description: "unauthorized" }, + }, + }, + }, + + "/api/support/{id}": { + get: { + tags: ["support"], + summary: "Get single support", + description: "", + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string" }, + }, + ], + responses: { + 200: { description: "support fetched successfully" }, + 401: { description: "unauthorized" }, + }, + }, + patch: { + tags: ["support"], + summary: "Update support", + description: "", + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + required: true, + content: { + "application/json": { + example: JSON.stringify({}), // put your request body + }, + }, + }, + responses: { + 200: { description: "support updated successfully" }, + 500: { description: "Validation error or internal server error" }, + }, + }, + delete: { + tags: ["support"], + summary: "Delete support", + description: "", + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string" }, + }, + ], + responses: { + 200: { description: "support delete successfully" }, + 401: { description: "unauthorized" }, + }, + }, + }, +}; + + + \ No newline at end of file diff --git a/src/app/modules/support/support.validation.ts b/src/app/modules/support/support.validation.ts new file mode 100644 index 0000000..80dc5da --- /dev/null +++ b/src/app/modules/support/support.validation.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; + +const create_support = z.object({ + issueName: z.string().min(1, "issueName is required"), + description: z.string().min(1, "description is required"), + type: z.enum([ + "TECHNICAL", + "BILLING", + "DOMAIN", + "TEMPLATE", + "PAYMENT", + "ACCOUNT", + "FEATURE_REQUEST", + "BUG", + "OTHER", + ]), +}); + +const update_support = z.object({ + issueName: z.string().optional(), + description: z.string().optional(), + type: z.enum(["BUG", "PAYMENT", "ACCOUNT", "OTHER"]).optional(), + + status: z.enum(["OPEN", "IN_PROGRESS", "RESOLVED", "REJECTED"]).optional(), + + resolvedBy: z.string().optional(), + resolvedAt: z.coerce.date().optional(), +}); + +export const support_validations = { + create_support, + update_support, +}; diff --git a/src/routes.ts b/src/routes.ts index c7b2768..af2c705 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -2,10 +2,12 @@ import { Router } from "express"; import accountRouter from "./app/modules/account/account.route"; import profileRoute from "./app/modules/profile/profile.route"; import planRoute from "./app/modules/plan/plan.route"; +import supportRoute from "./app/modules/support/support.route"; const appRouter = Router(); const moduleRoutes = [ + { path: "/support", route: supportRoute }, { path: "/plan", route: planRoute }, { path: "/profile", route: profileRoute },{ path: "/auth", route: accountRouter }]; diff --git a/src/swaggerOptions.ts b/src/swaggerOptions.ts index a787bda..6084f85 100644 --- a/src/swaggerOptions.ts +++ b/src/swaggerOptions.ts @@ -4,6 +4,7 @@ import { configs } from "./app/configs"; import { accountSwaggerDocs } from "./app/modules/account/account.swagger"; import { profileSwaggerDocs } from "./app/modules/profile/profile.swagger"; import { planSwaggerDocs } from "./app/modules/plan/plan.swagger"; +import { supportSwaggerDocs } from "./app/modules/support/support.swagger"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -20,6 +21,7 @@ export const swaggerOptions = { ...accountSwaggerDocs, ...profileSwaggerDocs, ...planSwaggerDocs, + ...supportSwaggerDocs, }, servers: configs.env === "production"