diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..8a7aad81 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/src/controllers/orderController.ts", + "outFiles": [ + "${workspaceFolder}/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 9b941c1b..80420789 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,10 @@ "migrate": "npx sequelize-cli db:migrate", "migrate:undo": "npx sequelize-cli db:migrate:undo", "migrate:undo:all": "npx sequelize-cli db:migrate:undo:all", + "migrate:undo:all": "npx sequelize-cli db:migrate:undo:all", "seed": "npx sequelize-cli db:seed:all", - "seed:undo": "npx sequelize-cli db:seed:undo:all" + "seed:undo": "npx sequelize-cli db:seed:undo", + "seed:undo:all": "npx sequelize-cli db:seed:undo:all" }, "keywords": [], "author": "", @@ -101,4 +103,4 @@ "eslint --fix" ] } -} \ No newline at end of file +} diff --git a/src/controllers/cartController.ts b/src/controllers/cartController.ts new file mode 100644 index 00000000..2f526ebb --- /dev/null +++ b/src/controllers/cartController.ts @@ -0,0 +1,225 @@ +import { Request, Response } from 'express'; +import { Cart } from '../database/models/cart'; +import { Product, ProductAttributes } from '../database/models/Product'; +import User from '../database/models/user'; +import { sendInternalErrorResponse, validateFields } from '../validations'; +import sequelize from '../database/models'; +import logger from '../logs/config'; +import { Size } from '../database/models/Size'; + +const addCartItem = async (req: Request, res: Response): Promise => { + const { productId, sizeId } = req.body; + const { id: userId } = req.user as User; + try { + // Validate request body + const missingFields = validateFields(req, ['productId']); + if (missingFields.length > 0) { + res.status(400).json({ + ok: false, + message: `Following required fields are missing: ${missingFields.join(', ')}`, + }); + return; + } + + // Start transaction + const transaction = await sequelize.transaction(); + + try { + // Check if the product exists + const product = await Product.findByPk(productId, { + include: { + model: Size, + as: 'Sizes', + attributes: ['quantity'], + }, + 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, + }); + + 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) { + const [currentCartSize] = await sequelize.query('SELECT quantity from "CartsProducts" where "productId" =?', { + replacements: [productId], + }); + const quantity = (currentCartSize[0] as any).quantity; + const stockSize = (product as any).Sizes[0].quantity; + if (quantity < stockSize) { + // 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, productId], + } + ); + } else { + throw new Error('No more same products in stock'); + return; + } + } else { + // If the product doesn't exist in the cart, add it to the cart + await cartItem.addProducts(product, { + through: { quantity: 1 }, + transaction: 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 }, + 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; + } + } catch (error) { + // Handle any unexpected errors + console.error(error); + // res.status(500).json({ ok: false, message: 'An unexpected error occurred' }); + sendInternalErrorResponse(res, error); + } +}; + +//updating an item + +const updateCartItem = async (req: Request, res: Response): Promise => { + const { productId, quantity } = req.body; + const { id: userId } = req.user as User; + + const transaction = await sequelize.transaction(); + try { + const cartItem: any = await Cart.findOne({ + where: { userId }, + transaction, + }); + + if (!cartItem) { + res.status(404).json({ ok: false, message: 'Cart not found' }); + return; + } + if (quantity >= 1) { + await sequelize.query('UPDATE "CartsProducts" SET quantity =? WHERE "cartId" =? AND "productId" =?', { + replacements: [quantity, cartItem.id, productId], + transaction, + }); + + await transaction.commit(); + res.status(200).json({ ok: true, message: 'Cart updated successfully' }); + } else { + throw new Error('Quantity cannot be less than 1'); + } + } catch (error) { + await transaction.rollback(); + logger.error(error); + sendInternalErrorResponse(res, error); + } +}; + +//getting items of a cart + +const getCartItems = async (req: Request, res: Response): Promise => { + const { id: userId } = req.user as User; + + const transaction = await sequelize.transaction(); + try { + const cart: any = await Cart.findOne({ + where: { userId }, + include: [ + { + model: Product, + as: 'products', + through: { attributes: ['quantity'] }, + include: { + model: Size, + as: 'Sizes', + attributes: ['quantity'], + }, + }, + ], + transaction, + }); + + if (!cart) { + res.status(404).json({ ok: false, message: 'Cart not found' }); + return; + } + const [productIds] = await sequelize.query( + 'SELECT "productId" from "CartsProducts" where "cartId" =? AND "userId" =?', + { replacements: [cart.id, userId], transaction } + ); + const products = cart.products.map((product: any) => { + return { + id: product.id, + name: product.name, + description: product.description, + quantity: product.CartsProducts, + image: product.images[0], + sellerId: product.sellerId, + createdAt: product.createdAt, + }; + }); + res.status(200).json({ cart: products }); + } catch (error) { + await transaction.rollback(); + throw error; + } +}; + +//clear cart + +const clearCart = async (req: Request, res: Response): Promise => { + const { id: userId } = req.user as User; + + const transaction = await sequelize.transaction(); + try { + const cart = await Cart.findOne({ + where: { userId }, + transaction, + }); + + if (!cart) { + res.status(404).json({ ok: false, message: 'Cart not found' }); + return; + } + + 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' }); + } catch (error) { + await transaction.rollback(); + logger.error(error); + sendInternalErrorResponse(res, error); + } +}; + +export { addCartItem, updateCartItem, getCartItems, clearCart }; diff --git a/src/controllers/categoriesController.ts b/src/controllers/categoriesController.ts index 49b00bf2..b3a31a61 100644 --- a/src/controllers/categoriesController.ts +++ b/src/controllers/categoriesController.ts @@ -17,3 +17,14 @@ export const createCategory = async (req: Request, res: Response) => { res.status(500).json({ error: 'Failed to create category' }); } }; +export const getAllCategories = async (req: Request, res: Response) => { + try { + const categories = await Category.findAll(); + res.status(200).json({ ok: true, data: categories }); + } catch (error) { + if (error instanceof Error) { + logger.error(error.message); + } + res.status(500).json({ error: 'Failed to fetch categories' }); + } +}; diff --git a/src/controllers/orderController.ts b/src/controllers/orderController.ts new file mode 100644 index 00000000..f0cafe1f --- /dev/null +++ b/src/controllers/orderController.ts @@ -0,0 +1,147 @@ +import logger from '../logs/config'; +import { Request, Response } from 'express'; +import Order from '../database/models/order'; +import { sendInternalErrorResponse, validateFields } from '../validations'; +import sequelize from '../database/models/index'; +import { Transaction } from 'sequelize'; +import User from '../database/models/user'; +import { Product } from '../database/models/Product'; +import { Size } from '../database/models/Size'; +import Cart from '../database/models/cart'; + +export const createOrder = async (req: Request, res: Response): Promise => { + const transaction: Transaction = await sequelize.transaction(); + const { id: userId } = req.user as User; + try { + const missingFields = validateFields(req, ['orderItemIds', 'shippingAddress1', 'city', 'country', 'phone']); + if (missingFields.length !== 0) { + res.status(400).json({ ok: false, message: `the following fields are required: ${missingFields.join(',')}` }); + return; + } + const { shippingAddress1, shippingAddress2, orderItemIds, country, city, phone, zipCode } = req.body; + const cart = await Cart.findOne({ where: { userId }, transaction }); + if (!cart) { + res.status(404).json({ ok: false, message: 'No cart found for this user!' }); + return; + } + const [orderItems] = await sequelize.query('SELECT "productId", quantity from "CartsProducts" where "cartId" = ?', { + replacements: [cart.id], + transaction, + }); + const subTotalPrices = Promise.all( + orderItems.map(async (item: any) => { + const price = ( + (await Product.findOne({ + where: { id: item.productId }, + include: { model: Size, as: 'Sizes', attributes: ['price'] }, + transaction, + })) as any + ).Sizes[0].price; + return price * item.quantity; + }) + ); + const totalPrice = (await subTotalPrices).reduce((prev: number, cur: number) => prev + cur) as number; + const order = await Order.create( + { + phone, + shippingAddress1, + shippingAddress2, + country, + city, + zipCode, + orderItemIds, + userId, + totalPrice, + }, + { transaction } + ); + transaction.commit(); + res.status(201).json({ ok: true, message: 'Order created successfully', order }); + } catch (error) { + logger.error('error creating order'); + logger.error(error); + transaction.rollback(); + sendInternalErrorResponse(res, error); + return; + } +}; +export const getAllOrders = async (req: Request, res: Response): Promise => { + const transaction: Transaction = await sequelize.transaction(); + try { + const orders = await Order.findAll({ + attributes: ['id', 'totalPrice', 'country', 'city', 'phone'], + include: [ + { + model: User, + as: 'user', + attributes: ['email', 'phoneNumber', 'firstName', 'lastName'], + }, + { + model: Cart, + as: 'Carts', + attributes: ['id'], + }, + ], + transaction, + }); + + if (orders.length === 0) { + res.status(404).json({ + ok: false, + message: 'No orders found', + }); + } + const [orderItems] = await sequelize.query('SELECT "productId", quantity from "CartsProducts" where "cartId" = ?', { + replacements: [orders.map((order: any) => order.Carts.id)], + transaction, + }); + const orderedProducts = Promise.all( + orderItems.map(async (item: any) => { + const productsAndQuantity = []; + const product = await Product.findOne({ + where: { id: item.productId }, + include: { + model: Size, + as: 'Sizes', + }, + }); + productsAndQuantity.push({ product, quantity: item.quantity }); + return productsAndQuantity; + }) + ); + + res.status(200).json({ ok: true, data: { orders, orderItems: await orderedProducts } }); + } catch (error) { + logger.error(error); + sendInternalErrorResponse(res, error); + return; + } +}; +export const deleteOrder = async (req: Request, res: Response) => { + try { + const order = await Order.findByPk(req.params.id); + if (!order) { + res.status(404).json({ ok: false, message: `No order found with id: ${req.params.id}` }); + return; + } + await Order.destroy({ where: { id: req.params.id } }); + res.status(200).json({ ok: true, message: 'Order deleted successfully!' }); + } catch (error) { + logger.error(error); + sendInternalErrorResponse(res, error); + } +}; +export const updateOrder = async (req: Request, res: Response) => { + try { + const order = await Order.findByPk(req.params.id); + if (!order) { + res.status(404).json({ ok: false, message: `No order found with id: ${req.params.id}` }); + return; + } + await Order.update(req.body, { where: { id: req.params.id } }); + res.status(200).json({ ok: true, message: 'Order updated successfully!' }); + } catch (error) { + logger.error(error); + sendInternalErrorResponse(res, error); + } +}; diff --git a/src/controllers/productsController.ts b/src/controllers/productsController.ts index 3292113b..65a39b49 100644 --- a/src/controllers/productsController.ts +++ b/src/controllers/productsController.ts @@ -61,8 +61,10 @@ export const createProduct = async (req: Request, res: Response) => { export const createSize = async (req: Request, res: Response) => { try { const { productId } = req.params; - const { size, price, discount, expiryDate } = req.body as SizeAttributes; - await Size.create({ size, price, discount, expiryDate, productId }); + const { size, price, discount, expiryDate, quantity } = req.body as SizeAttributes; + + const result = await Size.create({ size, price, discount, expiryDate, productId, quantity }); + console.log('results', result); res.status(201).json({ ok: true, message: 'Product size added successfully', @@ -237,7 +239,7 @@ export const getAllProduct = async (req: Request, res: Response) => { // Filter products and remove expired sizes while keeping non-expired ones const availableProducts = products.map((product: { Sizes: any[]; toJSON: () => any }) => { const validSizes = product.Sizes.filter( - (size: { expiryDate: string | number | Date }) => new Date(size.expiryDate) > currentDate + (size: { expiryDate: string | number | Date }) => new Date(size.expiryDate) <= currentDate ); return { ...product.toJSON(), diff --git a/src/controllers/profileController.ts b/src/controllers/profileController.ts index bc516933..64393143 100644 --- a/src/controllers/profileController.ts +++ b/src/controllers/profileController.ts @@ -6,7 +6,7 @@ import Role from '../database/models/role'; import { sendInternalErrorResponse } from '../validations'; export const getUserProfile = async (req: Request, res: Response): Promise => { - const userId: string = req.params.userId; + const userId: string = req.params.userId ? req.params.userId : (req.user as User).id; try { const user = await User.findByPk(userId, { @@ -18,7 +18,7 @@ export const getUserProfile = async (req: Request, res: Response): Promise }); if (!user) { - res.status(404).json({ message: 'User not found' }); + res.status(404).json({ ok: false, message: 'User not found' }); return; } diff --git a/src/controllers/roleControllers.ts b/src/controllers/roleControllers.ts index aa19e33d..5c31d282 100644 --- a/src/controllers/roleControllers.ts +++ b/src/controllers/roleControllers.ts @@ -32,6 +32,8 @@ const createRole = async (req: Request, res: Response): Promise => { await transaction.rollback(); throw err; } + const createdRole = await Role.create({ name }); + res.status(201).json({ ok: true, data: createdRole }); } catch (error) { logger.error(error); sendInternalErrorResponse(res, error); diff --git a/src/controllers/sellerRequestController.ts b/src/controllers/sellerRequestController.ts index 4eafb853..639c3ea1 100644 --- a/src/controllers/sellerRequestController.ts +++ b/src/controllers/sellerRequestController.ts @@ -21,11 +21,11 @@ export const createSellerRequest = async (req: Request, res: Response): Promise< return; } if (!req.files || +req.files.length < 6) { - res.status(400).json({ message: 'Please upload all required documents(6)' }); + res.status(400).json({ ok: false, message: 'Please upload all required documents(6)' }); return; } if (req.body.agreement !== 'true') { - res.status(400).json({ message: 'Please agree to the terms and conditions' }); + res.status(400).json({ ok: false, message: 'Please agree to the terms and conditions' }); return; } const documents = await fileUploadService(req); diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index 9ae9af53..f7ff9670 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -8,6 +8,7 @@ import Role from '../database/models/role'; import { sendEmail } from '../helpers/send-email'; import { sendInternalErrorResponse, validateEmail, validateFields, validatePassword } from '../validations'; import { passwordEncrypt } from '../helpers/encrypt'; +import getDefaultRole from '../helpers/defaultRoleGenerator'; // Function for user signup export const signupUser = async (req: Request, res: Response) => { @@ -50,6 +51,7 @@ export const signupUser = async (req: Request, res: Response) => { password: hashPassword, gender, phoneNumber, + RoleId: await getDefaultRole(), }); const createdUser = newUser.dataValues; diff --git a/src/database/migrations/20240426195145-create-category.js b/src/database/migrations/20240426195145-create-category.js index 3f39ece2..535d4db7 100644 --- a/src/database/migrations/20240426195145-create-category.js +++ b/src/database/migrations/20240426195145-create-category.js @@ -17,6 +17,7 @@ module.exports = { name: { type: Sequelize.STRING, allowNull: false, + unique: true, }, description: { type: Sequelize.TEXT, diff --git a/src/database/migrations/20240510102608-create-user-product.js b/src/database/migrations/20240510102608-create-user-product.js new file mode 100644 index 00000000..f4481543 --- /dev/null +++ b/src/database/migrations/20240510102608-create-user-product.js @@ -0,0 +1,50 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('UserProducts', { + id: { + type: Sequelize.UUID, + defaultValue: Sequelize.UUIDV4, + primaryKey: true, + }, + userId: { + type: Sequelize.UUID, + allowNull: false, + references: { + model: 'Users', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + productId: { + type: Sequelize.UUID, + allowNull: false, + references: { + model: 'products', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + quantity: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 1, + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + }, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('UserProducts'); + }, +}; diff --git a/src/database/migrations/20240513164738-cart-model.js b/src/database/migrations/20240513164738-cart-model.js new file mode 100644 index 00000000..c321c8c7 --- /dev/null +++ b/src/database/migrations/20240513164738-cart-model.js @@ -0,0 +1,37 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('Carts', { + id: { + type: Sequelize.UUID, + primaryKey: true, + allowNull: false, + defaultValue: Sequelize.UUIDV4, + }, + userId: { + type: Sequelize.UUID, + allowNull: false, + references: { + model: 'Users', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + }, + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('Carts'); + }, +}; diff --git a/src/database/migrations/20240514160800-orders.js b/src/database/migrations/20240514160800-orders.js new file mode 100644 index 00000000..af5d0eaf --- /dev/null +++ b/src/database/migrations/20240514160800-orders.js @@ -0,0 +1,76 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('orders', { + id: { + type: Sequelize.UUID, + defaultValue: Sequelize.UUIDV4, + primaryKey: true, + }, + orderItemIds: { + type: Sequelize.UUID, + allowNull: false, + references: { + model: 'Carts', + key: 'id', + }, + }, + status: { + type: Sequelize.ENUM('pending', 'delivered', 'cancelled'), + defaultValue: 'pending', + }, + shippingAddress1: { + type: Sequelize.STRING, + allowNull: false, + }, + shippingAddress2: { + type: Sequelize.STRING, + allowNull: true, + }, + phone: { + type: Sequelize.STRING, + allowNull: false, + }, + zipCode: { + type: Sequelize.STRING, + allowNull: true, + }, + city: { + type: Sequelize.STRING, + allowNull: false, + }, + country: { + type: Sequelize.STRING, + allowNull: false, + }, + userId: { + type: Sequelize.UUID, + allowNull: false, + references: { + model: 'Users', + key: 'id', + }, + }, + totalPrice: { + type: Sequelize.FLOAT, + allowNull: false, + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: new Date(), + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: new Date(), + }, + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('orders'); + }, +}; diff --git a/src/database/migrations/20240514174638-cart-product.js b/src/database/migrations/20240514174638-cart-product.js new file mode 100644 index 00000000..2b759c9c --- /dev/null +++ b/src/database/migrations/20240514174638-cart-product.js @@ -0,0 +1,45 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('CartsProducts', { + cartId: { + type: Sequelize.UUID, + defaultValue: Sequelize.UUIDV4, + references: { + model: 'Carts', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + productId: { + type: Sequelize.UUID, + defaultValue: Sequelize.UUIDV4, + references: { + model: 'products', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + quantity: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 1, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('CartsProducts'); + }, +}; diff --git a/src/database/models/Category.ts b/src/database/models/Category.ts index 9063f94a..8226ac10 100644 --- a/src/database/models/Category.ts +++ b/src/database/models/Category.ts @@ -26,6 +26,7 @@ Category.init( name: { type: DataTypes.STRING, allowNull: false, + unique: true, }, description: { type: DataTypes.TEXT, diff --git a/src/database/models/Product.ts b/src/database/models/Product.ts index 4a1feb5a..55f0c8aa 100644 --- a/src/database/models/Product.ts +++ b/src/database/models/Product.ts @@ -4,6 +4,7 @@ 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; @@ -75,6 +76,6 @@ Product.init( ); Product.belongsTo(Category, { foreignKey: 'categoryId' }); -Product.belongsTo(User, { foreignKey: 'sellerId', as: 'Users' }); - -Product.hasMany(Size, { foreignKey: 'productId' }); +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/cart.ts b/src/database/models/cart.ts new file mode 100644 index 00000000..8e7a1b2f --- /dev/null +++ b/src/database/models/cart.ts @@ -0,0 +1,53 @@ +import { Model, DataTypes, Optional } from 'sequelize'; +import User from './user'; +import { Product } from './Product'; +import sequelize from '.'; + +interface CartAttributes { + id: string; + userId: string; +} +export interface CartCreationAttributes extends Optional {} + +export class Cart extends Model implements CartAttributes { + public id!: string; + public userId!: string; +} +Cart.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + allowNull: false, + defaultValue: DataTypes.UUIDV4, + }, + userId: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'Users', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + }, + { + sequelize, + modelName: 'Cart', + timestamps: true, + } +); +Cart.belongsTo(User, { + foreignKey: 'userId', + as: 'user', +}); + +Cart.belongsToMany(Product, { + through: 'CartsProducts', + foreignKey: 'cartId', + otherKey: 'productId', + as: 'products', +}); + +export default Cart; diff --git a/src/database/models/order.ts b/src/database/models/order.ts index e69de29b..23cfe4eb 100644 --- a/src/database/models/order.ts +++ b/src/database/models/order.ts @@ -0,0 +1,118 @@ +import { DataTypes, Model, Optional, UUIDV4 } from 'sequelize'; +import sequelize from './index'; +import User from './user'; +import { Product } from './Product'; +import Cart from './cart'; + +interface OrderAttributes { + id: string; + orderItemIds: string; + status?: string; // pending delivered cancelled + createdAt?: Date; + updatedAt?: Date; + shippingAddress1?: string; + shippingAddress2?: string; + phone?: string; + city?: string; + country?: string; + userId: string; + zipCode?: string; + totalPrice: number; +} + +interface OrderCreationAttributes extends Optional {} + +class Order extends Model implements OrderAttributes { + public id!: string; + public orderItemIds!: string; + public status!: string; // pending delivered cancelled + public createdAt!: Date; + public updatedAt!: Date; + public shippingAddress1!: string; + public shippingAddress2!: string; + public phone!: string; + public city!: string; + public country!: string; + public userId!: string; + public totalPrice!: number; + public zipCode?: string; +} + +Order.init( + { + id: { + type: DataTypes.UUID, + defaultValue: UUIDV4, + primaryKey: true, + }, + orderItemIds: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'Carts', + key: 'id', + }, + }, + status: { + type: DataTypes.ENUM('pending', 'delivered', 'cancelled'), + defaultValue: 'pending', + }, + shippingAddress1: { + type: DataTypes.STRING, + allowNull: false, + }, + shippingAddress2: { + type: DataTypes.STRING, + allowNull: true, + }, + phone: { + type: DataTypes.STRING, + allowNull: false, + }, + zipCode: { + type: DataTypes.STRING, + allowNull: true, + }, + city: { + type: DataTypes.STRING, + allowNull: false, + }, + country: { + type: DataTypes.STRING, + allowNull: false, + }, + userId: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'Users', + key: 'id', + }, + }, + totalPrice: { + type: DataTypes.FLOAT, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: new Date(), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: new Date(), + }, + }, + + { + sequelize, + modelName: 'Order', + tableName: 'orders', + } +); +Order.belongsTo(User, { foreignKey: 'userId', as: 'user' }); +User.hasMany(Order, { foreignKey: 'userId' }); +Cart.hasMany(Order, { foreignKey: 'orderItemIds', as: 'orders' }); +Order.belongsTo(Cart, { foreignKey: 'orderItemIds', as: 'Carts' }); +export default Order; diff --git a/src/database/models/role.ts b/src/database/models/role.ts index 5f258982..c43fc5fd 100644 --- a/src/database/models/role.ts +++ b/src/database/models/role.ts @@ -27,7 +27,7 @@ Role.init( defaultValue: UUIDV4, }, name: { - type: DataTypes.ENUM('admin', 'buyer', 'guest', 'vendor'), + type: DataTypes.ENUM('admin', 'buyer', 'seller'), allowNull: false, unique: true, set(value: string) { diff --git a/src/database/models/user.ts b/src/database/models/user.ts index 56603240..19fe6b30 100644 --- a/src/database/models/user.ts +++ b/src/database/models/user.ts @@ -135,19 +135,35 @@ User.init( defaultValue: false, }, }, - { sequelize: sequelize, timestamps: true, modelName: 'User' } -); - -User.beforeCreate(async (user: User) => { - try { - const defaultRole = await getDefaultRole(); - user.RoleId = defaultRole; - } catch (error) { - logger.error('Error setting default role:', error); - throw error; + { + sequelize: sequelize, + timestamps: true, + modelName: 'User', + hooks: { + beforeCreate: async (user: User) => { + try { + const defaultRole = await getDefaultRole(); + user.RoleId = defaultRole; + } catch (error) { + logger.error('Error setting default role:', error); + console.log('error setting default role:', error); + throw error; + } + }, + }, } -}); +); +// User.beforeCreate(async (user: User) => { +// try { +// const defaultRole = await getDefaultRole(); +// user.RoleId = defaultRole; +// } catch (error) { +// logger.error('Error setting default role:', error); +// console.log('error setting default role:', error); +// throw error; +// } +// }); User.belongsTo(Role); VendorRequest.belongsTo(User, { foreignKey: 'vendorId' }); User.hasOne(VendorRequest, { foreignKey: 'vendorId' }); diff --git a/src/database/seeders/20240429200629-add-seller-role.js b/src/database/seeders/20240429200629-add-seller-role.js new file mode 100644 index 00000000..68a94ee6 --- /dev/null +++ b/src/database/seeders/20240429200629-add-seller-role.js @@ -0,0 +1,21 @@ +'use strict'; +const { v4: uuidv4 } = require('uuid'); + +/** @type {import('sequelize-cli').Migration} */ + +module.exports = { + up: async (queryInterface, Sequelize) => { + return queryInterface.bulkInsert('Roles', [ + { + id: uuidv4(), + name: 'seller', + createdAt: new Date(), + updatedAt: new Date(), + }, + ]); + }, + + down: async (queryInterface, Sequelize) => { + return queryInterface.bulkDelete('Roles', { name: 'seller' }); + }, +}; diff --git a/src/docs/cart.yaml b/src/docs/cart.yaml new file mode 100644 index 00000000..478e3a82 --- /dev/null +++ b/src/docs/cart.yaml @@ -0,0 +1,55 @@ +paths: + /api/cart: + get: + summary: Get all cart items + tags: + - Cart + security: + - isAuthenticated: [] + responses: + '200': + description: Successful retrieval of cart items + '401': + description: Unauthorized user + + post: + summary: Add item to cart + tags: + - Cart + security: + - isAuthenticated: [] + responses: + '201': + description: Item added to cart successfully + '401': + description: Unauthorized user + + delete: + summary: Clear cart + tags: + - Cart + security: + - isAuthenticated: [] + responses: + '200': + description: Cart cleared successfully + '401': + description: Unauthorized user + + patch: + summary: Update a cart item by ID + tags: + - Cart + security: + - isAuthenticated: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + '200': + description: Cart item updated successfully + '401': + description: Unauthorized user diff --git a/src/docs/orders.yaml b/src/docs/orders.yaml new file mode 100644 index 00000000..6977821b --- /dev/null +++ b/src/docs/orders.yaml @@ -0,0 +1,262 @@ +paths: + /api/orders: + post: + summary: Create an order + description: Creates a new order for the authenticated user + tags: + - Orders + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + orderItemIds: + type: array + items: + type: string + example: ['item1', 'item2'] + shippingAddress1: + type: string + example: '123 Main St' + shippingAddress2: + type: string + example: 'Apt 4B' + country: + type: string + example: 'USA' + city: + type: string + example: 'New York' + phone: + type: string + example: '123-456-7890' + zipCode: + type: string + example: '10001' + required: + - orderItemIds + - shippingAddress1 + - city + - country + - phone + responses: + '201': + description: Order created successfully + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + example: true + message: + type: string + example: 'Order created successfully' + order: + $ref: '#/components/schemas/Order' + '400': + description: Missing required fields + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + example: false + message: + type: string + example: 'the following fields are required: ...' + '404': + description: No cart found for the user + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + example: false + message: + type: string + example: 'No cart found for this user!' + '500': + description: Internal server error + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + example: false + message: + type: string + example: 'error' + + get: + summary: Get all orders + description: Retrieves all orders for the authenticated user + tags: + - Orders + security: + - bearerAuth: [] + responses: + '200': + description: List of orders + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + example: true + data: + type: object + properties: + orders: + type: array + items: + $ref: '#/components/schemas/Order' + orderItems: + type: array + items: + type: object + properties: + product: + $ref: '#/components/schemas/Product' + quantity: + type: integer + example: 2 + '404': + description: No orders found + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + example: false + message: + type: string + example: 'No orders found' + '500': + description: Internal server error + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + example: false + message: + type: string + example: 'No orders found' + + /api/orders/{id}: + delete: + summary: Delete an order + description: Deletes an order by ID + tags: + - Orders + security: + - bearerAuth: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The order ID + responses: + '200': + description: Order deleted successfully + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + example: true + message: + type: string + example: 'Order deleted successfully!' + '404': + description: No order found with the given ID + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + example: false + message: + type: string + example: 'No order found with id: ...' + '500': + description: Internal server error + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + example: false + message: + type: string + example: 'Error deleting Order' + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + + schemas: + Order: + type: object + properties: + id: + type: string + example: 'order1' + totalPrice: + type: number + example: 100.50 + country: + type: string + example: 'USA' + city: + type: string + example: 'New York' + phone: + type: string + example: '123-456-7890' + userId: + type: string + example: 'user1' + shippingAddress1: + type: string + example: '123 Main St' + shippingAddress2: + type: string + example: 'Apt 4B' + zipCode: + type: string + example: '10001' + orderItemIds: + type: array + items: + type: string + example: ['item1', 'item2'] diff --git a/src/helpers/defaultRoleGenerator.ts b/src/helpers/defaultRoleGenerator.ts index 32c9a318..3f58b5e1 100644 --- a/src/helpers/defaultRoleGenerator.ts +++ b/src/helpers/defaultRoleGenerator.ts @@ -4,7 +4,7 @@ import logger from '../logs/config'; const getDefaultRole = async () => { const defaultRole = await Role.findOne({ where: { name: 'buyer' } }); - + console.log('defaultRole:', defaultRole?.id); if (!defaultRole) { logger.error('Default role not found.'); return; diff --git a/src/middlewares/authMiddlewares.ts b/src/middlewares/authMiddlewares.ts index 03634748..a2e93c5f 100644 --- a/src/middlewares/authMiddlewares.ts +++ b/src/middlewares/authMiddlewares.ts @@ -11,7 +11,7 @@ import { userToken } from '../helpers/token.generator'; config(); -export const isAuthenticated = (req: Request, res: Response, next: NextFunction) => { +export const isAuthenticated = async (req: Request, res: Response, next: NextFunction) => { try { const token = req.headers.authorization ?? req.params.token; @@ -20,7 +20,7 @@ export const isAuthenticated = (req: Request, res: Response, next: NextFunction) return res.status(401).json({ message: 'Authentication required.' }); } - jwt.verify(token, process.env.SECRET_KEY!, async (err, decoded: any) => { + await jwt.verify(token, process.env.SECRET_KEY!, async (err, decoded: any) => { if (err) { if (err.name === 'TokenExpiredError') { logger.error('Token has expired.'); diff --git a/src/routes/cartRoute.ts b/src/routes/cartRoute.ts new file mode 100644 index 00000000..705dda87 --- /dev/null +++ b/src/routes/cartRoute.ts @@ -0,0 +1,16 @@ +import express from 'express'; + +import { addCartItem, updateCartItem, getCartItems, clearCart } from '../controllers/cartController'; + +import { 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); + +export default cartRouter; diff --git a/src/routes/categoryRouter.ts b/src/routes/categoryRouter.ts index 9853930b..a81af66c 100644 --- a/src/routes/categoryRouter.ts +++ b/src/routes/categoryRouter.ts @@ -1,8 +1,11 @@ /* eslint-disable @typescript-eslint/no-misused-promises */ import express from 'express'; import { checkUserRoles, isAuthenticated } from '../middlewares/authMiddlewares'; -import { createCategory } from '../controllers/categoriesController'; +import { createCategory, getAllCategories } from '../controllers/categoriesController'; export const categoryRouter = express.Router(); -categoryRouter.post('/create-category', isAuthenticated, checkUserRoles('admin'), createCategory); +categoryRouter + .route('/') + .post(isAuthenticated, checkUserRoles('admin'), createCategory) + .get(isAuthenticated, checkUserRoles('admin'), getAllCategories); diff --git a/src/routes/index.ts b/src/routes/index.ts index 73ae50b2..64d4aa2a 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -6,12 +6,15 @@ import authRoute from './authRoute'; import roleRoute from './roleRoute'; import productRouter from './productRoutes'; import { categoryRouter } from './categoryRouter'; +import cartRouter from './cartRoute'; + import wishlistRoute from './wishlistRoute'; import notificationRoutes from './notificationRoutes'; import { permissionRoute } from './permissionRoute'; import { sellerRequestRouter } from './sellerRequestRoute'; import chatRoute from './chatRoute'; +import orderRouter from './orderRoute'; const router = Router(); router.use('/chats', chatRoute); @@ -21,9 +24,11 @@ router.use('/auth', authRoute); router.use('/roles', roleRoute); router.use('/products', productRouter); router.use('/category', categoryRouter); +router.use('/cart', cartRouter); router.use('/wishlist', wishlistRoute); router.use('/notifications', notificationRoutes); router.use('/permissions', permissionRoute); router.use('/vendor-requests', sellerRequestRouter); +router.use('/orders', orderRouter); export default router; diff --git a/src/routes/orderRoute.ts b/src/routes/orderRoute.ts new file mode 100644 index 00000000..fbe91a72 --- /dev/null +++ b/src/routes/orderRoute.ts @@ -0,0 +1,14 @@ +import { Router } from 'express'; +import { checkUserRoles, isAuthenticated } from '../middlewares/authMiddlewares'; +import { getAllOrders, createOrder, deleteOrder, updateOrder } from '../controllers/orderController'; +const orderRouter = Router(); + +orderRouter + .route('/') + .get(isAuthenticated, checkUserRoles('buyer'), getAllOrders) + .post(isAuthenticated, checkUserRoles('buyer'), createOrder); +orderRouter + .route('/:id') + .delete(isAuthenticated, checkUserRoles('admin'), deleteOrder) + .patch(isAuthenticated, checkUserRoles('buyer'), updateOrder); +export default orderRouter; diff --git a/src/routes/userRoute.ts b/src/routes/userRoute.ts index f596f62b..4b6e221c 100644 --- a/src/routes/userRoute.ts +++ b/src/routes/userRoute.ts @@ -18,7 +18,7 @@ import { checkUserRoles, isAuthenticated } from '../middlewares/authMiddlewares' const router = Router(); router.post('/signup', signupUser); -router.get('/:page?', isAuthenticated, getAllUser); +router.get('/:page?', isAuthenticated, checkUserRoles('admin'), getAllUser); router.get('/user/:id', isAuthenticated, getOneUser); router.delete('/:id', isAuthenticated, checkUserRoles('admin'), deleteUser); router.patch('/edit/:id', isAuthenticated, multerUpload.single('profileImage'), editUser);