diff --git a/src/products/dto/cart/add-to-cart.dto.ts b/src/products/dto/cart/add-to-cart.dto.ts new file mode 100644 index 0000000..0df51ee --- /dev/null +++ b/src/products/dto/cart/add-to-cart.dto.ts @@ -0,0 +1,13 @@ +// add-to-cart.dto.ts +import { isInt, IsInt, IsNotEmpty, IsNumber, min, Min } from 'class-validator'; + +export class AddToCartDto { + @IsInt() + @IsNotEmpty() + productId: number; + + @IsInt() + @IsNotEmpty() + @Min(1) + quantity: number; +} diff --git a/src/products/dto/cart/update-cart.dto.ts b/src/products/dto/cart/update-cart.dto.ts new file mode 100644 index 0000000..1cab8bb --- /dev/null +++ b/src/products/dto/cart/update-cart.dto.ts @@ -0,0 +1,8 @@ +// update-cart.dto.ts +import { IsInt, Min } from 'class-validator'; + +export class UpdateCartDto { + @IsInt() + @Min(1) + quantity: number; +} diff --git a/src/products/dto/create-product.dto.ts b/src/products/dto/products/create-product.dto.ts similarity index 100% rename from src/products/dto/create-product.dto.ts rename to src/products/dto/products/create-product.dto.ts diff --git a/src/products/dto/update-product.dto.ts b/src/products/dto/products/update-product.dto.ts similarity index 100% rename from src/products/dto/update-product.dto.ts rename to src/products/dto/products/update-product.dto.ts diff --git a/src/products/entities/cart.entity.ts b/src/products/entities/cart.entity.ts new file mode 100644 index 0000000..79816c3 --- /dev/null +++ b/src/products/entities/cart.entity.ts @@ -0,0 +1,47 @@ +import { Model, Table, Column, ForeignKey, BelongsTo, DataType } from "sequelize-typescript"; +import { User } from "../../users/entities/user.entity"; +import { Product } from "../../products/entities/product.entity"; +import { Invoice } from "src/invoice/entities/invoice.entity"; + +@Table +export class Cart extends Model { + @ForeignKey(() => User) + @Column + userId: number; + + @BelongsTo(() => User, { onDelete: "CASCADE" }) + user: User; + + @ForeignKey(() => Product) + @Column + productId: number; + + @BelongsTo(() => Product, { onDelete: "CASCADE" }) + product: Product; + + @ForeignKey(() => Invoice) + @Column + invoiceId: number; + + @BelongsTo(() => Invoice, { onDelete: "CASCADE" }) + invoice: Invoice; + + @Column({ + type: DataType.INTEGER, + allowNull: true, + }) + quantity: number; + + @Column({ + type: DataType.INTEGER, + allowNull: true, + }) + productPrice: number; + + @Column({ + type: DataType.ENUM("open", "closed"), + allowNull: false, + defaultValue: "open", + }) + status: "open" | "closed"; +} diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts index 687a2ff..918e980 100644 --- a/src/products/products.controller.ts +++ b/src/products/products.controller.ts @@ -1,18 +1,21 @@ -import { Controller, Get, Post, Body, Param, Delete, Query, Put, UseGuards } from "@nestjs/common"; +import { Controller, Get, Post, Body, Param, Delete, Query, Put, UseGuards, Request, Patch, HttpException, HttpStatus } from "@nestjs/common"; import { ProductsService } from "./products.service"; import { Product } from "./entities/product.entity"; -import { CreateProductDto } from "./dto/create-product.dto"; -import { UpdateProductDto } from "./dto/update-product.dto"; +import { CreateProductDto } from "./dto/products/create-product.dto"; +import { UpdateProductDto } from "./dto/products/update-product.dto"; import { RoleGuard } from "src/guard/role.guard"; +import { AddToCartDto } from "./dto/cart/add-to-cart.dto"; +import { JwtAuthGuard } from "src/guard/auth.guard"; +import { UpdateCartDto } from "./dto/cart/update-cart.dto"; -@Controller("products") +@Controller("shop") export class ProductsController { constructor(private readonly productsService: ProductsService) {} ////////////////////////////////////////products//////////////////////////////////////// //create a new product (admin) @UseGuards(RoleGuard) - @Post() + @Post("product") async create(@Body() createProductDto: CreateProductDto) { const product = await this.productsService.create(createProductDto); return { @@ -21,34 +24,83 @@ export class ProductsController { }; } //list of all product - @Get() + @Get("product") async findAll(@Query() query: { search?: string; priceMin?: number; priceMax?: number }) { const { search, priceMin, priceMax } = query; return this.productsService.findAll(search, priceMin, priceMax); } //get a product detail - @Get(":id") - async findOne(@Param("id") id: string){ + @Get("product/:id") + async findOne(@Param("id") id: string) { return this.productsService.findOne(id); } //edit a product info (admin) @UseGuards(RoleGuard) - @Put(":id") - async update( - @Param("id") id: string, - @Body() updateProductDto: UpdateProductDto - ){ + @Put("product/:id") + async update(@Param("id") id: string, @Body() updateProductDto: UpdateProductDto) { const product = await this.productsService.update(id, updateProductDto); return { - message : 'product updated successful', - product - } + message: "product updated successful", + product, + }; } //delete a product (admin) @UseGuards(RoleGuard) - @Delete(':id') - async remove(@Param('id') id: string): Promise<{ message: string }> { + @Delete("product/:id") + async remove(@Param("id") id: string): Promise<{ message: string }> { return this.productsService.remove(id); } - + ////////////////////////////////////////////cart/////////////////////////////////////////////////// + //create and a item to cart by user + @UseGuards(JwtAuthGuard) + @Post("cart") + async createAndAddItemToCart(@Body() addToCartDto: AddToCartDto, @Request() req: any) { + const userId = req.user.id; + return this.productsService.createAndAddItemToCart({ ...addToCartDto, userId }); + } + //get user cart items + @UseGuards(JwtAuthGuard) + @Get("cart") + async getUserOpenCart(@Request() req: any) { + const userId = req.user.id; + return this.productsService.getUserOpenCart(userId); + } + //edit quantity an item in cart by user + @UseGuards(JwtAuthGuard) + @Patch("cart/:productId") + async updateCart(@Param("productId") productId: number, @Body() updateCartDto: UpdateCartDto, @Request() req: any) { + const userId = req.user.id; + const updatedCart = await this.productsService.updateCart(userId, productId, updateCartDto.quantity); + return { + message: "Cart updated successfully", + updatedCart, + }; + } + //delete an item from cart by user + @UseGuards(JwtAuthGuard) + @Delete("cart/:productId") + async removeFromCart(@Param("productId") productId: number, @Request() req: any) { + const userId = req.user.id; + return await this.productsService.removeFromCart(userId, productId); + } + //clear whole cart by user + @UseGuards(JwtAuthGuard) + @Get("cart/clear-cart") + async clearCart(@Request() req: any) { + const userId = req.user.id; + return await this.productsService.clearCart(userId); + } + //get checkout process + @UseGuards(JwtAuthGuard) + @Get("cart/checkout") + async processOrder(@Request() req: any){ + const userId = req.user.id; + try { + const totalAmount = (await this.productsService.getUserOpenCart(userId)).totalPrice + const result = await this.productsService.processOrder(userId, totalAmount); + return result; + } catch (error) { + throw new HttpException(error.message || "Order processing failed.", HttpStatus.INTERNAL_SERVER_ERROR); + } + } } diff --git a/src/products/products.module.ts b/src/products/products.module.ts index 00ab141..22ac746 100644 --- a/src/products/products.module.ts +++ b/src/products/products.module.ts @@ -5,15 +5,22 @@ import { SequelizeModule } from "@nestjs/sequelize"; import { Product } from "./entities/product.entity"; import { RoleGuard } from "src/guard/role.guard"; import { JwtModule } from "@nestjs/jwt"; +import { Cart } from "./entities/cart.entity"; +import { JwtAuthGuard } from "src/guard/auth.guard"; +import { Invoice } from "src/invoice/entities/invoice.entity"; +import { InvoiceModule } from "src/invoice/invoice.module"; +import { WalletModule } from "src/wallet/wallet.module"; @Module({ - imports: [SequelizeModule.forFeature([Product]), + imports: [SequelizeModule.forFeature([Product,Cart,Invoice]), JwtModule.register({ secret: process.env.JWT_SECRET, signOptions: { expiresIn: '1h' }, - }) + }), + WalletModule, + InvoiceModule ], controllers: [ProductsController], - providers: [ProductsService,RoleGuard], + providers: [ProductsService,RoleGuard,JwtAuthGuard], }) export class ProductsModule {} diff --git a/src/products/products.service.ts b/src/products/products.service.ts index 44bd52f..3cb8baa 100644 --- a/src/products/products.service.ts +++ b/src/products/products.service.ts @@ -1,15 +1,25 @@ import { Injectable } from "@nestjs/common"; import { InjectModel } from "@nestjs/sequelize"; import { Product } from "./entities/product.entity"; -import { CreateProductDto } from "./dto/create-product.dto"; -import { UpdateProductDto } from "./dto/update-product.dto"; +import { CreateProductDto } from "./dto/products/create-product.dto"; +import { UpdateProductDto } from "./dto/products/update-product.dto"; import { Op } from "sequelize"; import { HttpException, HttpStatus } from "@nestjs/common"; +import { Cart } from "./entities/cart.entity"; +import { Invoice } from "src/invoice/entities/invoice.entity"; +import { InvoiceService } from "src/invoice/invoice.service"; +import { WalletService } from "src/wallet/WalletService"; @Injectable() export class ProductsService { - constructor(@InjectModel(Product) private readonly productModel: typeof Product) {} - + constructor( + @InjectModel(Product) private readonly productModel: typeof Product, + @InjectModel(Cart) private readonly cartModel: typeof Cart, + @InjectModel(Invoice) private readonly invoiceModel: typeof Invoice, + private invoiceService: InvoiceService, + private walletService: WalletService, + ) {} + ///////////////////////////////////////////products////////////////////////////////////////////// // create a new product async create(createProductDto: CreateProductDto): Promise { try { @@ -26,8 +36,8 @@ export class ProductsService { const newProduct = await this.productModel.create(createProductDto); return newProduct; } catch (error) { - if(error instanceof HttpException){ - throw error + if (error instanceof HttpException) { + throw error; } throw new HttpException("An error occurred while creating or updating the product.", HttpStatus.INTERNAL_SERVER_ERROR); } @@ -115,7 +125,7 @@ export class ProductsService { try { const { name, description, price, imageUrl, tags, quantity, brand, color, category } = updateProductDto; - let updated = false; + let updated = false; if (name && name !== product.name) { product.name = name; @@ -160,8 +170,8 @@ export class ProductsService { return product; } catch (error) { - if(error instanceof HttpException){ - throw error + if (error instanceof HttpException) { + throw error; } throw new HttpException("An error occurred while updating the product.", HttpStatus.INTERNAL_SERVER_ERROR); } @@ -187,4 +197,203 @@ export class ProductsService { throw new HttpException("An unexpected error occurred while deleting the product.", HttpStatus.INTERNAL_SERVER_ERROR); } } + ////////////////////////////////////////////cart///////////////////////////////////////////////// + //create and add item to a cart + async createAndAddItemToCart(addToCartDto: { userId: number; productId: number; quantity: number }): Promise<{ message: string; cartItem: Cart }> { + const { userId, productId, quantity } = addToCartDto; + + if (!userId || !productId || !quantity || isNaN(Number(quantity)) || Number(quantity) <= 0) { + throw new HttpException("Invalid parameters: userId, productId, and a positive quantity are required.", HttpStatus.BAD_REQUEST); + } + const product = await this.productModel.findByPk(productId); + if (!product) { + throw new HttpException("Product not found!", HttpStatus.NOT_FOUND); + } + if (product.quantity < quantity) { + throw new HttpException("Product quantity insufficient!", HttpStatus.CONFLICT); + } + try { + let invoice = await this.invoiceModel.findOne({ where: { userId, status: "pending" } }); + if (!invoice) { + invoice = await this.invoiceService.createInvoiceFromCart(userId); + } + const invoiceId = invoice.id; + + let cart = await this.cartModel.findOne({ where: { userId, productId, status: "open" } }); + + if (!cart) { + cart = await this.cartModel.create({ + userId, + productId, + invoiceId, + quantity, + productPrice: product.price, + status: "open", + }); + await cart.save(); + } else { + cart.quantity += Number(quantity); + await cart.save(); + } + + await this.invoiceService.updateTotalPayment(userId); + + return { + message: cart.id ? "Product quantity updated in cart successfully!" : "Product added to cart successfully!", + cartItem: cart, + }; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException("An unexpected error occurred while adding the product to cart. Please try again later.", HttpStatus.INTERNAL_SERVER_ERROR); + } + } + // Get user's cart + async getUserOpenCart(userId: number): Promise<{ cartItems: Cart[]; totalPrice: number }> { + if (!userId) { + throw new HttpException("User ID is required.", HttpStatus.BAD_REQUEST); + } + + try { + const cartItems = await this.cartModel.findAll({ + where: { userId, status: "open" }, + include: [ + { + model: Product, + attributes: ["name", "price"], + }, + ], + }); + + if (!cartItems || cartItems.length === 0) { + return { cartItems: [], totalPrice: 0 }; + } + + const totalPrice = cartItems.reduce((sum, item) => { + return sum + (Number(item.productPrice) * item.quantity || 0); + }, 0); + + return { cartItems, totalPrice }; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException("An unexpected error occurred while fetching the cart. Please try again later.", HttpStatus.INTERNAL_SERVER_ERROR); + } + } + // Update cart item quantity + async updateCart(userId: number, productId: number, quantity: number): Promise { + const cartItem = await this.cartModel.findOne({ where: { userId, productId, status: "open" } }); + + if (!cartItem) { + throw new HttpException("Product not found in the cart.", HttpStatus.NOT_FOUND); + } + + const product = await this.productModel.findByPk(productId); + + if (!product) { + throw new HttpException("Product not found.", HttpStatus.NOT_FOUND); + } + + if (product.quantity < quantity) { + throw new HttpException("Insufficient product quantity.", HttpStatus.CONFLICT); + } + + try { + cartItem.quantity = quantity; + await cartItem.save(); + await this.invoiceService.updateTotalPayment(userId); + return cartItem; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException("An unexpected error occurred while updating the cart. Please try again later.", HttpStatus.INTERNAL_SERVER_ERROR); + } + } + // Remove an item from cart + async removeFromCart(userId: number, productId: number): Promise<{ message: string; cartItem: Cart }> { + const cartItem = await this.cartModel.findOne({ where: { userId, productId, status: "open" } }); + if (!cartItem) { + throw new HttpException("Product not found in the cart.", HttpStatus.NOT_FOUND); + } + try { + await cartItem.destroy(); + await this.invoiceService.updateTotalPayment(userId); + return { message: "Item deleted from your cart successfully.", cartItem }; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException("An unexpected error occurred while removing the item from the cart. Please try again later.", HttpStatus.INTERNAL_SERVER_ERROR); + } + } + //delete whole cart by user + async clearCart(userId: number) { + await this.cartModel.destroy({ + where: { userId, status: "open" }, + }); + return { message: "Cart cleared successfully" }; + }//order + async processOrder(userId: number, totalAmount: number): Promise<{ message: string; invoice: Invoice }> { + try { + const carts = await this.cartModel.findAll({ where: { userId, status: "open" } }); + if (!carts || carts.length === 0) { + throw new HttpException("No open carts found for this user.", HttpStatus.NOT_FOUND); + } + + let invoice: Invoice | null = null; + for (const cart of carts) { + const invoiceId = cart.invoiceId; + invoice = await this.invoiceModel.findOne({ where: { id: invoiceId, userId } }); + + if (invoice && invoice.status === "paid") { + return { + message: `Order for cart ID ${cart.id} has already been processed.`, + invoice, + }; + } + } + + await this.walletService.processPayment(userId, totalAmount); + + for (const cartItem of carts) { + const { productId, quantity } = cartItem; + + const product = await this.productModel.findOne({ where: { id: productId } }); + + if (!product) { + throw new HttpException(`Product with ID ${productId} not found.`, HttpStatus.NOT_FOUND); + } + + if (product.quantity < quantity) { + throw new HttpException(`Insufficient stock for product ID ${productId}.`, HttpStatus.BAD_REQUEST); + } + + product.quantity -= quantity; + await product.save(); + } + + for (const cart of carts) { + cart.status = "closed"; + await cart.save(); + } + + if (invoice) { + invoice.status = "paid"; + await invoice.save(); + } + + return { message: "Order processed successfully!", invoice }; + } catch (error) { + console.error(error); + if (error instanceof HttpException) { + throw error; + } else { + throw new HttpException(`An error occurred while processing the order: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + } + }