247 lines
8.9 KiB
TypeScript
247 lines
8.9 KiB
TypeScript
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 };
|