diff --git a/migrations/20250105085732-create-invoice.js b/migrations/20250105062732-create-invoice.js similarity index 52% rename from migrations/20250105085732-create-invoice.js rename to migrations/20250105062732-create-invoice.js index af58df6..56cc8f9 100644 --- a/migrations/20250105085732-create-invoice.js +++ b/migrations/20250105062732-create-invoice.js @@ -1,10 +1,16 @@ -'use strict'; +"use strict"; module.exports = { - up: async (queryInterface, Sequelize) => { - await queryInterface.dropTable('Invoices', { cascade: true }); - await queryInterface.createTable('Invoices', { + const tableExists = await queryInterface + .describeTable("Invoices") + .then(() => true) + .catch(() => false); + + if (tableExists) { + await queryInterface.dropTable("Invoices", { cascade: true }); + } + await queryInterface.createTable("Invoices", { id: { type: Sequelize.INTEGER, autoIncrement: true, @@ -15,38 +21,34 @@ module.exports = { type: Sequelize.INTEGER, allowNull: false, references: { - model: 'Users', - key: 'id', - }, - onDelete: 'CASCADE', - }, - cartId: { - type: Sequelize.INTEGER, - allowNull: false, - references: { - model: 'Carts', - key: 'id', + model: "Users", + key: "id", }, - onDelete: 'CASCADE', + onDelete: "CASCADE", }, totalPaymentAmount: { type: Sequelize.FLOAT, allowNull: false, }, + status: { + type: Sequelize.ENUM("pending", "paid"), + allowNull: false, + defaultValue: "pending", + }, createdAt: { type: Sequelize.DATE, allowNull: false, - defaultValue: Sequelize.fn('NOW'), + defaultValue: Sequelize.fn("NOW"), }, updatedAt: { type: Sequelize.DATE, allowNull: false, - defaultValue: Sequelize.fn('NOW'), + defaultValue: Sequelize.fn("NOW"), }, }); }, down: async (queryInterface, Sequelize) => { - await queryInterface.dropTable('Invoices'); + await queryInterface.dropTable("Invoices", { cascade: true }); }, }; diff --git a/migrations/20250105063054-create-cart.js b/migrations/20250105063054-create-cart.js index 2dfc15d..02df389 100644 --- a/migrations/20250105063054-create-cart.js +++ b/migrations/20250105063054-create-cart.js @@ -2,14 +2,13 @@ module.exports = { up: async (queryInterface, Sequelize) => { - await queryInterface.dropTable('Carts', { cascade: true }); - + await queryInterface.dropTable("Carts", { cascade: true }); await queryInterface.createTable('Carts', { id: { type: Sequelize.INTEGER, - allowNull: false, autoIncrement: true, primaryKey: true, + allowNull: false, }, userId: { type: Sequelize.INTEGER, @@ -22,38 +21,44 @@ module.exports = { }, productId: { type: Sequelize.INTEGER, - allowNull: false, + allowNull: true, references: { - model: 'Products', + model: 'Products', + key: 'id', + }, + onDelete: 'CASCADE', + }, + invoiceId: { + type: Sequelize.INTEGER, + allowNull: true, + references: { + model: 'Invoices', key: 'id', }, onDelete: 'CASCADE', }, quantity: { type: Sequelize.INTEGER, - allowNull: false, + allowNull: true, }, productPrice: { type: Sequelize.DECIMAL(10, 2), - allowNull: false, - }, - totalPrice: { - type: Sequelize.DECIMAL(10, 2), - allowNull: false, + allowNull: true, }, - productName: { - type: Sequelize.STRING, + status: { + type: Sequelize.ENUM('open', 'closed'), allowNull: false, + defaultValue: 'open', }, createdAt: { type: Sequelize.DATE, allowNull: false, - defaultValue: Sequelize.NOW, + defaultValue: Sequelize.fn('NOW'), }, updatedAt: { type: Sequelize.DATE, allowNull: false, - defaultValue: Sequelize.NOW, + defaultValue: Sequelize.fn('NOW'), }, }); }, diff --git a/src/cart/cart.controller.ts b/src/cart/cart.controller.ts index a4e4831..d4fe4c2 100644 --- a/src/cart/cart.controller.ts +++ b/src/cart/cart.controller.ts @@ -10,11 +10,12 @@ import { Invoice } from "src/invoice/entities/invoice.entity"; export class CartController { constructor(private readonly cartService: CartService) {} + @UseGuards(JwtAuthGuard) @Post() - async addToCart(@Body() addToCartDto: AddToCartDto, @Request() req: any): Promise<{ message: string; cartItem: Cart }> { + async createAndAddItemToCart(@Body() addToCartDto: AddToCartDto, @Request() req: any): Promise<{ message: string; cartItem: Cart }> { const userId = req.user.id; - return this.cartService.addToCart({ ...addToCartDto, userId }); + return this.cartService.createAndAddItemToCart({ ...addToCartDto, userId }); } @UseGuards(JwtAuthGuard) @@ -45,21 +46,17 @@ export class CartController { }; } - @Post(':userId/checkout') - async processOrder( - @Param('userId') userId: number, - @Body('totalAmount') totalAmount: number, - ):Promise<{ message: string; invoices: Invoice}> { + @Post(":userId/checkout") + async processOrder(@Param("userId") userId: number, @Body("totalAmount") totalAmount: number): Promise<{ message: string; invoices: Invoice }> { if (!totalAmount || totalAmount <= 0) { - throw new HttpException('Invalid total amount.', HttpStatus.BAD_REQUEST); + throw new HttpException("Invalid total amount.", HttpStatus.BAD_REQUEST); } try { const result = await this.cartService.processOrder(userId, totalAmount); return result; } catch (error) { - throw new HttpException(error.message || 'Order processing failed.', HttpStatus.INTERNAL_SERVER_ERROR); + throw new HttpException(error.message || "Order processing failed.", HttpStatus.INTERNAL_SERVER_ERROR); } } - } diff --git a/src/cart/cart.service.ts b/src/cart/cart.service.ts index 568a6f8..b24a488 100644 --- a/src/cart/cart.service.ts +++ b/src/cart/cart.service.ts @@ -15,41 +15,74 @@ export class CartService { @Inject(forwardRef(() => InvoiceService)) private invoiceService: InvoiceService, ) {} - - // Add product to cart - async addToCart(addToCartDto: { userId: number; productId: number; quantity: number }): Promise<{ message: string; cartItem: Cart }> { + //create a cart and add item to cart + async createAndAddItemToCart(addToCartDto: { userId: number; productId: number; quantity: number }): Promise<{ message: string; cartItem: Cart }> { const { userId, productId, quantity } = addToCartDto; + try { + 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); + } - if (!userId || !productId || !quantity) { - throw new HttpException("Missing required parameters: userId, productId, and quantity are required.", HttpStatus.BAD_REQUEST); - } + const product = await this.productModel.findByPk(productId); + if (!product) { + throw new HttpException("Product not found!", HttpStatus.NOT_FOUND); + } - const product = await this.productModel.findByPk(productId); - if (!product) { - throw new HttpException("Product not found!", HttpStatus.NOT_FOUND); - } + let cart = await this.cartModel.findOne({ where: { userId, productId, status: "open" } }); - const existingCartItem = await this.cartModel.findOne({ - where: { userId, productId }, - }); + if (!cart) { + cart = await this.cartModel.create({ + userId, + productId, + quantity, + productPrice: product.price, + status: "open", + }); + + const invoice = await this.invoiceService.createInvoiceFromCart(userId); + cart.invoiceId = invoice.id; + await cart.save(); + } else { + const invoice = await this.invoiceService.getInvoiceByUserAndCart(userId); + if (!invoice) { + const newInvoice = await this.invoiceService.createInvoiceFromCart(userId); + cart.invoiceId = newInvoice.id; + await cart.save(); + } + } + + const invoiceId = cart.invoiceId; + + let existingCartItem = await this.cartModel.findOne({ + where: { userId, productId, invoiceId }, + }); - if (existingCartItem) { - existingCartItem.quantity += Number(quantity); - existingCartItem.totalPrice = existingCartItem.quantity * existingCartItem.productPrice; - await existingCartItem.save(); + if (existingCartItem) { + existingCartItem.quantity += Number(quantity); + await existingCartItem.save(); + + return { + message: "Product quantity updated in cart successfully!", + cartItem: existingCartItem, + }; + } + + const newCartItem = await this.cartModel.create({ + userId, + productId, + invoiceId, + quantity, + productPrice: product.price, + }); return { - message: "Product quantity updated in cart successfully!", - cartItem: existingCartItem, + message: "Product added to cart successfully!", + cartItem: newCartItem, }; + } catch (error) { + console.error("Error adding product to cart:", error); + throw new HttpException(error.message || "An error occurred while adding the product to the cart.", HttpStatus.INTERNAL_SERVER_ERROR); } - - const newCartItem = await this.cartModel.create({ userId, productId, quantity, productPrice: product.price, totalPrice: product.price * quantity, productName: product.name }); - - return { - message: "Product added to cart successfully!", - cartItem: newCartItem, - }; } // Get user's cart @@ -63,7 +96,7 @@ export class CartService { include: [ { model: Product, - attributes: ["id", "name", "price", "description", "imageUrl"], + attributes: [], }, ], }); @@ -72,7 +105,7 @@ export class CartService { throw new HttpException("No cart items found for the specified user.", HttpStatus.NOT_FOUND); } - const totalPrice = cartItems.reduce((sum, item) => sum + (Number(item.totalPrice) || 0), 0); + const totalPrice = cartItems.reduce((sum, item) => sum + (Number(item.productPrice * item.quantity) || 0), 0); return { cartItems, totalPrice }; } @@ -86,7 +119,6 @@ export class CartService { } cartItem.quantity = quantity; - cartItem.totalPrice = cartItem.quantity * cartItem.productPrice; await cartItem.save(); return cartItem; } @@ -146,7 +178,7 @@ export class CartService { } // Create the invoices for all cart - const invoices = await this.invoiceService.createInvoiceFromCart(userId,cartId); + const invoices = await this.invoiceService.createInvoiceFromCart(userId); return { message: "Order processed successfully", invoices }; } catch (error) { diff --git a/src/cart/dto/add-to-cart.dto.ts b/src/cart/dto/add-to-cart.dto.ts index 11bd45b..540da4a 100644 --- a/src/cart/dto/add-to-cart.dto.ts +++ b/src/cart/dto/add-to-cart.dto.ts @@ -8,6 +8,6 @@ export class AddToCartDto { @IsInt() @IsNotEmpty() - @Min(0) + @Min(1) quantity: number; } diff --git a/src/cart/entities/cart.entity.ts b/src/cart/entities/cart.entity.ts index c4bfd00..540221c 100644 --- a/src/cart/entities/cart.entity.ts +++ b/src/cart/entities/cart.entity.ts @@ -1,6 +1,7 @@ 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 { @@ -18,28 +19,29 @@ export class Cart extends Model { @BelongsTo(() => Product, { onDelete: "CASCADE" }) product: Product; + @ForeignKey(() => Invoice) + @Column + invoiceId: number; + + @BelongsTo(() => Invoice, { onDelete: "CASCADE" }) + invoice: Invoice; + @Column({ type: DataType.INTEGER, - allowNull: false, + allowNull: true, }) quantity: number; @Column({ type: DataType.DECIMAL(10, 2), - allowNull: false, + allowNull: true, }) productPrice: number; @Column({ - type: DataType.DECIMAL(10, 2), + type: DataType.ENUM("open", "closed"), allowNull: false, + defaultValue: "open", }) - totalPrice: number; - - @Column({ - type: DataType.STRING, - allowNull: false, - }) - productName: string; - + status: "open" | "closed"; } diff --git a/src/invoice/entities/invoice.entity.ts b/src/invoice/entities/invoice.entity.ts index 0cd92a9..c54258c 100644 --- a/src/invoice/entities/invoice.entity.ts +++ b/src/invoice/entities/invoice.entity.ts @@ -5,6 +5,7 @@ import { BelongsTo, DataType, Model, + HasMany, } from "sequelize-typescript"; import { User } from "../../users/entities/user.entity"; import { Cart } from "src/cart/entities/cart.entity"; @@ -18,16 +19,19 @@ export class Invoice extends Model { @BelongsTo(() => User, { onDelete: "CASCADE" }) user: User; - @ForeignKey(() => Cart) - @Column - cartId: number; - - @BelongsTo(() => Cart, { onDelete: "CASCADE" }) - cart: Cart; + @HasMany(() => Cart) + carts: Cart[]; @Column({ type: DataType.FLOAT, allowNull: false, }) totalPaymentAmount: number; + + @Column({ + type: DataType.ENUM("pending", "paid"), + allowNull: false, + defaultValue: "pending", + }) + status: string; } diff --git a/src/invoice/invoice.controller.ts b/src/invoice/invoice.controller.ts index 1d9df8c..cecb688 100644 --- a/src/invoice/invoice.controller.ts +++ b/src/invoice/invoice.controller.ts @@ -4,8 +4,8 @@ import { InvoiceService } from "./invoice.service"; @Controller("invoice") export class InvoiceController { constructor(private readonly invoiceService: InvoiceService) {} - @Get(":userId") - async getInvoices(@Param("userId") userId: number): Promise { - return this.invoiceService.getInvoicesByUser(userId); - } + // @Get(":userId") + // async getInvoices(@Param("userId") userId: number): Promise { + // return this.invoiceService.getInvoicesByUser(userId); + // } } diff --git a/src/invoice/invoice.service.ts b/src/invoice/invoice.service.ts index 50d7d2e..f285055 100644 --- a/src/invoice/invoice.service.ts +++ b/src/invoice/invoice.service.ts @@ -12,12 +12,12 @@ export class InvoiceService { private cartService: CartService, ) {} - async createInvoiceFromCart(userId: number,cartId:number): Promise { + async createInvoiceFromCart(userId: number): Promise { const user = await User.findByPk(userId); if (!user) { throw new HttpException("User not found", HttpStatus.NOT_FOUND); } - + const userCartItems = await this.cartService.getUserCart(userId); if (!userCartItems || userCartItems.cartItems.length === 0) { throw new HttpException("Cart is empty", HttpStatus.BAD_REQUEST); @@ -25,33 +25,47 @@ export class InvoiceService { const invoice = await this.invoiceModel.create({ userId, - cartId, - totalPaymentAmount:userCartItems.totalPrice, - }) - return invoice + totalPaymentAmount: userCartItems.totalPrice, + }); + return invoice; } - - - async getInvoicesByUser(userId: number): Promise { + + async getInvoiceByUserAndCart(userId: number): Promise { try { - if (!userId) { - throw new HttpException("User ID is required.", HttpStatus.BAD_REQUEST); + if (!userId ) { + throw new HttpException("User ID are required.", HttpStatus.BAD_REQUEST); } - const invoices = await this.invoiceModel.findAll({ - where: { userId }, + const invoice = await this.invoiceModel.findOne({ + where: { userId, status:'pending' }, }); - if (!invoices || invoices.length === 0) { - throw new HttpException("No invoices found for this user.", HttpStatus.NOT_FOUND); + if (!invoice) { + throw new HttpException("Invoice not found for this user and cart.", HttpStatus.NOT_FOUND); } - return invoices; + return invoice; } catch (error) { if (error instanceof HttpException) { throw error; } - throw new HttpException("An error occurred while retrieving invoices.", HttpStatus.INTERNAL_SERVER_ERROR); + throw new HttpException("An error occurred while retrieving the invoice.", HttpStatus.INTERNAL_SERVER_ERROR); + } + } + async getInvoiceByUserAndCartOrFalse(userId: number, cartId: number): Promise { + try { + const invoice = await this.invoiceModel.findOne({ + where: { userId, id: cartId }, + }); + + if (!invoice) { + return false; + } + + return invoice; + } catch (error) { + console.error("Error getting invoice:", error); + return false; } } }