From ee5eb0f0f5389700e27e7c3ac0a996c0dadb23d4 Mon Sep 17 00:00:00 2001 From: Md Sharafat Hassain Date: Sun, 12 Apr 2026 22:47:56 +0600 Subject: [PATCH] Order api: create order schema --- .../20260411142857_order_table/migration.sql | 27 ++++ .../20260411143108_fix_relation/migration.sql | 5 + prisma/schema/account.schema.prisma | 4 +- prisma/schema/order.prisma | 29 ++++ prisma/schema/profile.schema.prisma | 1 + src/app/{ => errors}/configs/index.ts | 0 src/app/middlewares/auth.ts | 2 +- src/app/middlewares/global_error_handler.ts | 2 +- src/app/modules/account/account.controller.ts | 2 +- src/app/modules/account/account.service.ts | 4 +- src/app/modules/order/order.controller.ts | 68 +++++++++ src/app/modules/order/order.route.ts | 24 ++++ src/app/modules/order/order.service.ts | 86 ++++++++++++ src/app/modules/order/order.swagger.ts | 132 ++++++++++++++++++ src/app/modules/order/order.validation.ts | 32 +++++ src/app/utils/cloudinary.ts | 2 +- src/app/utils/mail_sender.ts | 42 +++--- src/routes.ts | 2 + src/server.ts | 2 +- src/swaggerOptions.ts | 10 +- 20 files changed, 443 insertions(+), 33 deletions(-) create mode 100644 prisma/migrations/20260411142857_order_table/migration.sql create mode 100644 prisma/migrations/20260411143108_fix_relation/migration.sql create mode 100644 prisma/schema/order.prisma rename src/app/{ => errors}/configs/index.ts (100%) create mode 100644 src/app/modules/order/order.controller.ts create mode 100644 src/app/modules/order/order.route.ts create mode 100644 src/app/modules/order/order.service.ts create mode 100644 src/app/modules/order/order.swagger.ts create mode 100644 src/app/modules/order/order.validation.ts diff --git a/prisma/migrations/20260411142857_order_table/migration.sql b/prisma/migrations/20260411142857_order_table/migration.sql new file mode 100644 index 0000000..f86490a --- /dev/null +++ b/prisma/migrations/20260411142857_order_table/migration.sql @@ -0,0 +1,27 @@ +-- CreateEnum +CREATE TYPE "STATUS" AS ENUM ('INITIATED', 'CONFIRMED', 'ONGOING', 'DELIVERED', 'CANCELLED'); + +-- CreateEnum +CREATE TYPE "PAYMENT_TYPE" AS ENUM ('COD'); + +-- CreateTable +CREATE TABLE "Order" ( + "id" TEXT NOT NULL, + "shopAccountId" TEXT NOT NULL, + "productPrice" INTEGER NOT NULL, + "productQuantity" INTEGER NOT NULL, + "productName" TEXT NOT NULL, + "status" "STATUS" NOT NULL, + "customerName" TEXT NOT NULL, + "customerPhone" TEXT NOT NULL, + "customerAddress" TEXT NOT NULL, + "customerNote" TEXT NOT NULL, + "paymentType" "PAYMENT_TYPE" NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Order_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Order" ADD CONSTRAINT "Order_shopAccountId_fkey" FOREIGN KEY ("shopAccountId") REFERENCES "Profile"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260411143108_fix_relation/migration.sql b/prisma/migrations/20260411143108_fix_relation/migration.sql new file mode 100644 index 0000000..c15a0d0 --- /dev/null +++ b/prisma/migrations/20260411143108_fix_relation/migration.sql @@ -0,0 +1,5 @@ +-- DropForeignKey +ALTER TABLE "Order" DROP CONSTRAINT "Order_shopAccountId_fkey"; + +-- AddForeignKey +ALTER TABLE "Order" ADD CONSTRAINT "Order_shopAccountId_fkey" FOREIGN KEY ("shopAccountId") REFERENCES "Account"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema/account.schema.prisma b/prisma/schema/account.schema.prisma index fbd9f48..3b411f5 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()) - profile Profile? + profile Profile? //one-to-one + + orders Order[] //one-to-many } diff --git a/prisma/schema/order.prisma b/prisma/schema/order.prisma new file mode 100644 index 0000000..db25e7a --- /dev/null +++ b/prisma/schema/order.prisma @@ -0,0 +1,29 @@ +enum STATUS { + INITIATED + CONFIRMED + ONGOING + DELIVERED + CANCELLED +} + +enum PAYMENT_TYPE { + COD +} + +model Order { + id String @id @default(uuid()) + shopAccountId String + account Account @relation(fields: [shopAccountId], references: [id], onDelete: Cascade) + productPrice Int + productQuantity Int + productName String + status STATUS + customerName String + customerPhone String + customerAddress String + customerNote String + paymentType PAYMENT_TYPE + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/prisma/schema/profile.schema.prisma b/prisma/schema/profile.schema.prisma index ffca12e..c90aef7 100644 --- a/prisma/schema/profile.schema.prisma +++ b/prisma/schema/profile.schema.prisma @@ -9,4 +9,5 @@ model Profile { shopAddress String? shopMapLocation String? shopCategory String? + } diff --git a/src/app/configs/index.ts b/src/app/errors/configs/index.ts similarity index 100% rename from src/app/configs/index.ts rename to src/app/errors/configs/index.ts diff --git a/src/app/middlewares/auth.ts b/src/app/middlewares/auth.ts index 5723cf3..408421e 100644 --- a/src/app/middlewares/auth.ts +++ b/src/app/middlewares/auth.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from "express"; -import { configs } from "../configs"; +import { configs } from "../errors/configs"; import { AppError } from "../utils/app_error"; import { jwtHelpers, JwtPayloadType } from "../utils/JWT"; diff --git a/src/app/middlewares/global_error_handler.ts b/src/app/middlewares/global_error_handler.ts index c361f7f..7c1d72f 100644 --- a/src/app/middlewares/global_error_handler.ts +++ b/src/app/middlewares/global_error_handler.ts @@ -1,6 +1,6 @@ import { ErrorRequestHandler } from "express"; import { ZodError } from "zod"; -import { configs } from "../configs"; +import { configs } from "../errors/configs"; import handleZodError from "../errors/zodError"; import { TErrorSources } from "../types/error"; import { AppError } from "../utils/app_error"; diff --git a/src/app/modules/account/account.controller.ts b/src/app/modules/account/account.controller.ts index f0a467d..4904117 100644 --- a/src/app/modules/account/account.controller.ts +++ b/src/app/modules/account/account.controller.ts @@ -1,4 +1,4 @@ -import { configs } from "../../configs"; +import { configs } from "../../errors/configs"; import catchAsync from "../../utils/catch_async"; import manageResponse from "../../utils/manage_response"; import { account_services } from "./account.service"; diff --git a/src/app/modules/account/account.service.ts b/src/app/modules/account/account.service.ts index 1ecb290..ae01b8d 100644 --- a/src/app/modules/account/account.service.ts +++ b/src/app/modules/account/account.service.ts @@ -1,6 +1,6 @@ import bcrypt from "bcrypt"; import { Request } from "express"; -import { configs } from "../../configs"; +import { configs } from "../../errors/configs"; import { prisma } from "../../lib/prisma"; import { emailQueue } from "../../queues/email/email.queue"; import { AppError } from "../../utils/app_error"; @@ -17,7 +17,7 @@ const create_account_into_db = async (req: Request) => { if (existingAccount) { throw new AppError("Email already exists", 403); - } + } // hash password const hashPassword = bcrypt.hashSync(payload.password, 10); diff --git a/src/app/modules/order/order.controller.ts b/src/app/modules/order/order.controller.ts new file mode 100644 index 0000000..01d2222 --- /dev/null +++ b/src/app/modules/order/order.controller.ts @@ -0,0 +1,68 @@ + +import catchAsync from "../../utils/catch_async"; +import manageResponse from "../../utils/manage_response"; +import { order_service } from "./order.service"; + +const get_all_order = catchAsync(async (req, res) => { + const result = await order_service.get_all_order_from_db(req); + manageResponse(res, { + success: true, + statusCode: 200, + message: "All order fetched successfully.", + data: result, + meta: {}, + }); +}); + +const get_single_order = catchAsync(async (req, res) => { + const result = await order_service.get_single_order_from_db(req); + manageResponse(res, { + success: true, + statusCode: 200, + message: "Single order fetched successfully.", + data: result, + meta: {}, + }); +}); + +const create_order = catchAsync(async (req, res) => { + const result = await order_service.create_order_into_db(req); + manageResponse(res, { + success: true, + statusCode: 200, + message: "order created successfully.", + data: result, + meta: {}, + }); +}); + +const update_order = catchAsync(async (req, res) => { + const result = await order_service.update_order_into_db(req); + manageResponse(res, { + success: true, + statusCode: 200, + message: "order updated successfully.", + data: result, + meta: {}, + }); +}); + +const delete_order = catchAsync(async (req, res) => { + const result = await order_service.delete_order_from_db(req); + manageResponse(res, { + success: true, + statusCode: 200, + message: "order deleted successfully.", + data: result, + meta: {}, + }); +}); + +export const order_controller = { + get_all_order, + get_single_order, + create_order, + update_order, + delete_order, +}; + \ No newline at end of file diff --git a/src/app/modules/order/order.route.ts b/src/app/modules/order/order.route.ts new file mode 100644 index 0000000..d0d162e --- /dev/null +++ b/src/app/modules/order/order.route.ts @@ -0,0 +1,24 @@ + + import { Router } from "express"; +import RequestValidator from "../../middlewares/request_validator"; +import { order_controller } from "./order.controller"; +import { order_validations } from "./order.validation"; + +const router = Router(); + +router.get("/", order_controller.get_all_order); +router.post( + "/", + RequestValidator(order_validations.create_order), + order_controller.create_order, +); +router.get("/:id", order_controller.get_single_order); +router.patch( + "/:id", + RequestValidator(order_validations.update_order), + order_controller.update_order, +); +router.delete("/:id", order_controller.delete_order); + +export default router; + \ No newline at end of file diff --git a/src/app/modules/order/order.service.ts b/src/app/modules/order/order.service.ts new file mode 100644 index 0000000..7b54a28 --- /dev/null +++ b/src/app/modules/order/order.service.ts @@ -0,0 +1,86 @@ + +import { Request } from "express"; +import { prisma } from "../../lib/prisma"; +import { AppError } from "../../utils/app_error"; + +const get_all_order_from_db = async (req: Request) => { + // define your own login here + const result = await prisma.order.findMany(); + return result; +}; + +const get_single_order_from_db = async (req: Request) => { + // define your own login here + const { id } = req.params; + const result = await prisma.order.findUnique({ where: { id } }); + return result; +}; + +const create_order_into_db = async (req: Request) => { + // define your own login here + + const user = req.user + console.log(user) + const { + shopAccountId, + productPrice, + productQuantity, + productName, + customerName, + customerPhone, + customerAddress, + customerNote, + paymentType, + status, + } = req.body + const isUserExists = await prisma.account.findFirst({ + where: { + id: user?.accountId + } + }) + if (!isUserExists) { + throw new AppError("Account not found", 404); + } + + const result = await prisma.order.create({ + data: { + productPrice, + productQuantity, + productName, + customerName, + customerPhone, + customerAddress, + customerNote, + paymentType, + status, + account: { + connect: { + id: user?.accountId, + }, + }, + } + }); + return result; +}; + +const update_order_into_db = async (req: Request) => { + // define your own login here + const { id } = req.params; + const result = await prisma.order.update({ where: { id }, data: req.body }); + return result; +}; + +const delete_order_from_db = async (req: Request) => { + // define your own login here + const { id } = req.params; + const result = await prisma.order.delete({ where: { id } }); + return result; +}; + +export const order_service = { + get_all_order_from_db, + get_single_order_from_db, + create_order_into_db, + update_order_into_db, + delete_order_from_db, +}; diff --git a/src/app/modules/order/order.swagger.ts b/src/app/modules/order/order.swagger.ts new file mode 100644 index 0000000..04368b7 --- /dev/null +++ b/src/app/modules/order/order.swagger.ts @@ -0,0 +1,132 @@ + +export const orderSwaggerDocs = { + "/api/order": { + post: { + tags: ["order"], + summary: "Create new order", + description: "", + requestBody: { + required: true, + content: { + "application/json": { + example: JSON.stringify({ + + "productPrice": 1500, + "productQuantity": 2, + "productName": "Wireless Mouse", + "customerName": "Rahim Uddin", + "customerPhone": "+8801712345678", + "customerAddress": "Rangpur, Bangladesh", + "customerNote": "Please deliver between 3-5 PM", + "paymentType": "Cash on Delivery", + "status": "Pending" + + }), // put your request body + }, + }, + }, + responses: { + 201: { description: "order created successfully" }, + 500: { description: "Validation error or internal server error" }, + }, + }, + get: { + tags: ["order"], + summary: "Get all order", + description: "", + parameters: [ + { + name: "page", + in: "query", + required: false, + schema: { type: "number" }, + }, + { + name: "limit", + in: "query", + required: false, + schema: { type: "number" }, + }, + ], + responses: { + 200: { description: "order fetched successfully" }, + 401: { description: "unauthorized" }, + }, + }, + }, + + "/api/order/{id}": { + get: { + tags: ["order"], + summary: "Get single order", + description: "", + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string" }, + }, + ], + responses: { + 200: { description: "order fetched successfully" }, + 401: { description: "unauthorized" }, + }, + }, + patch: { + tags: ["order"], + summary: "Update order", + description: "", + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + required: true, + content: { + "application/json": { + example: JSON.stringify({ + "shopAccountId": "shop_12345", + "productPrice": 1500, + "productQuantity": 2, + "productName": "Wireless Mouse", + "customerName": "Rahim Uddin", + "customerPhone": "+8801712345678", + "customerAddress": "Rangpur, Bangladesh", + "customerNote": "Please deliver between 3-5 PM", + "paymentType": "Cash on Delivery", + "status": "Pending" + }), // put your request body + }, + }, + }, + responses: { + 200: { description: "order updated successfully" }, + 500: { description: "Validation error or internal server error" }, + }, + }, + delete: { + tags: ["order"], + summary: "Delete order", + description: "", + parameters: [ + { + name: "id", + in: "path", + required: true, + schema: { type: "string" }, + }, + ], + responses: { + 200: { description: "order delete successfully" }, + 401: { description: "unauthorized" }, + }, + }, + }, +}; + + diff --git a/src/app/modules/order/order.validation.ts b/src/app/modules/order/order.validation.ts new file mode 100644 index 0000000..52eaab8 --- /dev/null +++ b/src/app/modules/order/order.validation.ts @@ -0,0 +1,32 @@ + +import { uuid, z } from "zod"; + +const create_order = z.object({ + shopAccountId:z.string(), + productPrice:z.number(), + productQuantity:z.number(), + productName:z.string(), + customerName:z.string(), + customerPhone:z.string(), + customerAddress:z.string(), + customerNote:z.string(), + paymentType:z.string(), + status:z.string() +}); +const update_order = z.object({ + shopAccountId:z.string().optional(), + productPrice:z.number().optional(), + productQuantity:z.number().optional(), + productName:z.string().optional(), + customerName:z.string().optional(), + customerPhone:z.string().optional(), + customerAddress:z.string().optional(), + customerNote:z.string().optional(), + paymentType:z.string().optional(), + status:z.string().optional() +}); + +export const order_validations = { + create_order, + update_order, +}; diff --git a/src/app/utils/cloudinary.ts b/src/app/utils/cloudinary.ts index ae77b6b..0f8bed8 100644 --- a/src/app/utils/cloudinary.ts +++ b/src/app/utils/cloudinary.ts @@ -1,6 +1,6 @@ import { v2 as cloudinary } from 'cloudinary'; import fs from 'fs'; -import { configs } from '../configs'; +import { configs } from '../errors/configs'; type ICloudinaryResponse = { asset_id: string; diff --git a/src/app/utils/mail_sender.ts b/src/app/utils/mail_sender.ts index 57a5fe1..bbc4591 100644 --- a/src/app/utils/mail_sender.ts +++ b/src/app/utils/mail_sender.ts @@ -1,31 +1,31 @@ import nodemailer from 'nodemailer'; -import { configs } from '../configs'; +import { configs } from '../errors/configs'; type TMailContent = { - to: string, - subject: string, - textBody: string, - htmlBody: string, - name?: string + to: string, + subject: string, + textBody: string, + htmlBody: string, + name?: string } const transporter = nodemailer.createTransport({ - host: "smtp.gmail.com", - port: 465, - secure: true, // true for 465, false for other ports - auth: { - user: configs.email.app_email!, - pass: configs.email.app_password!, - }, + host: "smtp.gmail.com", + port: 465, + secure: true, // true for 465, false for other ports + auth: { + user: configs.email.app_email!, + pass: configs.email.app_password!, + }, }); // ✅ Email Sender Function const sendMail = async (payload: TMailContent) => { - const info = await transporter.sendMail({ - from: 'info@digitalcreditai.com', - to: payload.to, - subject: payload.subject, - text: payload.textBody, - html: ` + const info = await transporter.sendMail({ + from: 'info@digitalcreditai.com', + to: payload.to, + subject: payload.subject, + text: payload.textBody, + html: ` @@ -116,8 +116,8 @@ const sendMail = async (payload: TMailContent) => { `, - }); - return info + }); + return info }; export default sendMail; diff --git a/src/routes.ts b/src/routes.ts index c7b2768..8e4b611 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 orderRoute from "./app/modules/order/order.route"; const appRouter = Router(); const moduleRoutes = [ + { path: "/order", route: orderRoute }, { path: "/plan", route: planRoute }, { path: "/profile", route: profileRoute },{ path: "/auth", route: accountRouter }]; diff --git a/src/server.ts b/src/server.ts index 193e962..b730ede 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,5 @@ import app from "./app"; -import { configs } from "./app/configs/index"; +import { configs } from "./app/errors/configs/index"; import { prisma } from "./app/lib/prisma"; // import "./app/queues/worker"; diff --git a/src/swaggerOptions.ts b/src/swaggerOptions.ts index a787bda..18c1a22 100644 --- a/src/swaggerOptions.ts +++ b/src/swaggerOptions.ts @@ -1,9 +1,10 @@ import { fileURLToPath } from "node:url"; import path from "path"; -import { configs } from "./app/configs"; +import { configs } from "./app/errors/configs"; import { accountSwaggerDocs } from "./app/modules/account/account.swagger"; -import { profileSwaggerDocs } from "./app/modules/profile/profile.swagger"; +import { orderSwaggerDocs } from "./app/modules/order/order.swagger"; import { planSwaggerDocs } from "./app/modules/plan/plan.swagger"; +import { profileSwaggerDocs } from "./app/modules/profile/profile.swagger"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -18,8 +19,9 @@ export const swaggerOptions = { }, paths: { ...accountSwaggerDocs, - ...profileSwaggerDocs, - ...planSwaggerDocs, + ...profileSwaggerDocs, + ...planSwaggerDocs, + ...orderSwaggerDocs, }, servers: configs.env === "production"