diff --git a/src/controllers/getOrderController.ts b/src/controllers/getOrderController.ts new file mode 100644 index 0000000..66f8541 --- /dev/null +++ b/src/controllers/getOrderController.ts @@ -0,0 +1,48 @@ +import { Request, Response } from "express" +import { getOrderBuyer, updateOrderStatusService } from "../services/OrderService" +import * as mailService from "../services/mail.service"; +import { SUBJECTS, UserId } from "../types"; +import { orderStatusTemplate } from "../email-templates/orderStatus"; +import Order from "../sequelize/models/orders"; +import User from "../sequelize/models/users"; + + +export const getOrderController = async (req: Request, res: Response) => { + try { + const response = await getOrderBuyer(req, res) + return res.status(200).json({ + status: 200, + length: response?.length, + orders: response?.length === 0? "You have not order yet" : response + }) + } catch (error) { + console.log(error); + res.status(500).json({ + status: 500, + message: `error accured during fetching order ${error}` + }) + } +} + +export const updateOrderStatus = async (req: Request, res: Response) => { + + try { + const { id: userId} = req.user as UserId; + const { id: orderId } = req.params; + const { status } = req.body; + const buyerId = await Order.findByPk(orderId) + const dataValues = await User.findByPk(buyerId?.buyerId) + + const updatedItems = await updateOrderStatusService(userId, orderId, status); + if (updatedItems) { + // @ts-ignore + await mailService.sendNotification(dataValues?.dataValues.email, SUBJECTS.ORDER_STATUS_UPDATED, orderStatusTemplate(dataValues?.dataValues.username, buyerId?.id, buyerId?.createdAt, status)) + + return res.json({ message: 'Order items status updated', updatedItems }); + } + + } catch (error: any) { + res.status(500).json({ message: error.message }); + } + }; + \ No newline at end of file diff --git a/src/docs/orders.ts b/src/docs/orders.ts new file mode 100644 index 0000000..db5eabb --- /dev/null +++ b/src/docs/orders.ts @@ -0,0 +1,74 @@ +import { required } from "joi"; + +export const StatusSchema = { + type: "object", + properties: { + status: { + type: "string", + }, + }, +}; + +export const buyerAndSellerOrder = { + tags: ["orders"], + security: [{ bearerAuth: [] }], + summary: "Orders for seller and buyers", + responses: { + 200: { + description: "Success", + }, + 400: { + description: "Bad request", + }, + 401: { + description: "Unauthorized", + }, + 500: { + description: "Internal server error", + }, + }, +}; + +export const statusUpdate = { + tags: ["orders"], + security: [{ bearerAuth: [] }], + summary: "Status update for seller", + parameters: [ + { + name: "id", + in: "path", + required: true, + description: "order Id", + schema: { + type: "number", + }, + }, + ], + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { + status: { + type: "string", + }, + }, + required: ["status"], + }, + }, + }, + }, + responses: { + 201: { + description: "Upgrade successfully", + }, + 404: { + description: "Review not found", + }, + 500: { + description: "Internal server error.", + }, + }, +}; diff --git a/src/docs/swagger.ts b/src/docs/swagger.ts index db1d631..d944cb1 100644 --- a/src/docs/swagger.ts +++ b/src/docs/swagger.ts @@ -41,7 +41,8 @@ import { homepage } from "./home"; import { payment } from "./payments"; import { createReviewProduct, deleteReview, getReviewProduct, reviewSchema, updateReviewProduct } from "./reviews"; import { getAdProducts } from "./products"; -import { PrivateChatSchema, getAllUserPrivateChats, getUserToUserPrivateMessages, createPrivateChat } from "./privateChatDoc"; +import {PrivateChatSchema, getAllUserPrivateChats, getUserToUserPrivateMessages, createPrivateChat } from "./privateChatDoc" +import { StatusSchema, buyerAndSellerOrder, statusUpdate } from "./orders"; const docRouter = express.Router(); @@ -79,6 +80,7 @@ const options = { { name: "Carts", description: "Endpoints related to Cart" }, { name: "Payments", description: "Endpoints related to payments" }, { name: "PrivateChat", description: "Endpoints related to Private Chat" }, + {name: "orders", description: "Endpoints related to orders"} ], paths: { @@ -198,6 +200,12 @@ const options = { post: createPrivateChat, get: getUserToUserPrivateMessages, }, + "/api/v1/orders": { + get: buyerAndSellerOrder + }, + "/api/v1/orders/{id}/status": { + patch: statusUpdate + } }, components: { schemas: { @@ -211,6 +219,7 @@ const options = { Wish: wishSchema, Review: reviewSchema, PrivateChat: PrivateChatSchema, + status: StatusSchema }, securitySchemes: { bearerAuth: { diff --git a/src/email-templates/orderStatus.ts b/src/email-templates/orderStatus.ts new file mode 100644 index 0000000..32e80e7 --- /dev/null +++ b/src/email-templates/orderStatus.ts @@ -0,0 +1,63 @@ +export const orderStatusTemplate = (username: string, orderId: number, createdAt: Date, status: string) => { + return ` + + + + + + Order Status + + + + +
+

Order Status Updated 😍

+

Dear ${username}, Your product status with this order id #${orderId} has been successfully to ${status} .

+
Status updated at ${createdAt}

+ +
+ + + `; +}; diff --git a/src/routes/index.ts b/src/routes/index.ts index 138224d..2be3cae 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -8,6 +8,7 @@ import cartRoutes from "./cartRoutes"; import notificationRoutes from "./notificationRoutes"; import paymentRouter from "./paymentRoutes"; import PrivateChatRoutes from "./privateChatRoutes"; +import orderRouter from "./orderRoute"; const appROutes = Router(); appROutes.use("/chats", PrivateChatRoutes); @@ -19,4 +20,5 @@ appROutes.use("/messages", joinChatRoomRoutes); appROutes.use("/carts", cartRoutes); appROutes.use("/notifications", notificationRoutes); appROutes.use("/payment", paymentRouter); +appROutes.use("/orders", orderRouter) export default appROutes; diff --git a/src/routes/orderRoute.ts b/src/routes/orderRoute.ts new file mode 100644 index 0000000..13eede7 --- /dev/null +++ b/src/routes/orderRoute.ts @@ -0,0 +1,12 @@ +import express from "express" +import { getOrderController, updateOrderStatus, } from "../controllers/getOrderController"; +import { isLoggedIn } from "../middlewares/isLoggedIn"; +import { isAseller } from "../middlewares/sellerAuth"; + + +const orderRouter = express.Router() + +orderRouter.get('/', isLoggedIn, getOrderController) +orderRouter.patch("/:id/status", isLoggedIn, updateOrderStatus) + +export default orderRouter; \ No newline at end of file diff --git a/src/schemas/signUpSchema.ts b/src/schemas/signUpSchema.ts index 46a55e7..9827713 100644 --- a/src/schemas/signUpSchema.ts +++ b/src/schemas/signUpSchema.ts @@ -36,3 +36,9 @@ export const profileSchemas = Joi.object({ country: Joi.string() .optional() }) + +export const OrderStatus = Joi.object({ + status: Joi.string() + .valid("Pending", "Delivered", "Cancelled") + .required() +}) diff --git a/src/sequelize/migrations/x20240711191430-modify-order-items.js b/src/sequelize/migrations/x20240711191430-modify-order-items.js new file mode 100644 index 0000000..eff03e2 --- /dev/null +++ b/src/sequelize/migrations/x20240711191430-modify-order-items.js @@ -0,0 +1,31 @@ +module.exports = { + async up(queryInterface, Sequelize) { + return Promise.all([ + queryInterface.addColumn( + 'orderItems', + 'userId', + { + type: Sequelize.INTEGER, + allowNull: true, + }, + ), + queryInterface.addColumn( + 'orderItems', + 'status', + { + type: Sequelize.ENUM, + allowNull: false, + values: ["Pending", "Delivered", "Cancelled"], + defaultValue: 'Pending' + }, + ), + ]); + }, + + down(queryInterface, Sequelize) { + return Promise.all([ + queryInterface.removeColumn('orderItems', 'status'), + queryInterface.removeColumn('orderItems', 'userId'), + ]); + }, +}; \ No newline at end of file diff --git a/src/sequelize/models/orderItems.ts b/src/sequelize/models/orderItems.ts index 26f9e13..d88d986 100644 --- a/src/sequelize/models/orderItems.ts +++ b/src/sequelize/models/orderItems.ts @@ -1,6 +1,9 @@ import { DataTypes, Model } from "sequelize"; import sequelize from "../../config/dbConnection"; import Product from "./products"; +import Order from "./orders"; +import User from "./users" +import Profile from "./profiles"; export interface orderItemAttributes { @@ -56,5 +59,10 @@ OrderItem.belongsTo(Product, { foreignKey: "productId", as: "product" }); +OrderItem.belongsTo(User,{ + foreignKey: "userId", + as: "user" +}) + export default OrderItem; \ No newline at end of file diff --git a/src/sequelize/models/orders.ts b/src/sequelize/models/orders.ts index b09b543..316024a 100644 --- a/src/sequelize/models/orders.ts +++ b/src/sequelize/models/orders.ts @@ -2,6 +2,7 @@ import { Model, DataTypes } from "sequelize"; import sequelize from "../../config/dbConnection"; import User from "./users"; import OrderItem from "./orderItems"; +import Product from "./products"; export interface OrderAttributes { @@ -38,7 +39,7 @@ Order.init({ }, status: { type: DataTypes.ENUM, - values: ["pending", "delivered", "canceled"], + values: ["Pending", "Delivered", "Cancelled"], defaultValue: "pending", allowNull: false, }, @@ -62,5 +63,8 @@ Order.init({ Order.belongsTo(User, { foreignKey: "buyerId", as: "buyer" }); User.hasMany(Order, { foreignKey: "buyerId", as: "orders" }); Order.hasMany(OrderItem, { foreignKey: "orderId", as: "items" }); +Order.hasMany(OrderItem, { foreignKey: "status", as: "statuses"}) +Order.hasMany(Product, { foreignKey: "id", as: "products"}) +OrderItem.belongsTo(Order, { foreignKey: 'orderId', as: 'order' }); export default Order \ No newline at end of file diff --git a/src/services/OrderService.ts b/src/services/OrderService.ts new file mode 100644 index 0000000..60743d9 --- /dev/null +++ b/src/services/OrderService.ts @@ -0,0 +1,104 @@ +// @ts-nocheck +import { Request, Response } from "express"; +import OrderItem from "../sequelize/models/orderItems"; +import Order from "../sequelize/models/orders"; +import User from "../sequelize/models/users"; +import Product from "../sequelize/models/products"; +import { authStatus } from "../utils/isSellerOrNormalUser"; +import { Role } from "../sequelize/models/roles"; +import Profile from "../sequelize/models/profiles"; + +export const getOrderBuyer = async (req: Request, res: Response) => { + try { + await authStatus(req, res); + // @ts-ignore + const { roleId, id } = req.user; + const role = await Role.findByPk(roleId); + + if (role?.name === "seller") { + const products = await Product.findAll({ + where: { userId: id }, + }); + const productIds = products.map((product) => product.id); + + const sellerOrders = await OrderItem.findAll({ + where: { productId: productIds }, + include: [ + { + model: Order, + as: "order", + include: [{ model: User, as: "buyer", include: [{ model: Profile, as: "profile" }] }], + }, + { model: User, as: "user" }, + { model: Product, as: "product" }, + ], + }); + + const groupedOrders = sellerOrders.reduce((acc, item) => { + const { orderId } = item; + if (!acc[orderId]) { + acc[orderId] = { + order: item.order, + products: [], + }; + } + acc[orderId].products.push(item); + return acc; + }, {}); + + return Object.values(groupedOrders); + } else { + const buyerOrders = await Order.findAll({ + where: { buyerId: id }, + include: [ + { + model: OrderItem, + as: "items", + include: [{ model: Product, as: "product" }], + }, + { model: User, as: "buyer", include: [{ model: Profile, as: "profile" }] }, + ], + }); + + const groupedOrders = buyerOrders.map((order) => ({ + order, + products: order.items, + })); + + return groupedOrders; + } + } catch (error: any) { + throw new Error(error.message); + } +}; + +export const updateOrderStatusService = async (userId: string, orderId: string, status: string) => { + const user = await User.findByPk(userId); + if (user?.roleId !== 2) { + throw new Error("Unauthorized to update order status"); + } + + const orderItems = await OrderItem.findAll({ + where: { orderId }, + include: [ + { + model: Product, + as: "product", + where: { userId }, + }, + { + model: Order, + as: "order", + }, + ], + }); + + if (!orderItems.length) { + throw new Error("Order items not found or not associated with this seller"); + } + for (const item of orderItems) { + item.status = status; + await item.save(); + } + return orderItems; +}; diff --git a/src/services/payment.service.ts b/src/services/payment.service.ts index 31edbcf..a7259ff 100644 --- a/src/services/payment.service.ts +++ b/src/services/payment.service.ts @@ -57,8 +57,10 @@ export const placeOrder = async (cart: CartAttributes) => { const cartItem = await OrderItem.create( { orderId: order.id as number, + // @ts-ignore + userId: item.product.userId , productId: item.productId, - quantity: item.quantity + quantity: item.quantity, } ) }; diff --git a/src/types.ts b/src/types.ts index 5701d6e..9d26638 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,7 +17,8 @@ export enum SUBJECTS { REQUEST_2FA = "Request for 2FA", VERIFY_LOGIN = "Verify that It's you", DISABLE_2FA = "Disable 2-Factor Authentication", - PAYMENT_CONFIRMATION = "Payment Confirmation" + PAYMENT_CONFIRMATION = "Payment Confirmation", + ORDER_STATUS_UPDATED = "Status order updated." } export enum STATUS { @@ -26,6 +27,10 @@ export enum STATUS { FAILED = "Failed", } +export interface UserId { + id: string +} + export interface ProductType{ id?:number, name:string,