From eeb6e710a9515028d9e80fc487648f8211942c3a Mon Sep 17 00:00:00 2001 From: Solange Duhimbaze Ihirwe <159579750+solangeihirwe03@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:21:25 +0200 Subject: [PATCH] [deliver #187584937] buyer track order status --- README.md | 6 +- package-lock.json | 24 +- package.json | 3 +- .../20240604150804-create-orders.ts | 8 + src/databases/models/carts.ts | 2 +- src/databases/models/orders.ts | 12 + .../seeders/20240604133044-orders.ts | 4 + src/helpers/notifications.ts | 6 +- src/middlewares/validation.ts | 49 +- .../cart/controller/cartControllers.ts | 51 ++ .../cart/repositories/cartRepositories.ts | 47 +- src/modules/cart/test/cart.spec.ts | 398 ++++++++++++++- .../cart/validation/cartValidations.ts | 8 +- src/modules/user/test/user.spec.ts | 2 + src/routes/cartRouter.ts | 7 +- src/services/sendEmail.ts | 2 +- swagger.json | 464 ++++++++++++++---- 17 files changed, 952 insertions(+), 141 deletions(-) diff --git a/README.md b/README.md index 22924b82..a8344cb2 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ Our e-commerce web application server, developed by Team Ninjas, facilitates smo - Submit a seller request - Buyer track order status Endpoint - Admin update order status EndPoint +- Buyer get order history Endpoint ## TABLE OF API ENDPOINTS SPECIFICATION AND DESCRIPTION @@ -122,8 +123,9 @@ Our e-commerce web application server, developed by Team Ninjas, facilitates smo | 44 | PUT | /api/user/user-mark-notification/:id | 200 OK | private | user mark notification | | 45 | POST | /api/shop/buyer-review-product/:id | 200 OK | private | Buyer Create review | | 46 | POST | /api/user/user-submit-seller-request | 200 OK | private | Submit a seller request | -| 46 | POST | /api/cart/user-get-order-status/ | 200 OK | private | user get order status | -| 47 | PUT | /api/cart/admin-update-order-status/:id | 200 OK | private | admin update order status | +| 47 | GET | /api/cart/user-get-order-status/:id | 200 OK | private | user get order status | +| 48 | GET | /api/cart/buyer-get-order-history | 200 OK | private | buyer get order history | +| 49 | PUT | /api/cart/admin-update-order-status/:id | 200 OK | private | admin update order status | ## INSTALLATION diff --git a/package-lock.json b/package-lock.json index d7ea0ce6..a425feb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,8 +59,7 @@ "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "typescript": "^5.4.5", - "uuid": "^9.0.1", - "ws": "^8.17.0" + "uuid": "^9.0.1" }, "devDependencies": { "@types/bcrypt": "^5.0.2", @@ -11165,27 +11164,6 @@ "typedarray-to-buffer": "^3.1.5" } }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/xmlhttprequest-ssl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", diff --git a/package.json b/package.json index 0137d3cb..8f04e9da 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "src/services/stripe.ts", "src/services/cronJob.ts", "src/helpers/passwordExpiryNotifications.ts" - ], "reporter": [ "html", @@ -135,4 +134,4 @@ "eslint-plugin-import": "^2.29.1", "lint-staged": "^15.2.2" } -} \ No newline at end of file +} diff --git a/src/databases/migrations/20240604150804-create-orders.ts b/src/databases/migrations/20240604150804-create-orders.ts index 111e00fd..2c7a9978 100644 --- a/src/databases/migrations/20240604150804-create-orders.ts +++ b/src/databases/migrations/20240604150804-create-orders.ts @@ -46,6 +46,14 @@ export = { type: DataTypes.STRING, allowNull: false }, + shippingProcess: { + type: DataTypes.STRING, + allowNull: false + }, + expectedDeliveryDate:{ + type:DataTypes.DATE, + allowNull: false + }, createdAt: { type: DataTypes.DATE, allowNull: false, diff --git a/src/databases/models/carts.ts b/src/databases/models/carts.ts index c53f064b..a066777c 100644 --- a/src/databases/models/carts.ts +++ b/src/databases/models/carts.ts @@ -26,7 +26,7 @@ class Carts extends Model implements CartAttributes { static associate() { Carts.belongsTo(Users, { foreignKey: "userId", as: "buyer" }); Carts.hasMany(CartProducts, { foreignKey: "cartId", as: "cartProducts" }); - Carts.hasMany(Orders,{foreignKey: "cartId", as: "order"}) + Carts.hasMany(Orders,{foreignKey: "cartId", as: "orders"}) } } diff --git a/src/databases/models/orders.ts b/src/databases/models/orders.ts index fb393f7e..c4c3007d 100644 --- a/src/databases/models/orders.ts +++ b/src/databases/models/orders.ts @@ -14,6 +14,8 @@ export interface OrderAttributes { paymentMethodId: string; orderDate: Date; status: string; + shippingProcess: string; + expectedDeliveryDate: Date createdAt: Date; updatedAt: Date; } @@ -26,6 +28,8 @@ class Orders extends Model implements OrderAttributes { declare paymentMethodId: string; declare orderDate: Date; declare status: string; + declare shippingProcess: string; + declare expectedDeliveryDate: Date; declare createdAt: Date; declare updatedAt: Date; @@ -68,6 +72,14 @@ Orders.init( type: new DataTypes.STRING, allowNull: false }, + shippingProcess: { + type: DataTypes.STRING, + allowNull: false + }, + expectedDeliveryDate:{ + type:DataTypes.DATE, + allowNull: false + }, createdAt: { field: "createdAt", type: DataTypes.DATE, diff --git a/src/databases/seeders/20240604133044-orders.ts b/src/databases/seeders/20240604133044-orders.ts index a14dbef8..b4f41fe7 100644 --- a/src/databases/seeders/20240604133044-orders.ts +++ b/src/databases/seeders/20240604133044-orders.ts @@ -22,6 +22,8 @@ module.exports = { paymentMethodId: 1, orderDate: new Date("2024-01-01"), status: "completed", + shippingProcess : "your order have been completed", + expectedDeliveryDate: new Date("2024-08-01"), createdAt: new Date(), updatedAt: new Date() }, @@ -42,6 +44,8 @@ module.exports = { paymentMethodId: 2, orderDate: new Date("2024-01-15"), status: "completed", + shippingProcess : "your order have reached Kigali in 30 minutes it will be reached to you", + expectedDeliveryDate: new Date("2024-08-05"), createdAt: new Date(), updatedAt: new Date() } diff --git a/src/helpers/notifications.ts b/src/helpers/notifications.ts index f9b6259c..21e36f09 100644 --- a/src/helpers/notifications.ts +++ b/src/helpers/notifications.ts @@ -83,13 +83,13 @@ eventEmitter.on("passwordExpiry", async ({ userId, message }) => { await saveAndEmitNotification(userId, message, "passwordExpiry"); }); -eventEmitter.on('orderStatusUpdated', async (order) => { +eventEmitter.on("orderStatusUpdated", async (order) => { const orderStatus = await fetchOrderWithCarts(order.id) const userId = orderStatus.carts.userId - const message = `The order that was created on ${order.orderDate} status has been updated to ${order.status}.`; + const message = order.shippingProcess; await userRepositories.addNotification(userId, message); await sendEmailOrderStatus(userId, message); - io.to(userId).emit('orderStatusUpdated', message) + io.to(userId).emit("orderStatusUpdated", message) }); cron.schedule("0 0 * * *", async () => { diff --git a/src/middlewares/validation.ts b/src/middlewares/validation.ts index 362d38e7..33cb4532 100644 --- a/src/middlewares/validation.ts +++ b/src/middlewares/validation.ts @@ -893,6 +893,50 @@ const isSellerRequestExist = async (req: Request, res: Response, next: NextFunct } }; +const isOrderExist = async (req: Request, res:Response, next:NextFunction)=>{ + try{ + let order: any; + if (req.user.role === "buyer") { + if(req.params.id){ + order = await cartRepositories.getOrderByOrderIdAndUserId(req.params.id, req.user.id) + if(!order){ + return res.status(httpStatus.NOT_FOUND).json({ + status: httpStatus.NOT_FOUND, + error: "order not found" + }) + } + }else{ + order = await cartRepositories.getOrdersByUserId(req.user.id) + if(!order.orders || order.orders.length === 0){ + return res.status(httpStatus.NOT_FOUND).json({ + status: httpStatus.NOT_FOUND, + error: "orders not found" + }) + } + } + + } + if(req.user.role === "admin"){ + order = await cartRepositories.getOrderById(req.params.id) + if (!order) { + return res.status(httpStatus.NOT_FOUND).json({ + status: httpStatus.NOT_FOUND, + error: "order Not Found", + }); + } + } + (req as any).order = order + return next(); + } + catch(error){ + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ + status: httpStatus.INTERNAL_SERVER_ERROR, + error: error.message + }) + } +} + + export { validation, isUserExist, @@ -924,5 +968,6 @@ export { isProductExistIntoWishList, isProductOrdered, isUserProfileComplete, - isSellerRequestExist -}; + isSellerRequestExist, + isOrderExist +}; \ No newline at end of file diff --git a/src/modules/cart/controller/cartControllers.ts b/src/modules/cart/controller/cartControllers.ts index 3ceb248d..0e26f970 100644 --- a/src/modules/cart/controller/cartControllers.ts +++ b/src/modules/cart/controller/cartControllers.ts @@ -348,6 +348,56 @@ const paymentCanceled = (req: Request, res: Response) => { } }; +const buyerGetOrderStatus = async (req: ExtendRequest, res: Response) => { + try { + const order = req.order.shippingProcess + return res.status(httpStatus.OK).json({ + 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 buyerGetOrders = async(req:ExtendRequest, res:Response)=>{ + try{ + const orders = req.order + return res.status(httpStatus.OK).json({ + message: "Orders found successfully", + data: {orders} + }) + } + 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) => { + try { + const order = req.order + await cartRepositories.updateOrderStatus(req.params.id, req.body.status,req.body.shippingProcess); + eventEmitter.emit("orderStatusUpdated", order); + return res.status(httpStatus.OK).json({ + message: "Status updated successfully!", + data: { order } + }) + }catch(error){ + return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ + status: httpStatus.INTERNAL_SERVER_ERROR, + error: error.message + }) + } + +} + export { buyerGetCart, buyerGetCarts, @@ -364,5 +414,6 @@ export { paymentCanceled, addProductToExistingCart, buyerGetOrderStatus, + buyerGetOrders, adminUpdateOrderStatus }; \ No newline at end of file diff --git a/src/modules/cart/repositories/cartRepositories.ts b/src/modules/cart/repositories/cartRepositories.ts index ebed9e54..7c82fc60 100644 --- a/src/modules/cart/repositories/cartRepositories.ts +++ b/src/modules/cart/repositories/cartRepositories.ts @@ -135,6 +135,47 @@ const saveOrder = async(lineItems: any, shopIds: any, productIds: any, session: return await db.Orders.create(order) } +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, shippingProcess: string) => { + return await db.Orders.update( + { + status: status, + shippingProcess: shippingProcess + }, + { where: { id: orderId } } + ); +}; +const getOrdersByUserId = async (userId: string) => { + return await db.Carts.findOne( + { + where: + { userId: userId }, + include: [ + { + model: db.Orders, + as: "orders", + } + ] + }); +}; + + export default { getCartsByUserId, getCartProductsByCartId, @@ -154,5 +195,9 @@ export default { findCartProductByCartId, findCartIdbyUserId, findProductById, - saveOrder + saveOrder, + getOrderByOrderIdAndUserId, + getOrderById, + getOrdersByUserId, + 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 4d88fbfc..6137cb9e 100644 --- a/src/modules/cart/test/cart.spec.ts +++ b/src/modules/cart/test/cart.spec.ts @@ -18,6 +18,7 @@ import { isCartIdExist, isProductIdExist, isCartProductExist, + isOrderExist } from "../../../middlewares/validation"; import productRepositories from "../../product/repositories/productRepositories"; import { @@ -1266,4 +1267,399 @@ describe("Payment Handlers", () => { done(error) }); }); -}); \ No newline at end of file +}); + +describe("isOrderExist Middleware", () => { + let req, res, next, sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + req = { + user: {}, + params: { id: "order-id" }, + }; + res = { + status: sinon.stub().returnsThis(), + json: sinon.stub().returnsThis() + }; + next = sinon.stub(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should find the order for a buyer with order ID", async () => { + req.user.role = "buyer"; + 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 find orders for a buyer without order ID", async () => { + req.user.role = "buyer"; + req.user.id = "user-id"; + const mockOrders = { orders: [{ id: "order-id" }] }; + sandbox.stub(cartRepositories, "getOrdersByUserId").resolves(mockOrders); + + req.params.id = null; + await isOrderExist(req, res, next); + + expect(req.order).to.equal(mockOrders); + expect(next).to.have.been.calledOnce; + }); + + it("should return 404 if no orders found for a buyer without order ID", async () => { + req.user.role = "buyer"; + req.user.id = "user-id"; + sandbox.stub(cartRepositories, "getOrdersByUserId").resolves({ orders: [] }); + + req.params.id = 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, + error: "orders not found", + }); + expect(next).not.to.have.been.called; + }); + + it("should find the order for an admin", async () => { + req.user.role = "admin"; + 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"; + 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, + error: "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.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, + error: "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({ + message: "Order Status found successfully", + data: { + order: req.order.shippingProcess + } + }); + }); +}); + +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", "Kigali"); + + expect(result).to.equal(mockUpdateResult); + expect(db.Orders.update).to.have.been.calledOnceWith( + { + status: "completed", + shippingProcess: "Kigali" + }, + { 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", "Mombasa"); + + expect(result).to.equal(mockUpdateResult); + expect(db.Orders.update).to.have.been.calledOnceWith( + { status: "completed", + shippingProcess : "Mombasa" + }, + { 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", "Kigali"); + 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 = { + params: { id: "order-id" }, + body: { status: "completed", + shippingProcess: "Kigali" + }, + 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({ + message: "Status updated successfully!", + data: { order: req.order } + }); + }); +}); + +describe("adminUpdateOrderStatus", () => { + let req, res, sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + req = { + params: { id: "order-id" }, + body: { + status: "completed", + shippingProcess: "Mombasa" + }, + order: { id: "order-id" } + }; + res = { + status: sinon.stub().returnsThis(), + json: sinon.stub().returnsThis() + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + it("should handle errors", async () => { + const errorMessage = "An error occurred"; + sandbox.stub(cartRepositories, "updateOrderStatus").throws(new Error(errorMessage)); + + await cartController.adminUpdateOrderStatus(req, res); + + expect(res.status).to.have.been.calledWith(httpStatus.INTERNAL_SERVER_ERROR); + expect(res.json).to.have.been.calledWith({ + status: httpStatus.INTERNAL_SERVER_ERROR, + error: errorMessage + }); + }); +}); + +describe("buyerGetOrders", () => { + let req, res, sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + req = { + order: { id: "order-id" } // You can mock more detailed order data here + }; + res = { + status: sinon.stub().returnsThis(), + json: sinon.stub().returnsThis() + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return the orders successfully", async () => { + await cartController.buyerGetOrders(req, res); + + expect(res.status).to.have.been.calledWith(httpStatus.OK); + expect(res.json).to.have.been.calledWith({ + message: "Orders found successfully", + data: { + orders: req.order + } + }); + }); +}); diff --git a/src/modules/cart/validation/cartValidations.ts b/src/modules/cart/validation/cartValidations.ts index 02cbb70b..560360af 100644 --- a/src/modules/cart/validation/cartValidations.ts +++ b/src/modules/cart/validation/cartValidations.ts @@ -16,7 +16,13 @@ const cartSchema = Joi.object({ const paymentCheckoutSchema = Joi.object({ cartId: Joi.string().required() }) + +const updateOrderStatusSchema = Joi.object({ + status: Joi.string().required(), + shippingProcess :Joi.string().required() + }); export { cartSchema, - paymentCheckoutSchema + paymentCheckoutSchema, + updateOrderStatusSchema } \ No newline at end of file diff --git a/src/modules/user/test/user.spec.ts b/src/modules/user/test/user.spec.ts index 0067588f..3ab0c19c 100644 --- a/src/modules/user/test/user.spec.ts +++ b/src/modules/user/test/user.spec.ts @@ -19,6 +19,8 @@ import { hashPassword } from "../../../helpers"; const imagePath = path.join(__dirname, "../test/testImage.jpg"); const imageBuffer = fs.readFileSync(imagePath); +import { sendEmail, sendEmailOrderStatus } from "../../../services/sendEmail"; +import { transporter } from "../../../services/sendEmail"; chai.use(chaiHttp); const router = () => chai.request(app); diff --git a/src/routes/cartRouter.ts b/src/routes/cartRouter.ts index 017cad66..e39a3661 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,paymentCheckoutSchema } from "../modules/cart/validation/cartValidations"; +import { cartSchema,paymentCheckoutSchema,updateOrderStatusSchema } from "../modules/cart/validation/cartValidations"; import { webhook } from "../services/stripe"; const router: Router = Router(); @@ -69,4 +70,8 @@ router.get( router.get("/payment-success", userAuthorization(["buyer"]),cartControllers.paymentSuccess) router.get("/payment-canceled", userAuthorization(["buyer"]),cartControllers.paymentCanceled) +router.get("/user-get-order-status/:id",userAuthorization(["buyer"]), isOrderExist, cartControllers.buyerGetOrderStatus ) +router.put("/admin-update-order-status/:id", userAuthorization(["admin"]),validation(updateOrderStatusSchema),isOrderExist, cartControllers.adminUpdateOrderStatus) +router.get("/buyer-get-order-history", userAuthorization(["buyer"]),isOrderExist, cartControllers.buyerGetOrders) + export default router; \ No newline at end of file diff --git a/src/services/sendEmail.ts b/src/services/sendEmail.ts index a7005fee..520a0d40 100644 --- a/src/services/sendEmail.ts +++ b/src/services/sendEmail.ts @@ -51,7 +51,7 @@ const sendEmailOrderStatus = async (userId: string, message: string) => { const mailOptions: SendMailOptions = { from: process.env.MAIL_ID, to: user.email, - subject: "Order status", + subject: "Order notification", text: message }; diff --git a/swagger.json b/swagger.json index 8e1a8ccf..37b1e505 100644 --- a/swagger.json +++ b/swagger.json @@ -3483,7 +3483,9 @@ }, "/api/cart/checkout": { "post": { - "tags": ["Buyer Routes"], + "tags": [ + "Buyer Routes" + ], "parameters": [ { "name": "cartId", @@ -3519,58 +3521,62 @@ } }, "/api/user/user-submit-seller-request": { - "post": { - "tags": ["User Routes"], - "summary": "Submit a seller request", - "description": "Allows a user to submit a request to become a seller. Only buyers can submit this request. The user's profile must be complete and they must not have an existing seller request.", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Seller request submitted successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "integer", - "example": 200 - }, - "message": { - "type": "string", - "example": "Seller request submitted successfully" - }, - "data": { + "post": { + "tags": [ + "User Routes" + ], + "summary": "Submit a seller request", + "description": "Allows a user to submit a request to become a seller. Only buyers can submit this request. The user's profile must be complete and they must not have an existing seller request.", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Seller request submitted successfully", + "content": { + "application/json": { + "schema": { "type": "object", "properties": { - "sellerRequest": { + "status": { + "type": "integer", + "example": 200 + }, + "message": { + "type": "string", + "example": "Seller request submitted successfully" + }, + "data": { "type": "object", "properties": { - "id": { - "type": "string", - "example": "uuid" - }, - "userId": { - "type": "string", - "example": "uuid" - }, - "requestStatus": { - "type": "string", - "example": "Pending" - }, - "createdAt": { - "type": "string", - "format": "date-time", - "example": "2024-07-04T12:34:56Z" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "example": "2024-07-04T12:34:56Z" + "sellerRequest": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid" + }, + "userId": { + "type": "string", + "example": "uuid" + }, + "requestStatus": { + "type": "string", + "example": "Pending" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "example": "2024-07-04T12:34:56Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "example": "2024-07-04T12:34:56Z" + } + } } } } @@ -3578,72 +3584,69 @@ } } } - } - } - }, - "400": { - "description": "User profile is incomplete or Seller request already submitted", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "integer", - "example": 400 - }, - "message": { - "type": "string", - "example": "User profile is incomplete. Please fill out all required fields or Seller request already submitted" + }, + "400": { + "description": "User profile is incomplete or Seller request already submitted", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "integer", + "example": 400 + }, + "message": { + "type": "string", + "example": "User profile is incomplete. Please fill out all required fields or Seller request already submitted" + } + } } } } - } - } - }, - "401": { - "description": "Unauthorized access", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "integer", - "example": 401 - }, - "message": { - "type": "string", - "example": "Not authorized" + }, + "401": { + "description": "Unauthorized access", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "integer", + "example": 401 + }, + "message": { + "type": "string", + "example": "Not authorized" + } + } } } } - } - } - }, - "500": { - "description": "Internal Server error", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "integer", - "example": 500 - }, - "error": { - "type": "string", - "example": "Internal Server error" + }, + "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/user/user-get-notifications": { "get": { @@ -3817,6 +3820,261 @@ } } } + }, + "/api/cart/buyer-get-order-history": { + "get": { + "tags": [ + "Buyer Routes" + ], + "summary": "Get Orders for Buyer", + "description": "Retrieve all orders associated with the authenticated buyer", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Orders found successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "orders": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Orders not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "integer" + }, + "error": { + "type": "string" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "integer" + }, + "error": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/api/cart/user-get-order-status/{id}": { + "get": { + "tags": [ + "Buyer Routes" + ], + "summary": "Buyer get order status", + "description": "Retrieve the status of an order by order ID.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The ID of the order to get the status for", + "schema": { + "type": "string" + } + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Order Status found successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Order Status found successfully" + }, + "data": { + "type": "object", + "properties": { + "order": { + "type": "string", + "example": "shipped" + } + } + } + } + } + } + } + }, + "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