import { inject, injectable } from "inversify"; import { ClientSession, Types, startSession } from "mongoose"; import { CartRepository, CartShipItemRepo } from "./cart.repository"; import { AddToCartDTO } from "./DTO/addToCart.dto"; import { BulkAddToCartDTO } from "./DTO/bulkAddToCart.dto"; import { CartDTO } from "./DTO/cart.dto"; import { RemoveFromCartDTO } from "./DTO/removeFromCart.dto"; import { UpdateCartDTO } from "./DTO/updateCart.dto"; import { CartMessage, ProductMessage } from "../../common/enums/message.enum"; import { BadRequestError, InternalError } from "../../core/app/app.errors"; import { IOCTYPES } from "../../IOC/ioc.types"; import { ICart } from "./models/Abstraction/ICart"; import { IProduct } from "../product/models/Abstraction/IProduct"; import { ProductRepository } from "../product/Repository/product"; import { ProductVariantRepository } from "../product/Repository/productVarinat"; @injectable() class CartService { @inject(IOCTYPES.CartRepository) cartRepo: CartRepository; @inject(IOCTYPES.CartShipItemRepo) cartShipItemRepo: CartShipItemRepo; @inject(IOCTYPES.ProductRepository) productRepo: ProductRepository; @inject(IOCTYPES.ProductVariantRepository) productVariantRepo: ProductVariantRepository; async getCartS(userId: string) { const doc = (await this.cartRepo.getCartWithAggregate(userId))[0] as ICart; if (!doc) { throw new BadRequestError(CartMessage.CartNotFound); } const cart = CartDTO.transformCart(doc); const cartShipmentItems = await this.cartShipItemRepo.getCartShipmentItems(doc._id.toString()); const recommended = await this.getRecommendedProducts(doc); return { cart: { ...cart, cartShipmentItems }, recommended }; } async clearCart(userId: string) { const deletedCart = await this.cartRepo.model.findOneAndDelete({ user: userId }); if (!deletedCart) throw new BadRequestError(CartMessage.CartNotFound); return { message: CartMessage.ItemDeleted, }; } async addToCartS(addToCartDto: AddToCartDTO, userId: string) { const session = await startSession(); session.startTransaction(); try { const { productId, variantId } = addToCartDto; // Get or create cart let cart = await this.cartRepo.model.findOne({ user: userId }); if (!cart) { cart = (await this.cartRepo.model.create([{ user: userId }], { session }))[0]; } const existingCartItem = cart.items.find((item) => item.product === productId && item.variant.toString() === variantId); const existingQuantity = existingCartItem ? existingCartItem.quantity : 0; await this.validateProduct(productId, variantId, existingQuantity + 1); if (existingCartItem) { // Update quantity if item already exists in the cart existingCartItem.quantity += 1; } else { // Add new item to the cart cart.items.push({ product: productId, quantity: 1, variant: new Types.ObjectId(variantId) }); } await cart.save({ session }); // Commit the transaction await session.commitTransaction(); session.endSession(); return { message: CartMessage.ItemAdded, cart: await this.getCartS(userId), }; } catch (error) { // Rollback any changes made in the transaction await session.abortTransaction(); session.endSession(); throw error; // Re-throw the error to be handled by the calling errorHandler } } async bulkAddToCartS(bulkAddToCartDto: BulkAddToCartDTO, userId: string) { const session = await startSession(); session.startTransaction(); try { const { items } = bulkAddToCartDto; // Get or create cart let cart = await this.cartRepo.model.findOne({ user: userId }); if (!cart) { cart = (await this.cartRepo.model.create([{ user: userId }], { session }))[0]; } // Validate all products before making any changes for (const item of items) { const existingCartItem = cart.items.find( (cartItem) => cartItem.product === item.productId && cartItem.variant.toString() === item.variantId, ); const existingQuantity = existingCartItem ? existingCartItem.quantity : 0; await this.validateProduct(item.productId, item.variantId, existingQuantity + item.quantity); } // Process each item for (const item of items) { const existingCartItem = cart.items.find( (cartItem) => cartItem.product === item.productId && cartItem.variant.toString() === item.variantId, ); if (existingCartItem) { // Update quantity if item already exists in the cart existingCartItem.quantity += item.quantity; } else { // Add new item to the cart cart.items.push({ product: item.productId, quantity: item.quantity, variant: new Types.ObjectId(item.variantId), }); } } await cart.save({ session }); // Commit the transaction await session.commitTransaction(); session.endSession(); return { message: CartMessage.ItemAdded, cart: await this.getCartS(userId), }; } catch (error) { // Rollback any changes made in the transaction await session.abortTransaction(); session.endSession(); throw error; // Re-throw the error to be handled by the calling errorHandler } } async updateCartS(updateCartDto: UpdateCartDTO, userId: string) { const { productId, variantId, quantity } = updateCartDto; const cart = await this.cartRepo.model.findOne({ user: userId }); if (!cart) throw new BadRequestError(CartMessage.CartNotFound); const existItem = cart.items.find((item) => item.product === productId && item.variant.toString() === variantId.toString()); if (!existItem) throw new BadRequestError(CartMessage.ItemNotFound); // validate the stock await this.validateProduct(productId, variantId, quantity); // update the quantity existItem.quantity = quantity; await cart.save(); return { message: CartMessage.CartUpdated, cart: await this.getCartS(userId), }; } async removeFromCartS(removeFromCartDto: RemoveFromCartDTO, userId: string) { const { productId, variantId } = removeFromCartDto; await this.validateProduct(productId, variantId); // Find the cart for the user const cart = await this.cartRepo.model.findOne({ user: userId }); if (!cart) throw new BadRequestError(CartMessage.CartNotFound); // Find the item in the cart const itemIndex = cart.items.findIndex((item) => item.product === productId && item.variant.toString() === variantId.toString()); if (itemIndex === -1) throw new BadRequestError(CartMessage.ItemNotFound); // Remove the item from the cart cart.items.splice(itemIndex, 1); await cart.save(); return { message: CartMessage.ItemDeleted, cart: await this.getCartS(userId), }; } async clearCartAndShipmentItems(userId: string, session: ClientSession) { const cart = await this.cartRepo.model.findOneAndDelete({ user: userId }, { session }); if (cart) { await this.cartShipItemRepo.model.deleteMany({ cart: cart._id }, { session }); } else { throw new InternalError(); } } async getRecommendedProducts(cart: ICart) { if (!cart) return; const items = cart.items as unknown as { product: IProduct }[]; const productIds = items.map((item) => item.product._id); const categoryIds = items.map((item) => item.product.category.toString()); const tags = items.flatMap((item) => item.product.tags); const recommendedProducts = await this.productRepo.getRecommendedProducts(productIds, categoryIds, tags); return recommendedProducts; } ///==============================> helper methods // Helper method to check if the product exists and if the stock is sufficient private async validateProduct(productId: number, variantId: string, totalQuantity?: number) { // Check if product exists const product = await this.productRepo.findById(productId); if (!product) throw new BadRequestError(ProductMessage.ProductNotFound); // Check if product variant exists const productVariant = await this.productVariantRepo.findById(variantId); if (!productVariant) throw new BadRequestError(ProductMessage.ProductNotFound); if (!product.variants.includes(productVariant._id)) throw new BadRequestError(CartMessage.ProductNotBelongToVariant); if (productVariant.product !== product._id) throw new BadRequestError(CartMessage.ProductNotBelongToVariant); if (totalQuantity && totalQuantity > productVariant.price.order_limit) { // Validate order limit throw new BadRequestError(CartMessage.OrderLimitExceeded); } if (totalQuantity && productVariant.stock < totalQuantity) { // Validate stock throw new BadRequestError(CartMessage.StockNotEnough); } return { product, productVariant, }; } } export { CartService };