From 7d2d3a435079921a19a810998f21beb50979f2d9 Mon Sep 17 00:00:00 2001 From: Solange Duhimbaze Ihirwe <159579750+solangeihirwe03@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:14:56 +0200 Subject: [PATCH] [finishes #187584937] buyer track order status --- README.md | 12 +- ...ew.ts => 20240612090847-productReviews.ts} | 0 src/middlewares/validation.ts | 84 +++-- .../cart/controller/cartControllers.ts | 59 ++-- .../cart/repositories/cartRepositories.ts | 29 +- src/modules/cart/test/cart.spec.ts | 315 +++++++++++++++++- .../cart/validation/cartValidations.ts | 14 +- src/routes/cartRouter.ts | 8 +- src/types/index.d.ts | 1 + swagger.json | 160 ++++++++- 10 files changed, 613 insertions(+), 69 deletions(-) rename src/databases/migrations/{20240612090847-product-review.ts => 20240612090847-productReviews.ts} (100%) diff --git a/README.md b/README.md index 586b684e..2e1be38d 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ Our e-commerce web application server, developed by Team Ninjas, facilitates smo - buyer delete product from wishlist - buyer view All products fromwishList Endpoint - buyer view single product fromwishList Endpoint +- Buyer track order status Endpoint +- Admin update order status Endpoint ## TABLE OF API ENDPOINTS SPECIFICATION AND DESCRIPTION @@ -108,6 +110,8 @@ Our e-commerce web application server, developed by Team Ninjas, facilitates smo | 38 | delete | /api/shop/delete-whishlist-product:id | 200 OK | private | buyer delete product from wishlist | | 39 | get | /buyer-view-whishlist-product | 200 ok | private | buyer view All product from wishList| | 40 | get | /api/shop/buyer-view-whishlist-product/{id}| 200 ok | private | buyer view product from wishList | +| 41 | POST | /api/cart/user-get-order-status/ | 200 OK | private | user get order status | +| 42 | PUT | /api/cart/admin-update-order-status/:id | 200 OK | private | admin update order status | ## INSTALLATION @@ -176,12 +180,4 @@ Our e-commerce web application server, developed by Team Ninjas, facilitates smo 7. Run the Migration: ```sh npm run createAllTables - ``` -8. Delete the Seeder: - ```sh - npm run deleteAllSeeders - ``` -9. Delete the Migration: - ```sh - npm run deleteAllTables ``` \ No newline at end of file diff --git a/src/databases/migrations/20240612090847-product-review.ts b/src/databases/migrations/20240612090847-productReviews.ts similarity index 100% rename from src/databases/migrations/20240612090847-product-review.ts rename to src/databases/migrations/20240612090847-productReviews.ts diff --git a/src/middlewares/validation.ts b/src/middlewares/validation.ts index d4631c30..13ec33ab 100644 --- a/src/middlewares/validation.ts +++ b/src/middlewares/validation.ts @@ -28,24 +28,24 @@ import db from "../databases/models"; const validation = (schema: Joi.ObjectSchema | Joi.ArraySchema) => - async (req: Request, res: Response, next: NextFunction) => { - try { - const { error } = schema.validate(req.body, { abortEarly: false }); - - if (error) { - throw new Error( - error.details - .map((detail) => detail.message.replace(/"/g, "")) - .join(", ") - ); + async (req: Request, res: Response, next: NextFunction) => { + try { + const { error } = schema.validate(req.body, { abortEarly: false }); + + if (error) { + throw new Error( + error.details + .map((detail) => detail.message.replace(/"/g, "")) + .join(", ") + ); + } + return next(); + } catch (error) { + res + .status(httpStatus.BAD_REQUEST) + .json({ status: httpStatus.BAD_REQUEST, message: error.message }); } - return next(); - } catch (error) { - res - .status(httpStatus.BAD_REQUEST) - .json({ status: httpStatus.BAD_REQUEST, message: error.message }); - } - }; + }; const isUserExist = async (req: Request, res: Response, next: NextFunction) => { try { @@ -193,8 +193,7 @@ const verifyUserCredentials = async ( await sendEmail( user.email, "E-Commerce Ninja Login", - `Dear ${ - user.lastName || user.email + `Dear ${user.lastName || user.email }\n\nUse This Code To Confirm Your Account: ${otp}` ); @@ -529,13 +528,13 @@ const isGoogleEnabled = async (req: any, res: Response, next: NextFunction) => { const isCartExist = async (req: ExtendRequest, res: Response, next: NextFunction) => { try { - const cart = await cartRepositories.getCartsByUserId (req.user.id); + const cart = await cartRepositories.getCartsByUserId(req.user.id); if (!cart) { - return res.status(httpStatus.NOT_FOUND).json({ status: httpStatus.NOT_FOUND, message: "No cart found. Please create a cart first." }); - } - req.cart = cart; - return next(); - + return res.status(httpStatus.NOT_FOUND).json({ status: httpStatus.NOT_FOUND, message: "No cart found. Please create a cart first." }); + } + req.cart = cart; + return next(); + } catch (error) { return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, @@ -763,6 +762,40 @@ const isUserWishlistExistById = async ( } }; +const isOrderExist = async (req: ExtendRequest, res: Response, next: NextFunction) => { + try { + let order; + if (req.user.role === "buyer") { + + order = await cartRepositories.getOrderByOrderIdAndUserId(req.body.orderId, req.user.id) + if (!order) { + return res.status(httpStatus.NOT_FOUND).json({ + status: httpStatus.NOT_FOUND, + message: "order Not Found", + }); + } + } + if(req.user.role === "admin"){ + order = await cartRepositories.getOrderById(req.body.orderId) + if (!order) { + return res.status(httpStatus.NOT_FOUND).json({ + status: httpStatus.NOT_FOUND, + message: "order Not Found", + }); + } + } + req.order = order + + return next(); + } catch (error) { + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ + status: httpStatus.INTERNAL_SERVER_ERROR, + message: error.message, + }); + } +} + + export { validation, isUserExist, @@ -790,4 +823,5 @@ export { isProductExistToWishlist, isUserWishlistExist, isUserWishlistExistById, + isOrderExist }; \ No newline at end of file diff --git a/src/modules/cart/controller/cartControllers.ts b/src/modules/cart/controller/cartControllers.ts index ad14ab39..b0b1df5b 100644 --- a/src/modules/cart/controller/cartControllers.ts +++ b/src/modules/cart/controller/cartControllers.ts @@ -200,35 +200,6 @@ const buyerCreateUpdateCart = async (req: ExtendRequest, res: Response) => { }); } }; -const buyerGetOrderStatus = async(req:ExtendRequest, res:Response)=>{ - try{ - const status= await cartRepositories.getOrderStatus(req.params.id) - return res.status(200).json({ - message: "Order Status found successfully", - data: status - }) - - }catch(error){ - return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ - status: httpStatus.INTERNAL_SERVER_ERROR, - error: error.message - }) - } -} - - -const adminUpdateOrderStatus = async(req:ExtendRequest, res:Response)=>{ - const orderId = req.params.id; - const updatedStatus: any = { - status:req.body.status - }; - - const updateStatus = await cartRepositories.updateOrderStatus(orderId, updatedStatus); - return res.status(httpStatus.OK).json({ - status: "Status updated successfully!", - data:updateStatus - }) -} const buyerClearCartProduct = async (req: ExtendRequest, res: Response) => { try { @@ -301,6 +272,36 @@ const buyerCheckout = async (req: ExtendRequest, res: Response) => { }); } }; + +const buyerGetOrderStatus = async (req: ExtendRequest, res: Response) => { + try { + const order = req.order + return res.status(httpStatus.OK).json({ + status: httpStatus.OK, + message: "Order Status found successfully", + data: { + order + } + }) + + } catch (error) { + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ + status: httpStatus.INTERNAL_SERVER_ERROR, + error: error.message + }) + } +} + +const adminUpdateOrderStatus = async (req: ExtendRequest, res: Response) => { + + const order = req.order + await cartRepositories.updateOrderStatus(req.body.orderId, req.body.status); + return res.status(httpStatus.OK).json({ + status: httpStatus.OK, + message: "Status updated successfully!", + data: { order } + }) +} export { buyerGetCart, buyerGetCarts, diff --git a/src/modules/cart/repositories/cartRepositories.ts b/src/modules/cart/repositories/cartRepositories.ts index 7330ea45..cd45ccb7 100644 --- a/src/modules/cart/repositories/cartRepositories.ts +++ b/src/modules/cart/repositories/cartRepositories.ts @@ -87,6 +87,30 @@ const deleteCartById = async (id: string) => { const findCartByAttributes = async(key1: string, value1:any, key2: string, value2:any): Promise => { return await db.Carts.findOne({ where: { [key1]: value1, [key2]: value2 } }) } +const getOrderByOrderIdAndUserId = async(orderId: string, userId: string)=>{ + return await db.Orders.findOne({ + where: { id: orderId }, + include: [ + { + model: db.Carts, + as: "carts", + where: {userId:userId} + } + ] + }) +} + +const getOrderById = async(orderId: string)=>{ + return await db.Orders.findOne({where: {id:orderId}}) +} + +const updateOrderStatus = async(orderId: string, status:string)=>{ + return await db.Orders.update( + {status: status}, + {where: {id: orderId}} + ) +} + export default { getCartsByUserId, getCartProductsByCartId, @@ -100,5 +124,8 @@ export default { deleteCartById, deleteCartProduct, deleteAllCartProducts, - findCartByAttributes + findCartByAttributes, + getOrderByOrderIdAndUserId, + getOrderById, + updateOrderStatus }; \ No newline at end of file diff --git a/src/modules/cart/test/cart.spec.ts b/src/modules/cart/test/cart.spec.ts index e4bb346c..ac33e59f 100644 --- a/src/modules/cart/test/cart.spec.ts +++ b/src/modules/cart/test/cart.spec.ts @@ -13,6 +13,7 @@ import { isCartExist, isCartIdExist, isProductIdExist, + isOrderExist } from "../../../middlewares/validation"; import productRepositories from "../../product/repositories/productRepositories"; import { @@ -927,4 +928,316 @@ describe("buyerClearCarts", () => { }) ).to.be.true; }); -}); \ No newline at end of file +}); + +describe("isOrderExist Middleware", () => { + let req, res, next, sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + req = { + user: {}, + body: {} + }; + res = { + status: sinon.stub().returnsThis(), + json: sinon.stub().returnsThis() + }; + next = sinon.stub(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should find the order for a buyer", async () => { + req.user.role = "buyer"; + req.body.orderId = "order-id"; + req.user.id = "user-id"; + const mockOrder = { id: "order-id" }; + sandbox.stub(cartRepositories, "getOrderByOrderIdAndUserId").resolves(mockOrder); + + await isOrderExist(req, res, next); + + expect(req.order).to.equal(mockOrder); + expect(next).to.have.been.calledOnce; + }); + + it("should return 404 if order is not found for a buyer", async () => { + req.user.role = "buyer"; + req.body.orderId = "order-id"; + req.user.id = "user-id"; + sandbox.stub(cartRepositories, "getOrderByOrderIdAndUserId").resolves(null); + + await isOrderExist(req, res, next); + + expect(res.status).to.have.been.calledWith(httpStatus.NOT_FOUND); + expect(res.json).to.have.been.calledWith({ + status: httpStatus.NOT_FOUND, + message: "order Not Found", + }); + expect(next).not.to.have.been.called; + }); + + it("should find the order for an admin", async () => { + req.user.role = "admin"; + req.body.orderId = "order-id"; + const mockOrder = { id: "order-id" }; + sandbox.stub(cartRepositories, "getOrderById").resolves(mockOrder); + + await isOrderExist(req, res, next); + + expect(req.order).to.equal(mockOrder); + expect(next).to.have.been.calledOnce; + }); + + it("should return 404 if order is not found for an admin", async () => { + req.user.role = "admin"; + req.body.orderId = "order-id"; + sandbox.stub(cartRepositories, "getOrderById").resolves(null); + + await isOrderExist(req, res, next); + + expect(res.status).to.have.been.calledWith(httpStatus.NOT_FOUND); + expect(res.json).to.have.been.calledWith({ + status: httpStatus.NOT_FOUND, + message: "order Not Found", + }); + expect(next).not.to.have.been.called; + }); + + it("should return 500 if there is a server error", async () => { + req.user.role = "buyer"; + req.body.orderId = "order-id"; + req.user.id = "user-id"; + sandbox.stub(cartRepositories, "getOrderByOrderIdAndUserId").throws(new Error("Database error")); + + await isOrderExist(req, res, next); + + expect(res.status).to.have.been.calledWith(httpStatus.INTERNAL_SERVER_ERROR); + expect(res.json).to.have.been.calledWith({ + status: httpStatus.INTERNAL_SERVER_ERROR, + message: "Database error", + }); + expect(next).not.to.have.been.called; + }); +}); + +describe("getOrderByOrderIdAndUserId", () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return the order if found", async () => { + const mockOrder = { id: "order-id", carts: [{ userId: "user-id" }] }; + sandbox.stub(db.Orders, "findOne").resolves(mockOrder); + + const order = await cartRepositories.getOrderByOrderIdAndUserId("order-id", "user-id"); + + expect(order).to.equal(mockOrder); + expect(db.Orders.findOne).to.have.been.calledOnceWith({ + where: { id: "order-id" }, + include: [ + { + model: db.Carts, + as: "carts", + where: { userId: "user-id" } + } + ] + }); + }); + + it("should return null if order is not found", async () => { + sandbox.stub(db.Orders, "findOne").resolves(null); + + const order = await cartRepositories.getOrderByOrderIdAndUserId("order-id", "user-id"); + + expect(order).to.be.null; + expect(db.Orders.findOne).to.have.been.calledOnceWith({ + where: { id: "order-id" }, + include: [ + { + model: db.Carts, + as: "carts", + where: { userId: "user-id" } + } + ] + }); + }); + + it("should throw an error if there is a database error", async () => { + const errorMessage = "Database error"; + sandbox.stub(db.Orders, "findOne").throws(new Error(errorMessage)); + + try { + await cartRepositories.getOrderByOrderIdAndUserId("order-id", "user-id"); + throw new Error("Expected getOrderByOrderIdAndUserId to throw an error"); + } catch (error) { + expect(error.message).to.equal(errorMessage); + } + }); +}); + +describe("buyerGetOrderStatus", () => { + let req, res, sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + req = { + order: { id: "order-id" } + }; + res = { + status: sinon.stub().returnsThis(), + json: sinon.stub().returnsThis() + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return the order status", async () => { + await cartController.buyerGetOrderStatus(req, res); + + expect(res.status).to.have.been.calledWith(httpStatus.OK); + expect(res.json).to.have.been.calledWith({ + status: httpStatus.OK, + message: "Order Status found successfully", + data: { + order: req.order + } + }); + }); +}); + +describe("getOrderById", () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return the order if found", async () => { + const mockOrder = { id: "order-id" }; + sandbox.stub(db.Orders, "findOne").resolves(mockOrder); + + const order = await cartRepositories.getOrderById("order-id"); + + expect(order).to.equal(mockOrder); + expect(db.Orders.findOne).to.have.been.calledOnceWith({ where: { id: "order-id" } }); + }); + + it("should return null if order is not found", async () => { + sandbox.stub(db.Orders, "findOne").resolves(null); + + const order = await cartRepositories.getOrderById("order-id"); + + expect(order).to.be.null; + expect(db.Orders.findOne).to.have.been.calledOnceWith({ where: { id: "order-id" } }); + }); + + it("should throw an error if there is a database error", async () => { + const errorMessage = "Database error"; + sandbox.stub(db.Orders, "findOne").throws(new Error(errorMessage)); + + try { + await cartRepositories.getOrderById("order-id"); + throw new Error("Expected getOrderById to throw an error"); + } catch (error) { + expect(error.message).to.equal(errorMessage); + } + }); +}); + +describe("updateOrderStatus", () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should update the order status", async () => { + const mockUpdateResult = [1]; + sandbox.stub(db.Orders, "update").resolves(mockUpdateResult); + + const result = await cartRepositories.updateOrderStatus("order-id", "completed"); + + expect(result).to.equal(mockUpdateResult); + expect(db.Orders.update).to.have.been.calledOnceWith( + { status: "completed" }, + { where: { id: "order-id" } } + ); + }); + + it("should return an array with 0 if no rows were affected", async () => { + const mockUpdateResult = [0]; + sandbox.stub(db.Orders, "update").resolves(mockUpdateResult); + + const result = await cartRepositories.updateOrderStatus("order-id", "completed"); + + expect(result).to.equal(mockUpdateResult); + expect(db.Orders.update).to.have.been.calledOnceWith( + { status: "completed" }, + { where: { id: "order-id" } } + ); + }); + + it("should throw an error if there is a database error", async () => { + const errorMessage = "Database error"; + sandbox.stub(db.Orders, "update").throws(new Error(errorMessage)); + + try { + await cartRepositories.updateOrderStatus("order-id", "completed"); + throw new Error("Expected updateOrderStatus to throw an error"); + } catch (error) { + expect(error.message).to.equal(errorMessage); + } + }); +}); +describe("adminUpdateOrderStatus", () => { + let req, res, sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + req = { + body: { orderId: "order-id", status: "completed" }, + order: { id: "order-id" } + }; + res = { + status: sinon.stub().returnsThis(), + json: sinon.stub().returnsThis() + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should update order status", async () => { + const mockUpdateStatus = [1]; + sandbox.stub(cartRepositories, "updateOrderStatus").resolves(mockUpdateStatus); + + await cartController.adminUpdateOrderStatus(req, res); + + expect(res.status).to.have.been.calledWith(httpStatus.OK); + expect(res.json).to.have.been.calledWith({ + status: httpStatus.OK, + message: "Status updated successfully!", + data: { order: req.order } + }); + }); +}); diff --git a/src/modules/cart/validation/cartValidations.ts b/src/modules/cart/validation/cartValidations.ts index 4a21b7a3..d8c4d548 100644 --- a/src/modules/cart/validation/cartValidations.ts +++ b/src/modules/cart/validation/cartValidations.ts @@ -13,7 +13,19 @@ const cartSchema = Joi.object({ "any.required": "quantity is required" }) }); +const orderStatusSchema = Joi.object({ + orderId: Joi.string().pattern(uuidPattern).required().messages({ + "string.pattern.base": "Order ID must be a valid UUID", + "string.empty": "Order ID is required" + }) +}); + const updateOrderStatusSchema = Joi.object({ + orderId: Joi.string().pattern(uuidPattern).required(), + status: Joi.string().required() + }); export { - cartSchema + cartSchema, + orderStatusSchema, + updateOrderStatusSchema } \ No newline at end of file diff --git a/src/routes/cartRouter.ts b/src/routes/cartRouter.ts index 54df1ccc..86ed1bed 100644 --- a/src/routes/cartRouter.ts +++ b/src/routes/cartRouter.ts @@ -7,9 +7,10 @@ import { isCartProductExist, isProductIdExist, validation, + isOrderExist } from "../middlewares/validation"; import * as cartControllers from "../modules/cart/controller/cartControllers"; -import { cartSchema } from "../modules/cart/validation/cartValidations"; +import { cartSchema, updateOrderStatusSchema, orderStatusSchema } from "../modules/cart/validation/cartValidations"; const router: Router = Router(); @@ -62,4 +63,7 @@ router.get( isCartIdExist, cartControllers.buyerCheckout ); -export default router; \ No newline at end of file + +router.post("/user-get-order-status",userAuthorization(["buyer"]),validation(orderStatusSchema),isOrderExist, cartControllers.buyerGetOrderStatus ) +router.put("/admin-update-order-status", userAuthorization(["admin"]),validation(updateOrderStatusSchema),isOrderExist, cartControllers.adminUpdateOrderStatus) +export default router; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 23aaa7e5..1a30f3ce 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -28,6 +28,7 @@ export interface ExtendRequest extends Request { offset: number; }; searchQuery?: any; + order?: any; } export interface IProduct { diff --git a/swagger.json b/swagger.json index 2e82c0e9..eddb95e1 100644 --- a/swagger.json +++ b/swagger.json @@ -3070,7 +3070,7 @@ } }, - "/api/shop/delete-wishlist-products": { + "/api/shop/delete-whishlist-products": { "delete": { "tags": ["Buyer Routes"], "summary": "Delete All Product From Wishlist", @@ -3155,7 +3155,7 @@ } }, - "/api/shop/delete-wishlist-product/{id}": { + "/api/shop/delete-whishlist-product/{id}": { "delete": { "tags": ["Buyer Routes"], "summary": "Delete Single Product from Wishlist", @@ -3249,6 +3249,162 @@ } } } + }, + "/api/cart/user-get-order-status": { + "post": { + "tags": [ + "Buyer Routes" + ], + "summary": "Get order status for a buyer", + "description": "Retrieve the status of an order by order ID for a buyer.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The ID of the order to retrieve the status for", + "schema": { + "type": "string" + } + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Order status retrieved successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Order Status found successfully" + }, + "data": { + "type": "string", + "example": "pending" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "integer", + "example": 500 + }, + "error": { + "type": "string", + "example": "Internal server error" + } + } + } + } + } + } +} + } + }, + "/api/cart/admin-update-order-status/{id}": { + "put": { + "tags": [ + "Buyer Routes" + ], + "summary": "Admin update order status", + "description": "Update the status of an order by order ID.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The ID of the order to update the status for", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Updated status of the order", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "shipped" + } + }, + "required": [ + "status" + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Status updated successfully!", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "Status updated successfully!" + }, + "data": { + "type": "object", + "example": { + "affectedRows": 1 + } + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "integer", + "example": 500 + }, + "error": { + "type": "string", + "example": "Internal server error" + } + } + } + } + } + } + } + } } } } \ No newline at end of file