cart module refactoring

master
aliMohtarami 1 month ago
parent 725412c59c
commit 89051142da
  1. 13
      src/products/dto/cart/add-to-cart.dto.ts
  2. 8
      src/products/dto/cart/update-cart.dto.ts
  3. 0
      src/products/dto/products/create-product.dto.ts
  4. 0
      src/products/dto/products/update-product.dto.ts
  5. 47
      src/products/entities/cart.entity.ts
  6. 90
      src/products/products.controller.ts
  7. 13
      src/products/products.module.ts
  8. 227
      src/products/products.service.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;
}

@ -0,0 +1,8 @@
// update-cart.dto.ts
import { IsInt, Min } from 'class-validator';
export class UpdateCartDto {
@IsInt()
@Min(1)
quantity: number;
}

@ -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<Cart> {
@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";
}

@ -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);
}
}
}

@ -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 {}

@ -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<Product> {
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<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);
}
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);
}
}
}
}

Loading…
Cancel
Save