From 2a6c966d9fea5c269549b40e35ee5d7abc53b1da Mon Sep 17 00:00:00 2001 From: JeanIrad Date: Mon, 3 Jun 2024 09:03:47 +0200 Subject: [PATCH] [finishes #187713856] bug fix in adding product to Cart --- src/controllers/cartController.ts | 222 ++++++++++-------- src/controllers/productsController.ts | 2 +- src/controllers/userController.ts | 2 + .../migrations/20240514174638-cart-product.js | 10 + src/database/models/Product.ts | 2 - src/database/models/Size.ts | 2 - src/database/models/cartsProducts.ts | 58 +++++ src/helpers/stockSizeManagers.ts | 22 ++ src/routes/cartRoute.ts | 10 +- src/routes/categoryRouter.ts | 2 +- 10 files changed, 223 insertions(+), 109 deletions(-) create mode 100644 src/database/models/cartsProducts.ts create mode 100644 src/helpers/stockSizeManagers.ts diff --git a/src/controllers/cartController.ts b/src/controllers/cartController.ts index cd115549..28710d85 100644 --- a/src/controllers/cartController.ts +++ b/src/controllers/cartController.ts @@ -1,110 +1,103 @@ import { Request, Response } from 'express'; import { Cart } from '../database/models/cart'; -import { Product, ProductAttributes } from '../database/models/Product'; +import { Product } from '../database/models/Product'; import User from '../database/models/user'; -import { sendInternalErrorResponse, validateFields } from '../validations'; +import { sendInternalErrorResponse } from '../validations'; import sequelize from '../database/models'; import logger from '../logs/config'; import { Size } from '../database/models/Size'; +import CartsProducts from '../database/models/cartsProducts'; +import { validateFields } from '../validations'; +import { Transaction } from 'sequelize'; +import { checkStockSize, updateStock } from '../helpers/stockSizeManagers'; const addCartItem = async (req: Request, res: Response): Promise => { - const { productId, sizeId } = req.body; + const { productId, sizeId, quantity } = req.body; const { id: userId } = req.user as User; + let transaction: Transaction | null = null; + // validate the required fields + const requiredFields = validateFields(req, ['productId', 'sizeId']); + if (requiredFields.length > 0) { + res.status(400).json({ ok: false, message: `Required fields: ${requiredFields} ` }); + return; + } try { - // Validate request body - const missingFields = validateFields(req, ['productId', 'sizeId']); - if (missingFields.length > 0) { - res.status(400).json({ - ok: false, - message: `Following required fields are missing: ${missingFields.join(', ')}`, - }); + // check if the product exist, given the productId + transaction = await sequelize.transaction(); + const product = await Product.findByPk(productId, { transaction }); + if (!product) { + res.status(404).json({ ok: false, message: 'Product not found' }); return; } - - // Start transaction - const transaction = await sequelize.transaction(); - - try { - // Check if the product exists + let size: Size | null = null; + // check if the product size exist + size = await Size.findOne({ where: { productId, id: sizeId }, transaction }); + if (!size) { + res.status(404).json({ ok: false, message: 'Size not found for this product' }); + return; + } + const stock = await checkStockSize(sizeId, productId, quantity); + if (stock <= 0) { const product = await Product.findByPk(productId, { - include: { - model: Size, - as: 'sizes', - where: { id: sizeId }, - attributes: ['quantity', 'price'], - }, - transaction, - }); - if (!product) { - res.status(404).json({ ok: false, message: 'Product was not found in stock!' }); - return; - } - - // Check if the item already exists in the cart - const cartItem: any = await Cart.findOne({ - include: [ - { - model: User, - as: 'user', - attributes: ['id', 'firstName', 'lastName', 'email', 'phoneNumber', 'gender'], - }, - { - model: Product, - as: 'products', - through: { attributes: ['quantity'] }, - }, - ], - where: { userId }, - transaction: transaction, + include: [{ model: Size, as: 'sizes', where: { id: sizeId }, attributes: ['quantity'] }], }); - if (cartItem) { - // Check if the product being added is already in the cart - const existingProduct = cartItem.products.find((product: ProductAttributes) => product.id === productId); - if (existingProduct) { - // If the product already exists in the cart, increment the quantity in the join table - await sequelize.query( - 'UPDATE "CartsProducts" SET quantity = quantity + 1 WHERE "cartId" =? AND "productId" =?', - { - replacements: [cartItem.id, product.id], - } - ); - } else { - // If the product doesn't exist in the cart, add it to the cart - await cartItem.addProducts(product, { - through: { quantity: 1 }, - transaction: transaction, - }); - } + res + .status(404) + .json({ ok: false, message: `Our stock has ${product?.sizes[0].quantity} product(s) of this size only!` }); + return; + } + await updateStock(sizeId, productId, stock); + // Find the cart for the current user + const cart = await Cart.findOne({ where: { userId }, transaction }); + if (cart) { + // Check if product already exists in cart (optional for quantity update) + let existingCartItem: CartsProducts | null = null; + if (sizeId) { + existingCartItem = await CartsProducts.findOne({ + where: { cartId: cart.id, productId: productId, sizeId: sizeId }, + transaction, + }); } else { - // If the cart is empty, create a new cart item - const newCartItem: any = await Cart.create({ userId }, { transaction }); - await newCartItem.addProducts(product, { - through: { quantity: 1 }, + existingCartItem = await CartsProducts.findOne({ + where: { cartId: cart.id, productId }, transaction, }); } + // Update quantity if product already exists, otherwise create a new entry + if (existingCartItem) { + existingCartItem.quantity += quantity ?? 1; + await existingCartItem.save({ transaction }); + } else { + await CartsProducts.create({ + cartId: cart.id, + productId, + sizeId, + quantity: quantity ?? 1, + }); + } + } else { + const newCart = await Cart.create( + { + userId, + }, + { transaction } + ); - // Commit the transaction - await transaction.commit(); - res.status(200).json({ ok: true, message: 'Product added to cart successfully' }); - } catch (error) { - // Rollback the transaction if an error occurs - await transaction.rollback(); - throw error; + await CartsProducts.create({ cartId: newCart.id, productId, sizeId, quantity: quantity ?? 1 }, { transaction }); } + await transaction.commit(); + res.status(200).json({ message: 'Product added to cart successfully' }); + return; } catch (error) { - // Handle any unexpected errors - console.error(error); - // res.status(500).json({ ok: false, message: 'An unexpected error occurred' }); - sendInternalErrorResponse(res, error); + await transaction?.rollback(); + sendInternalErrorResponse(res, error instanceof Error ? error.message : error); + return; } }; -//updating an item - const updateCartItem = async (req: Request, res: Response): Promise => { - const { productId, quantity } = req.body; + const { productId, quantity, sizeId } = req.body; const { id: userId } = req.user as User; const transaction = await sequelize.transaction(); @@ -119,10 +112,13 @@ const updateCartItem = async (req: Request, res: Response): Promise => { return; } if (quantity >= 1) { - await sequelize.query('UPDATE "CartsProducts" SET quantity =? WHERE "cartId" =? AND "productId" =?', { - replacements: [quantity, cartItem.id, productId], - transaction, - }); + await sequelize.query( + 'UPDATE "CartsProducts" SET quantity =? WHERE "cartId" =? AND "productId" =? and "sizeId" =?', + { + replacements: [quantity, cartItem.id, productId, sizeId], + transaction, + } + ); await transaction.commit(); res.status(200).json({ ok: true, message: 'Cart updated successfully' }); @@ -154,7 +150,7 @@ const getCartItems = async (req: Request, res: Response): Promise => { { model: Size, as: 'sizes', - attributes: ['quantity', 'price'], + attributes: ['size', 'price'], }, ], }, @@ -165,23 +161,52 @@ const getCartItems = async (req: Request, res: Response): Promise => { res.status(404).json({ ok: false, message: 'Cart not found' }); return; } - - const products = cart.products.map((product: any) => { + const cartsProducts = await CartsProducts.findAll({ where: { cartId: cart.id }, transaction }); + const allProducts = Promise.all( + cartsProducts.map(async item => { + return { + product: await Product.findOne({ + where: { id: item.productId }, + attributes: ['id', 'name', 'sellerId', 'images'], + include: [ + { + model: Size, + as: 'sizes', + where: { + id: item.sizeId, + }, + attributes: ['price', 'size', 'id'], + }, + ], + transaction, + }), + quantity: item.quantity, + }; + }) + ); + if ((await allProducts).length < 1) { + res.status(404).json({ ok: false, message: 'No Product in the Cart' }); + return; + } + const sendProducts = [...(await allProducts)].map(item => { + if (!item.product) return null; + const { id, name, sellerId, images, sizes } = item.product; return { - cartId: cart.id, - id: product.id, - name: product.name, - description: product.description, - quantity: product.CartsProducts, - image: product.images[0], - sellerId: product.sellerId, - createdAt: product.createdAt, + id, + name, + sizes, + sellerId, + image: images[0], + quantity: item.quantity, }; }); - res.status(200).json({ cart: products }); + await transaction.commit(); + res.status(200).json({ ok: true, cartId: cart.id, cartProducts: sendProducts }); + return; } catch (error) { await transaction.rollback(); - throw error; + logger.error(error); + sendInternalErrorResponse(res, error instanceof Error ? error.message : error); } }; @@ -205,6 +230,7 @@ const clearCart = async (req: Request, res: Response): Promise => { await sequelize.query('DELETE FROM "CartsProducts" WHERE "cartId" =?', { replacements: [cart.id], transaction }); await transaction.commit(); res.status(200).json({ ok: true, message: 'Cart cleared successfully' }); + return; } catch (error) { await transaction.rollback(); logger.error(error); diff --git a/src/controllers/productsController.ts b/src/controllers/productsController.ts index 76a0f6da..00fd53b2 100644 --- a/src/controllers/productsController.ts +++ b/src/controllers/productsController.ts @@ -260,7 +260,7 @@ export const getAllProduct = async (req: Request, res: Response) => { ); return { ...product.toJSON(), - Sizes: validSizes, + // Sizes: validSizes, }; }); diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index 941cb152..356c5b74 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -169,6 +169,8 @@ export const editUserRole = async (req: Request, res: Response) => { res.status(404).json({ ok: false, error: 'User request not found' }); return; } + const role = await Role.findByPk(roleId); + if (!role) return res.status(404).json({ ok: false, message: `There is no Role with this id: ${roleId}` }); await user.update({ RoleId: roleId }); await requestedUser.update({ status: 'approved' }); diff --git a/src/database/migrations/20240514174638-cart-product.js b/src/database/migrations/20240514174638-cart-product.js index 2b759c9c..eaacc8dd 100644 --- a/src/database/migrations/20240514174638-cart-product.js +++ b/src/database/migrations/20240514174638-cart-product.js @@ -24,6 +24,16 @@ module.exports = { onUpdate: 'CASCADE', onDelete: 'CASCADE', }, + sizeId: { + type: Sequelize.UUID, + defaultValue: Sequelize.UUIDV4, + references: { + model: 'sizes', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, quantity: { type: Sequelize.INTEGER, allowNull: false, diff --git a/src/database/models/Product.ts b/src/database/models/Product.ts index 0b745139..49155d3f 100644 --- a/src/database/models/Product.ts +++ b/src/database/models/Product.ts @@ -4,7 +4,6 @@ import { UUIDV4 } from 'sequelize'; import { Category } from './Category'; import { Size } from './Size'; import User from './user'; -import Cart from './cart'; export interface ProductAttributes { id?: string; @@ -79,4 +78,3 @@ Product.init( Product.belongsTo(Category, { foreignKey: 'categoryId' }); Product.belongsTo(User, { foreignKey: 'sellerId', as: 'user' }); Product.hasMany(Size, { foreignKey: 'productId', as: 'sizes' }); -// Product.hasMany(Cart, { foreignKey: 'productId', as: 'carts' }); diff --git a/src/database/models/Size.ts b/src/database/models/Size.ts index 0920dcff..ef0652e3 100644 --- a/src/database/models/Size.ts +++ b/src/database/models/Size.ts @@ -1,6 +1,5 @@ import { Model, Optional, DataTypes, UUIDV4 } from 'sequelize'; import sequelize from './index'; - export interface SizeAttributes { id: number; size?: string; @@ -31,7 +30,6 @@ Size.init( type: DataTypes.UUID, defaultValue: UUIDV4, primaryKey: true, - autoIncrement: true, }, size: { type: DataTypes.STRING, diff --git a/src/database/models/cartsProducts.ts b/src/database/models/cartsProducts.ts new file mode 100644 index 00000000..e9fcb8e8 --- /dev/null +++ b/src/database/models/cartsProducts.ts @@ -0,0 +1,58 @@ +import { Model, DataTypes } from 'sequelize'; +import sequelize from './index'; +import Cart from './cart'; +import { Product } from './Product'; +import { Size } from './Size'; + +class CartsProducts extends Model { + public cartId!: string; + public productId!: string; + public sizeId!: string; + public quantity!: number; +} + +CartsProducts.init( + { + cartId: { + type: DataTypes.UUID, + primaryKey: true, + references: { + model: Cart, + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + productId: { + type: DataTypes.UUID, + primaryKey: true, + references: { + model: Product, + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + sizeId: { + type: DataTypes.UUID, + primaryKey: true, + references: { + model: Size, + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + quantity: { + type: DataTypes.INTEGER, + allowNull: false, + }, + }, + { + sequelize, + modelName: 'CartsProducts', + timestamps: true, + } +); + +export default CartsProducts; diff --git a/src/helpers/stockSizeManagers.ts b/src/helpers/stockSizeManagers.ts new file mode 100644 index 00000000..f1b156e4 --- /dev/null +++ b/src/helpers/stockSizeManagers.ts @@ -0,0 +1,22 @@ +import { Product } from '../database/models/Product'; +import { Size } from '../database/models/Size'; + +// function to check the current stock size, note that this function should be applied whenever the order has been successfully paid +export const updateStock = async (sizeId: string, productId: string, newQuantity = 0) => { + const size = await Size.findOne({ where: { id: sizeId, productId } }); + if (!size) throw new Error('size not found'); + if (newQuantity === 0) { + await size.destroy(); + } + size.quantity = newQuantity; + await size.save(); +}; +export const checkStockSize = async (sizeId: string, productId: string, quantity = 1): Promise => { + const product = await Product.findByPk(productId, { + include: [{ model: Size, as: 'sizes', where: { id: sizeId }, attributes: ['quantity'] }], + }); + if (!product) throw new Error('Product Not Available'); + // Note that I am getting a unique size which is one of the many sizes, I am allowed to grab the first since that's the only one I have + const result = product.sizes[0].quantity > quantity ? product.sizes[0].quantity - quantity : -1; + return result; +}; diff --git a/src/routes/cartRoute.ts b/src/routes/cartRoute.ts index 705dda87..92219c0a 100644 --- a/src/routes/cartRoute.ts +++ b/src/routes/cartRoute.ts @@ -2,15 +2,15 @@ import express from 'express'; import { addCartItem, updateCartItem, getCartItems, clearCart } from '../controllers/cartController'; -import { isAuthenticated } from '../middlewares/authMiddlewares'; +import { checkUserRoles, isAuthenticated } from '../middlewares/authMiddlewares'; const cartRouter = express.Router(); cartRouter .route('/') - .get(isAuthenticated, getCartItems) - .post(isAuthenticated, addCartItem) - .delete(isAuthenticated, clearCart); -cartRouter.route('/:id').patch(isAuthenticated, updateCartItem); + .get([isAuthenticated, checkUserRoles('buyer')], getCartItems) + .post([isAuthenticated, checkUserRoles('buyer')], addCartItem) + .delete([isAuthenticated, checkUserRoles('buyer')], clearCart); +cartRouter.route('/:id').patch([isAuthenticated, checkUserRoles('buyer')], updateCartItem); export default cartRouter; diff --git a/src/routes/categoryRouter.ts b/src/routes/categoryRouter.ts index a81af66c..fdf10033 100644 --- a/src/routes/categoryRouter.ts +++ b/src/routes/categoryRouter.ts @@ -8,4 +8,4 @@ export const categoryRouter = express.Router(); categoryRouter .route('/') .post(isAuthenticated, checkUserRoles('admin'), createCategory) - .get(isAuthenticated, checkUserRoles('admin'), getAllCategories); + .get(isAuthenticated, checkUserRoles('seller'), getAllCategories);