import { inject, injectable } from "inversify"; import { ClientSession, FilterQuery, isValidObjectId, startSession } from "mongoose"; import slugify from "slugify"; import { PaginationDTO } from "../../../common/dto/pagination.dto"; import { CategoryThemeEnum } from "../../../common/enums/category.enum"; import { AdMessage, AttributeMessage, CartMessage, CategoryMessage, CommonMessage, ProductMessage, ShopMessage, } from "../../../common/enums/message.enum"; import { CreateProductStep, ProductDiscountType, ProductMarketStatus, ProductStatus } from "../../../common/enums/product.enum"; import { Question_CommentStatus } from "../../../common/enums/question_comment.enum"; import { CompareSearchQueries, FilterByStatusProductsQueries, ProductsCommentsQueries, ProductsQuestionsQueries, VariantQueries, } from "../../../common/types/query.type"; import { BadRequestError, ForbiddenError } from "../../../core/app/app.errors"; import { PriceChangeEvent } from "../../../events/priceChange"; import { IOCTYPES } from "../../../IOC/ioc.types"; import { paginationUtils } from "../../../utils/pagination.utils"; import { TimeService } from "../../../utils/time.service"; import { AddIncredibleOffersParamDto } from "../../admin/DTO/product-param.dto"; import { BrandDTO } from "../../brand/DTO/brand.dto"; import { AttributeValueRepo, CategoryAttributeRepo, CategoryRepository } from "../../category/category.repository"; import { CategoryTreeDTO } from "../../category/DTO/category.dto"; import { NotificationService } from "../../notification/notification.service"; import { PricingRepository } from "../../pricing/pricing.repository"; import { SellerProductQueryDTO } from "../../seller/DTO/sellerProductQuery.dto"; import { SellerRepository } from "../../seller/seller.repository"; import { OwnerRef } from "../../shop/models/Abstraction/IShop"; import { ShopRepo } from "../../shop/shop.repository"; import { CommentDTO } from "../../user/DTO/comment.dto"; import { QuestionDTO } from "../../user/DTO/question.dto"; import { WishlistRepo } from "../../user/user.repository"; import { ProductAdDTO } from "../DTO/addProductAd.dto"; import { AddVariantDTO } from "../DTO/addVariant.dto"; import { AddWishlistDTO, RemoveWishlistDTO } from "../DTO/addwishlist.dto"; import { ProductFinalStepDTO, ProductStepOneDTO, ProductStepTwoDTO } from "../DTO/CreateProduct.dto"; import { CreateAnswerQuestionDTO, CreateCommentDTO, CreateQuestionDTO } from "../DTO/createQuestion.dto"; import { CreateReportDTO } from "../DTO/createReport.dto"; import { CreateReportQuestionDTO } from "../DTO/createReportQuestion.dto"; import { AddObserverDTO, RemoveObserverDTO } from "../DTO/observer.dto"; import { ProductDTO, ProductVariantDTO, RejectCommentDTO, SellerPanelProductDTO } from "../DTO/product.dto"; import { AddVoiceToProductDTO, UpdateProductDTO, UpdateProductFinalStepDTO, UpdateProductStepOneDTO, UpdateProductStepTwoDTO, } from "../DTO/updateProduct.dto"; import { ActivateVariantDTO, UpdateVariantDTO } from "../DTO/updateVariant.dto"; import { IComment } from "../models/Abstraction/IComment"; import { IProduct, ISpecifications } from "../models/Abstraction/IProduct"; import { AdType, IProductAd } from "../models/Abstraction/IProductAd"; import { IDimensions, IPrice, IProductVariant } from "../models/Abstraction/IProductVariant"; import { IQuestion } from "../models/Abstraction/IQuestion"; import { ReportQuestionType } from "../models/Abstraction/IReportQuestion"; import { CommentRepository } from "../Repository/comment"; import { IncredibleOffersRepo } from "../Repository/incredibleOffers"; import { PopularProductRepo } from "../Repository/popularProduct"; import { PriceHistoryRepo } from "../Repository/pricehistory"; import { ProductRepository } from "../Repository/product"; import { ProductAdRepo } from "../Repository/productAd"; import { ProductObserveRepo } from "../Repository/productObserve"; import { ProductReportRepo } from "../Repository/productReport"; import { ProductVariantRepository } from "../Repository/productVarinat"; import { QuestionRepository } from "../Repository/question"; import { ReportQuestionRepo } from "../Repository/reportQuestion"; @injectable() class ProductService { @inject(IOCTYPES.SellerRepository) sellerRepo: SellerRepository; @inject(IOCTYPES.ProductRepository) productRepo: ProductRepository; @inject(IOCTYPES.PricingRepo) pricingRepo: PricingRepository; @inject(IOCTYPES.ProductVariantRepository) productVariantRepo: ProductVariantRepository; @inject(IOCTYPES.CategoryRepository) categoryRepo: CategoryRepository; @inject(IOCTYPES.CategoryAttributeRepository) categoryAttRepo: CategoryAttributeRepo; @inject(IOCTYPES.AttributeValueRepository) attributeValueRepo: AttributeValueRepo; @inject(IOCTYPES.QuestionRepository) questionRepo: QuestionRepository; @inject(IOCTYPES.CommentRepository) commentRepo: CommentRepository; @inject(IOCTYPES.ReportQuestionRepo) reportQuestionRepo: ReportQuestionRepo; @inject(IOCTYPES.ProductReportRepo) productReportRepo: ProductReportRepo; @inject(IOCTYPES.PriceHistoryRepo) priceHistoryRepo: PriceHistoryRepo; @inject(IOCTYPES.ProductAdRepo) productAdRepo: ProductAdRepo; @inject(IOCTYPES.PopularProductRepo) popularProductRepo: PopularProductRepo; @inject(IOCTYPES.ProductObserveRepo) productObserveRepo: ProductObserveRepo; @inject(IOCTYPES.WishlistRepo) wishlistRepo: WishlistRepo; @inject(IOCTYPES.ShopRepo) shopeRepo: ShopRepo; @inject(IOCTYPES.IncredibleOffersRepo) incredibleOffersRepo: IncredibleOffersRepo; @inject(IOCTYPES.NotificationService) notificationService: NotificationService; //############################################################### async searchProducts(q: string) { const products = await this.productRepo.model.find( { $or: [ { title_en: { $regex: q, $options: "i" } }, { title_fa: { $regex: q, $options: "i" } }, { model: { $regex: q, $options: "i" } }, ], variants: { $elemMatch: { $exists: true } }, }, { _id: 1, title_fa: 1, title_en: 1, imagesUrl: 1, model: 1, description: 1, variants: 1 }, ); return { products }; } //############################################################### //################# product creation and crud variant ########### async createProductS(createStepOneDto: ProductStepOneDTO, ownerId: string) { await this.checkForDuplicateProduct(createStepOneDto.model); const shop = await this.shopeRepo.model.findOne({ owner: ownerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const createdProduct = await this.productRepo.model.create({ ...createStepOneDto, title_en: createStepOneDto.title_en ?? slugify(createStepOneDto.title_fa), shop: shop._id, }); return { message: ProductMessage.Created, draftProduct: this.mapToDraftProductDTO(createdProduct, "attribute"), }; } //************************************************************** //************************************************************** async createProductStepTwoS(createStepTwoDto: ProductStepTwoDTO, ownerId: string) { //TODO:check if attribute belong to the category of product const session = await startSession(); session.startTransaction(); try { const shop = await this.shopeRepo.model.findOne({ owner: ownerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const product = await this.validateProduct(createStepTwoDto.productId, ownerId); // Ensure the step is correct this.ensureCorrectStep(product.step, CreateProductStep.Attribute); // Initialize an array to hold the specifications const specifications: ISpecifications[] = []; // Loop through each attribute in the DTO for (const attribute of createStepTwoDto.attributes) { const attributeDetails = await this.categoryAttRepo.model.findById(attribute.id); if (!attributeDetails) { throw new BadRequestError(AttributeMessage.AttributeIdIsIncorrect); } // Fetch the attribute values from the database // const attributeValues = await this.attributeValueRepo.model.find({ // _id: { $in: attribute.values }, // }); // Check if any attribute values were found // if (attributeValues.length === 0 ) { // throw new BadRequestError(`No valid attribute values found for attribute id ${attribute.id}`); // } // Map the values to their text value // const selectedValues = attributeValues.map((val: IAttributeValue) => val.text); const selectedValues = attribute.values.map((val) => String(val)); specifications.push({ title: attributeDetails.title, values: selectedValues, }); } // Update the product with the new specifications and other fields const updatedProduct = await this.productRepo.model.findByIdAndUpdate( createStepTwoDto.productId, { specifications, description: createStepTwoDto.description, metaDescription: createStepTwoDto.metaDescription, tags: createStepTwoDto.tags, advantages: createStepTwoDto.advantages, disAdvantages: createStepTwoDto.disAdvantages, step: CreateProductStep.Attribute, }, { new: true, session }, ); await session.commitTransaction(); await session.endSession(); return { message: ProductMessage.CreatedAttribute, draftProduct: this.mapToDraftProductDTO(updatedProduct as IProduct, "image"), }; } catch (error) { await session.abortTransaction(); await session.endSession(); throw error; } } //************************************************************** //************************************************************** async createProductFinalStepS(createFinalStepDto: ProductFinalStepDTO, ownerId: string) { const product = await this.validateProduct(createFinalStepDto.productId, ownerId); this.ensureCorrectStep(product.step, CreateProductStep.Image); const imagesUrl = { cover: createFinalStepDto.coverImage, list: createFinalStepDto.imagesList }; const updatedProduct = await this.productRepo.model.findByIdAndUpdate( createFinalStepDto.productId, { imagesUrl, step: CreateProductStep.Image, status: ProductStatus.Approved, }, { new: true }, ); return { message: ProductMessage.ProductReadyForRelease, updatedProduct, }; } //############################################################### //################# product creation and crud variant ########### async createProductForShopS(createStepOneDto: ProductStepOneDTO, shopId: string) { await this.checkForDuplicateProduct(createStepOneDto.model); const shop = await this.shopeRepo.model.findById(shopId); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const createdProduct = await this.productRepo.model.create({ ...createStepOneDto, title_en: createStepOneDto.title_en ?? slugify(createStepOneDto.title_fa), shop: shop._id, }); return { message: ProductMessage.Created, draftProduct: this.mapToDraftProductDTO(createdProduct, "attribute"), }; } //************************************************************** //************************************************************** async createProductStepTwoForShopS(createStepTwoDto: ProductStepTwoDTO, shopId: string) { //TODO:check if attribute belong to the category of product const session = await startSession(); session.startTransaction(); try { const shop = await this.shopeRepo.model.findById(shopId); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const product = await this.validateProduct(createStepTwoDto.productId, shop.owner.toString()); // Ensure the step is correct this.ensureCorrectStep(product.step, CreateProductStep.Attribute); // Initialize an array to hold the specifications const specifications: ISpecifications[] = []; // Loop through each attribute in the DTO for (const attribute of createStepTwoDto.attributes) { const attributeDetails = await this.categoryAttRepo.model.findById(attribute.id); if (!attributeDetails) { throw new BadRequestError(AttributeMessage.AttributeIdIsIncorrect); } // Fetch the attribute values from the database // const attributeValues = await this.attributeValueRepo.model.find({ // _id: { $in: attribute.values }, // }); // Check if any attribute values were found // if (attributeValues.length === 0) { // throw new BadRequestError(`No valid attribute values found for attribute id ${attribute.id}`); // } // Map the values to their text value const selectedValues = attribute.values.map((val) => String(val)); specifications.push({ title: attributeDetails.title, values: selectedValues, }); } // Update the product with the new specifications and other fields const updatedProduct = await this.productRepo.model.findByIdAndUpdate( createStepTwoDto.productId, { specifications, description: createStepTwoDto.description, metaDescription: createStepTwoDto.metaDescription, tags: createStepTwoDto.tags, advantages: createStepTwoDto.advantages, disAdvantages: createStepTwoDto.disAdvantages, step: CreateProductStep.Attribute, }, { new: true, session }, ); await session.commitTransaction(); await session.endSession(); return { message: ProductMessage.CreatedAttribute, draftProduct: this.mapToDraftProductDTO(updatedProduct as IProduct, "image"), }; } catch (error) { await session.abortTransaction(); await session.endSession(); throw error; } } //************************************************************** //************************************************************** async createProductFinalStepForShopS(createFinalStepDto: ProductFinalStepDTO, shopId: string) { const shop = await this.shopeRepo.model.findById(shopId); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const product = await this.validateProduct(createFinalStepDto.productId, shop.owner.toString()); this.ensureCorrectStep(product.step, CreateProductStep.Image); const imagesUrl = { cover: createFinalStepDto.coverImage, list: createFinalStepDto.imagesList }; const updatedProduct = await this.productRepo.model.findByIdAndUpdate( createFinalStepDto.productId, { imagesUrl, step: CreateProductStep.Image, status: ProductStatus.Pending, }, { new: true }, ); if (!updatedProduct) { throw new BadRequestError(ProductMessage.CanNotUpdateProduct); } await this.notificationService.notifyNewProduct(shop.owner.toString(), updatedProduct.model); return { message: ProductMessage.ProductReadyForRelease, updatedProduct, }; } //#####################product update######################### //############################################################ async updateProductStepOneS(updateStepOneDto: UpdateProductStepOneDTO, ownerId: string) { const session = await startSession(); session.startTransaction(); try { const shop = await this.shopeRepo.model.findOne({ owner: ownerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const product = await this.validateProduct(updateStepOneDto.productId, ownerId); // ensure the product is in draft status this.ensureDraftStatus(product.status); const updatedProduct = await this.productRepo.model.findByIdAndUpdate( updateStepOneDto.productId, { ...updateStepOneDto, // step: CreateProductStep.Attribute, }, { new: true, session }, ); await session.commitTransaction(); await session.endSession(); return { message: ProductMessage.ProductUpdated, draftProduct: this.mapToDraftProductDTO(updatedProduct as IProduct, "attribute"), }; } catch (error) { await session.abortTransaction(); await session.endSession(); throw error; } } //************************************************************** //************************************************************** async updateProductStepTwoS(updateStepTwoDto: UpdateProductStepTwoDTO, ownerId: string) { const session = await startSession(); session.startTransaction(); try { const shop = await this.shopeRepo.model.findOne({ owner: ownerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const product = await this.validateProduct(updateStepTwoDto.productId, ownerId); // ensure the product is in draft status this.ensureDraftStatus(product.status); // ensure the step is correct // this.ensureCorrectStep(product.step, CreateProductStep.Attribute); // update specifications as in the creation step const specifications: ISpecifications[] = []; for (const attribute of updateStepTwoDto.attributes || []) { const attributeDetails = await this.categoryAttRepo.model.findById(attribute.id); if (!attributeDetails) { throw new BadRequestError(AttributeMessage.AttributeIdIsIncorrect); } // const attributeValues = await this.attributeValueRepo.model.find({ // _id: { $in: attribute.values }, // }); // if (attributeValues.length === 0) { // throw new BadRequestError(`No valid attribute values found for attribute id ${attribute.id}`); // } const selectedValues = attribute.values.map((val) => String(val)); specifications.push({ title: attributeDetails.title, values: selectedValues, }); } const updatedProduct = await this.productRepo.model.findByIdAndUpdate( updateStepTwoDto.productId, { specifications, description: updateStepTwoDto.description, metaDescription: updateStepTwoDto.metaDescription, tags: updateStepTwoDto.tags, advantages: updateStepTwoDto.advantages, disAdvantages: updateStepTwoDto.disAdvantages, }, { new: true, session }, ); await session.commitTransaction(); await session.endSession(); return { message: ProductMessage.ProductUpdated, draftProduct: this.mapToDraftProductDTO(updatedProduct as IProduct, "image"), }; } catch (error) { await session.abortTransaction(); await session.endSession(); throw error; } } //************************************************************** //************************************************************** async updateProductFinalStepS(updateFinalStepDto: UpdateProductFinalStepDTO, ownerId: string) { const product = await this.validateProduct(updateFinalStepDto.productId, ownerId); // ensure the product is in draft status this.ensureDraftStatus(product.status); // ensure the step is correct // this.ensureCorrectStep(product.step, CreateProductStep.Image); const imagesUrl = { cover: updateFinalStepDto.coverImage, list: updateFinalStepDto.imagesList }; const updatedProduct = await this.productRepo.model.findByIdAndUpdate( updateFinalStepDto.productId, { imagesUrl, // step: CreateProductStep.Image, status: ProductStatus.Approved, }, { new: true }, ); return { message: ProductMessage.ProductUpdated, updatedProduct, }; } async updateProductFinalStepForSeller(updateFinalStepDto: UpdateProductFinalStepDTO, ownerId: string) { const product = await this.validateProduct(updateFinalStepDto.productId, ownerId); // ensure the product is in draft status this.ensureDraftStatus(product.status); // ensure the step is correct // this.ensureCorrectStep(product.step, CreateProductStep.Image); const imagesUrl = { cover: updateFinalStepDto.coverImage, list: updateFinalStepDto.imagesList }; const updatedProduct = await this.productRepo.model.findByIdAndUpdate( updateFinalStepDto.productId, { imagesUrl, // step: CreateProductStep.Image, status: ProductStatus.Pending, }, { new: true }, ); return { message: ProductMessage.ProductUpdated, updatedProduct, }; } async addVoiceToProduct(productId: number, addVoiceDto: AddVoiceToProductDTO) { const product = await this.productRepo.model.findByIdAndUpdate(productId, { voice: addVoiceDto.voice }, { new: true }); if (!product) throw new BadRequestError(ProductMessage.NotFound); return { message: ProductMessage.ProductUpdated, product, }; } //########################### async deleteVoiceToProduct(productId: number) { const product = await this.productRepo.model.findByIdAndUpdate(productId, { voice: null }, { new: true }); if (!product) throw new BadRequestError(ProductMessage.NotFound); return { message: ProductMessage.ProductUpdated, product, }; } //************************************************************** //************************************************************** async addVariantS(addVariantDto: AddVariantDTO, ownerId: string, productId: number) { const session = await startSession(); session.startTransaction(); try { const product = await this.validateProduct(productId, ownerId); const shop = await this.shopeRepo.model.findOne({ owner: ownerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); this.ensureProductIsNotInDraft(product); const category = await this.ensureCategoryTheme(product.category.toString(), addVariantDto); await this.checkVariantCount(product, category.theme); // Check if a variant with the same colorId, meterageId, or sizeId already exists // Check if a variant with the same colorId, meterageId, or sizeId already exists const query: FilterQuery = { product: product._id }; if (addVariantDto.colorId) { query.color = addVariantDto.colorId; } if (addVariantDto.meterageId) { query.meterage = addVariantDto.meterageId; } if (addVariantDto.sizeId) { query.size = addVariantDto.sizeId; } if (category.toString() !== CategoryThemeEnum.No_color_No_sized) { const existingVariant = await this.productVariantRepo.model.findOne(query); if (existingVariant) throw new BadRequestError(ProductMessage.VariantAlreadyExists); } const productVariant = await this.createProductVariant(addVariantDto, product._id, shop._id.toString(), session); await this.productRepo.model.findByIdAndUpdate(product._id, { $push: { variants: productVariant } }, { session }); // product.variants.push(productVariant[0]._id); // await product.save({ session }); const { colorId, sizeId, meterageId } = addVariantDto; PriceChangeEvent.emitPriceChange({ colorId, sizeId, meterageId, product: productId, variantId: productVariant[0]._id.toString(), shop: shop._id.toString(), retail_price: productVariant[0].price.retailPrice, selling_price: productVariant[0].price.selling_price, }); // Commit the transaction await session.commitTransaction(); await session.endSession(); return { message: ProductMessage.VariantCreated, productVariant: productVariant[0], }; } catch (error) { // Rollback any changes made in the transaction await session.abortTransaction(); await session.endSession(); throw error; // Re-throw the error to be handled by the calling errorHandler } } //************************************************************** //************************************************************** async updateVariantS(updateVariantDto: UpdateVariantDTO, ownerId: string, productId: number) { //TODO:check for stock for adding special sales const product = await this.validateProduct(productId, ownerId); this.ensureProductIsNotInDraft(product); const shop = await this.shopeRepo.model.findOne({ owner: ownerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); // Calculate the discount based on the discount type const { sellingPrice, discountPercent } = this.calculateDiscount(updateVariantDto); const product_price: Partial = { retailPrice: updateVariantDto.retailPrice, selling_price: sellingPrice, order_limit: updateVariantDto.order_limit, is_specialSale: updateVariantDto.discount_percent || updateVariantDto.discount_value ? true : false, discount_percent: discountPercent, specialSale_order_limit: updateVariantDto.specialSale_order_limit, specialSale_quantity: updateVariantDto.specialSale_quantity, specialSale_endDate: updateVariantDto.specialSale_endDate, }; const { saleFormat } = updateVariantDto; const updatedVariant = await this.productVariantRepo.model.findByIdAndUpdate( updateVariantDto.variantId, { price: product_price, sellerSpecialCode: updateVariantDto.sellerSpecialCode, stock: updateVariantDto.stock, isWholeSale: saleFormat && saleFormat.wholeSale.length > 0, saleFormat: saleFormat ? saleFormat : { wholeSale: [] }, }, { new: true, }, ); PriceChangeEvent.emitPriceChange({ colorId: updatedVariant?.color, sizeId: updatedVariant?.size, meterageId: updatedVariant?.meterage, product: productId, variantId: updatedVariant?._id.toString() as string, shop: shop._id.toString(), isSpecial: product_price.is_specialSale, // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain retail_price: updatedVariant?.price.retailPrice!, // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain selling_price: updatedVariant?.price.selling_price!, }); return { message: ProductMessage.VariantUpdated, updatedVariant, }; } //###################################################################### //########################### product observer ######################### async removeObserver(userId: string, productId: number, removeObserverDto: RemoveObserverDTO) { const product = await this.validateProduct(productId); const productVariant = await this.productVariantRepo.findById(removeObserverDto.variantId); if (product._id !== productVariant?.product) throw new BadRequestError(CartMessage.ProductNotBelongToVariant); await this.productObserveRepo.model.findOneAndDelete({ user: userId, product: product._id, variant: productVariant._id }); return { message: CommonMessage.Deleted, }; } async addObserver(userId: string, productId: number, addObserverDto: AddObserverDTO) { const product = await this.validateProduct(productId); const productVariant = await this.productVariantRepo.findById(addObserverDto.variantId); if (product._id !== productVariant?.product) throw new BadRequestError(CartMessage.ProductNotBelongToVariant); const existObserver = await this.productObserveRepo.model.findOne({ user: userId, product: productId, variant: productVariant._id, }); if (!existObserver) { const newObserver = await this.productObserveRepo.model.create({ user: userId, product: productId, variant: addObserverDto.variantId, registeredPrice: productVariant.price.selling_price, }); return { message: CommonMessage.Created, newObserver, }; } return { message: CommonMessage.Created, observer: existObserver, }; } //###################################################################### //########################### product wishlist ######################### async checkProductInWishlist(userId: string, productId: number, variantId: string) { if (!isValidObjectId(variantId)) throw new BadRequestError([CommonMessage.NotValidId, variantId]); await this.validateProduct(productId); const isInWishlist = await this.wishlistRepo.model.findOne({ user: userId, product: productId, variant: variantId }); return { isInWishlist: !!isInWishlist, }; } async removeWishlist(userId: string, productId: number, removeDto: RemoveWishlistDTO) { const product = await this.validateProduct(productId); const productVariant = await this.productVariantRepo.findById(removeDto.variantId); if (product._id !== productVariant?.product) throw new BadRequestError(CartMessage.ProductNotBelongToVariant); await this.wishlistRepo.model.findOneAndDelete({ user: userId, product: product._id, variant: productVariant._id }); return { message: CommonMessage.Deleted, }; } async addToWishlist(userId: string, productId: number, addDto: AddWishlistDTO) { const product = await this.validateProduct(productId); const productVariant = await this.productVariantRepo.findById(addDto.variantId); if (product._id !== productVariant?.product) throw new BadRequestError(CartMessage.ProductNotBelongToVariant); const existItem = await this.wishlistRepo.model.findOne({ user: userId, product: productId, variant: productVariant._id }); if (!existItem) { const newItem = await this.wishlistRepo.model.create({ user: userId, product: productId, variant: addDto.variantId, }); return { message: CommonMessage.Created, wishlistItem: newItem, }; } return { message: CommonMessage.Created, wishlistItem: existItem, }; } //###################################################################### //###################################################################### async updateStock(operations: Array<{ variantId: string; quantity: number }>, type: "increase" | "decrease", session: ClientSession) { const bulkOperations = []; for (const operation of operations) { const { variantId, quantity } = operation; const product = await this.productVariantRepo.findById(variantId); if (!product) { throw new BadRequestError(`Product variant with id ${variantId} not found`); } if (type === "decrease" && product.stock < quantity) { throw new BadRequestError(`Insufficient stock for product variant with id ${variantId}`); } bulkOperations.push({ updateOne: { filter: { _id: variantId }, update: { $inc: { stock: type === "increase" ? quantity : -quantity }, }, }, }); } if (bulkOperations.length > 0) { await this.productVariantRepo.model.bulkWrite(bulkOperations, { session }); } } //###################################################################### //###################################################################### async reduceStock(productVariantId: string, quantity: number, session: ClientSession) { const product = await this.productVariantRepo.findById(productVariantId); if (!product) { throw new BadRequestError(`Product variant with id ${productVariantId} not found`); } if (product.stock < quantity) { throw new BadRequestError(`Insufficient stock for product variant with id ${productVariantId}`); } product.stock -= quantity; await product.save({ session }); } //###################################################################### //###################################################################### async increaseStock(productVariantId: string, quantity: number, session: ClientSession): Promise { const product = await this.productVariantRepo.findById(productVariantId); if (!product) { throw new Error(`Product variant with id ${productVariantId} not found`); } product.stock += quantity; await product.save({ session }); } //############################################################### //###################### product ads ############################ async addProductAdS(sellerId: string, createDto: ProductAdDTO, productId: number) { await this.validateProduct(productId, sellerId); const newAd = await this.productAdRepo.model.create({ product: productId, seller: sellerId, variant: createDto.variantId, ...createDto, }); return { message: CommonMessage.Created, newAd, }; } //************************************************************** //************************************************************** async getProductAds(sellerId: string, productId: number, variantId: string) { await this.validateProduct(productId, sellerId); const productAd = await this.productAdRepo.model.findOne({ product: productId, seller: sellerId, variant: variantId }); if (!productAd) throw new BadRequestError(AdMessage.NotFoundWithId); const totalCost = this.getTotalCostForAd(productAd); return { productAd, totalCost, }; } //############################################################### //######################### get product ######################### async getAdminPanelProducts(ownerRef: OwnerRef, queryDto: SellerProductQueryDTO) { const shops = await this.shopeRepo.model.find({ ownerRef }).select("_id"); const shopIds = shops.map((shop) => shop._id); const { docs, count } = await this.productRepo.getSellersProducts(shopIds, queryDto); const brands = docs.map(({ brand }: any) => BrandDTO.transformBrand(brand)); const categories = docs.map(({ category }: any) => CategoryTreeDTO.transformCategory(category)); const products = docs.map((doc: IProduct) => SellerPanelProductDTO.transformProduct(doc)); return { brands, categories, count, products, }; } async findSellerProduct(ownerId: string, queryDto: SellerProductQueryDTO) { const shop = await this.shopeRepo.model.findOne({ owner: ownerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const { docs, count, category } = await this.productRepo.getSellerProducts(shop._id.toString(), queryDto); const brands = docs.map(({ brand }: any) => BrandDTO.transformBrand(brand)); const categories = category.map((cat: any) => CategoryTreeDTO.transformCategory(cat)); const products = docs.map((doc: IProduct) => SellerPanelProductDTO.transformProduct(doc)); return { brands, categories, count, products, }; } async filterProductsByStatus(queries: FilterByStatusProductsQueries) { const { docs, count } = await this.productRepo.getProductsByStatus(queries); const brands = docs.map(({ brand }: any) => BrandDTO.transformBrand(brand)); const categories = docs.map(({ category }: any) => CategoryTreeDTO.transformCategory(category)); const products = docs.map((doc: IProduct) => SellerPanelProductDTO.transformProduct(doc)); return { brands, categories, count, products, }; } async getProductDetailsForSellerPanel(ownerId: string, productId: number) { const shop = await this.shopeRepo.model.findOne({ owner: ownerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); await this.validateProduct(productId, ownerId); const product = await this.productRepo.model.findById(productId).populate("category brand"); return { product, }; } async getProductDetailsForAdminPanel(productId: number) { await this.validateProduct(productId); const product = await this.productRepo.model.findById(productId).populate("category brand"); return { product, }; } //************************************************************** //************************************************************** async getAllVariantS(ownerId: string, productId: number, queries: VariantQueries) { const shop = await this.shopeRepo.model.findOne({ owner: ownerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const product = await this.validateProduct(productId, ownerId); const { docs, count } = await this.productVariantRepo.getProductVariants(shop._id.toString(), product._id, queries); const productVariants = docs.map((doc: IProductVariant) => ProductVariantDTO.transformProduct(doc)); return { count, productVariants }; } //************************************************************** //************************************************************** async getAllProductsVariantsS(ownerId: string, queries: VariantQueries) { const shop = await this.shopeRepo.model.findOne({ owner: ownerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const { docs, count } = await this.productVariantRepo.getAllProductsVariants(shop._id.toString(), queries); const productsVariants = docs.map((doc: IProductVariant) => ProductVariantDTO.transformProduct(doc)); return { count, productsVariants }; } async getSingleVariant(ownerId: string, productId: number, variantId: string) { if (!isValidObjectId(variantId)) throw new BadRequestError(ProductMessage.ProductVariantNotFound); const shop = await this.shopeRepo.model.findOne({ owner: ownerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); const product = await this.validateProduct(productId, ownerId); const docs = await this.productVariantRepo.getSingleVariant(shop._id.toString(), product._id, variantId); const productVariant = ProductVariantDTO.transformProduct(docs[0]); return { productVariant, // product, }; } //************************************************************** //************************************************************** async getProductDetails(productId: number) { await this.validateProduct(productId); const docs = await this.productRepo.getProductDetails(productId); if ((!docs?.[0]?.variants || docs?.[0]?.variants.length === 0) && docs?.[0]?.category.theme !== "noColor_noSize") { throw new BadRequestError(CommonMessage.NotEnoughVariant); } const product = ProductDTO.transformProduct(docs[0]); const categoryPath = await this.categoryRepo.getCategoryPath(product.category._id.toString()); return { product, categoryPath }; } //************************************************************** async draft(id: number) { const updatedProduct = await this.productRepo.model.findByIdAndUpdate(id, { status: ProductStatus.Draft }, { new: true }); if (!updatedProduct) throw new BadRequestError(CommonMessage.NotValidId); return { message: CommonMessage.Updated, updatedProduct, }; } async approve(id: number) { const updatedProduct = await this.productRepo.model.findByIdAndUpdate(id, { status: ProductStatus.Approved }, { new: true }); if (!updatedProduct) throw new BadRequestError(CommonMessage.NotValidId); return { message: CommonMessage.Updated, updatedProduct, }; } async pending(id: number) { const updatedProduct = await this.productRepo.model.findByIdAndUpdate(id, { status: ProductStatus.Pending }, { new: true }); if (!updatedProduct) throw new BadRequestError(CommonMessage.NotValidId); return { message: CommonMessage.Updated, updatedProduct, }; } async reject(id: number, rejectCommentDto: RejectCommentDTO) { const updatedProduct = await this.productRepo.model.findByIdAndUpdate( id, { status: ProductStatus.Rejected, adminComments: rejectCommentDto.adminComments }, { new: true }, ); if (!updatedProduct) throw new BadRequestError(CommonMessage.NotValidId); return { message: CommonMessage.Updated, updatedProduct, }; } //************************************************************** //************************************************************** async getProductPriceHistoryS(productId: number, since: string) { const priceHistory = await this.priceHistoryRepo.getPriceHistory(productId, since); return { priceHistory, }; } //************************************************************** //************************************************************** async searchForCompareProductsS(queries: CompareSearchQueries) { const product = await this.validateProduct(queries.productId); const { docs, count } = await this.productRepo.getProductsForCompareSearch(product.category.toString(), queries); const products = docs.map((doc: IProduct) => ProductDTO.transformProduct(doc)); return { count, products }; } //************************************************************** //************************************************************** async compareProducts(productIds: string[] | string) { const docs = await this.productRepo.getProductSpecification(productIds); const products = docs.map((doc: IProduct) => ProductDTO.transformProduct(doc)); return { products }; } //############################################################### //################### delete a product ######################### async deleteProduct(ownerId: string, productId: number) { const session = await startSession(); session.startTransaction(); try { // validate product existence and ownership const product = await this.validateProduct(productId, ownerId); const pStatus = product.status; if (pStatus !== ProductStatus.Draft) throw new BadRequestError(ProductMessage.CanNotDelete); const shop = await this.shopeRepo.model.findOne({ owner: ownerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); // const productVariants = await this.productVariantRepo.model.find({ product: productId, shop: shop._id.toString() }, { _id: 1 }); await this.productRepo.model.findByIdAndDelete(product._id, { session }); // delete all variants belong to the product // if (productVariants.length > 0) { // await this.productVariantRepo.model.deleteMany({ _id: { $in: productVariants.map((variant) => variant._id) } }, { session }); // } await session.commitTransaction(); await session.endSession(); return { message: ProductMessage.Deleted, }; } catch (error) { // Rollback any changes made in the transaction await session.abortTransaction(); await session.endSession(); throw error; // Re-throw the error to be handled by the calling errorHandler } } async softDelete(ownerId: string, productId: number) { const session = await startSession(); session.startTransaction(); try { // validate product existence and ownership const product = await this.validateProduct(productId, ownerId); const shop = await this.shopeRepo.model.findOne({ owner: ownerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); // const productVariants = await this.productVariantRepo.model.find({ product: productId, shop: shop._id.toString() }, { _id: 1 }); await this.productRepo.model.findByIdAndUpdate(product._id, { deleted: true }, { session }); // // delete all variants belong to the product // if (productVariants.length > 0) { // await this.productVariantRepo.model.deleteMany({ _id: { $in: productVariants.map((variant) => variant._id) } }, { session }); // } await session.commitTransaction(); await session.endSession(); return { message: ProductMessage.Deleted, }; } catch (error) { await session.abortTransaction(); await session.endSession(); throw error; } } //################################################################ async cloneProduct(ownerId: string, productId: number) { const product = await this.validateProduct(productId, ownerId); const cloneProduct = await this.productRepo.model.create({ title_fa: `${product.title_fa}-c`, title_en: `${product.title_en}-c`, model: `${product.model}-c`, source: product.source, description: product.description, metaDescription: product.metaDescription, tags: product.tags, advantages: product.advantages, disAdvantages: product.disAdvantages, specifications: product.specifications, category: product.category, shop: product.shop, brand: product.brand, imagesUrl: product.imagesUrl, status: ProductStatus.Draft, step: CreateProductStep.Image, isFake: true, }); return { message: ProductMessage.Cloned, cloneProduct, }; } //############################################################### //################### product comment and question ############## async addQuestionS(userId: string, productId: number, createQuestionDto: CreateQuestionDTO) { const session = await startSession(); session.startTransaction(); try { const product = await this.validateProduct(productId); const newQuestion = await this.questionRepo.model.create( [ { content: createQuestionDto.content, user: userId, product: productId, }, ], { session }, ); await this.notificationService.notifyProductQuestion(product.title_fa, session); await session.commitTransaction(); return { message: "سوال با موفقیت اضافه شد", content: newQuestion[0].content, }; } catch (error) { await session.abortTransaction(); throw error; } finally { await session.endSession(); } } async getCommentsForReport() { const comments = await this.commentRepo.getCommentsForReport(); return comments; } async getQuestionsForReport() { const questions = await this.questionRepo.getQuestionsForReport(); return questions; } //############################################################### async getQuestions(productId: number) { await this.validateProduct(productId); const docs = await this.questionRepo.model.find({ product: productId, status: Question_CommentStatus.Accepted }); const questions = docs.map((doc) => QuestionDTO.transformQuestion(doc)); return { questions, }; } //############################################################### async getQuestionsForAdminPanel(query: ProductsQuestionsQueries) { const { count, docs } = await this.questionRepo.getProductsQuestions(query); const questions = docs.map((doc: IQuestion) => QuestionDTO.transformQuestion(doc)); return { questions, count, }; } //############################################################### async answerQuestion(id: string, createDto: CreateAnswerQuestionDTO, ownerId: string, ownerRef: OwnerRef) { const question = await this.questionRepo.model.findById(id); if (!question) throw new BadRequestError(CommonMessage.NotValidId); const shop = await this.shopeRepo.model.findOne({ owner: ownerId, ownerRef }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); question.answers?.push({ content: createDto.content, owner: shop.owner, ownerRef: shop.ownerRef, }); await question.save(); return { message: CommonMessage.Updated, question, }; } //############################################################### async approveQuestion(id: string) { const updatedQuestion = await this.questionRepo.model.findByIdAndUpdate(id, { status: Question_CommentStatus.Accepted }, { new: true }); if (!updatedQuestion) throw new BadRequestError(CommonMessage.NotValidId); return { message: CommonMessage.Updated, updatedQuestion, }; } //############################################################### async addCommentS(userId: string, productId: number, createCommentDto: CreateCommentDTO) { const session = await startSession(); session.startTransaction(); try { const product = await this.validateProduct(productId); const newComment = await this.commentRepo.model.create( [ { title: createCommentDto.title, advantage: createCommentDto.advantage, disAdvantage: createCommentDto.disAdvantage, content: createCommentDto.content, user: userId, product: productId, rate: createCommentDto.rate, }, ], { session }, ); await this.notificationService.notifyProductComment(product.title_fa, session); await session.commitTransaction(); return { message: "کامنت با موفقیت اضافه شد", content: newComment[0].content, }; } catch (error) { await session.abortTransaction(); throw error; } finally { await session.endSession(); } } async getComments(productId: number) { await this.validateProduct(productId); const docs = await this.commentRepo.model.find({ product: productId, status: Question_CommentStatus.Accepted }); const comments = docs.map((doc) => CommentDTO.transformComment(doc)); return { comments, }; } async getCommentsForAdminPanel(query: ProductsCommentsQueries) { const { count, docs } = await this.commentRepo.getProductsComments(query); const comments = docs.map((doc: IComment) => CommentDTO.transformComment(doc)); return { comments, count, }; } async approveComment(id: string) { const updatedComment = await this.commentRepo.model.findByIdAndUpdate(id, { status: Question_CommentStatus.Accepted }, { new: true }); if (!updatedComment) throw new BadRequestError(CommonMessage.NotValidId); const product = await this.productRepo.model.findById(updatedComment.product); if (!product) { throw new BadRequestError(ProductMessage.ProductNotFound); } const rate = product.rate || 0; const totalRate = product.totalRate || 0; const commentRate = updatedComment.rate || 0; if (isNaN(rate) || isNaN(totalRate) || isNaN(commentRate)) { throw new BadRequestError("Invalid rate values"); } product.rate = (rate * totalRate + commentRate) / (totalRate + 1); product.totalRate += 1; await product.save(); return { message: CommonMessage.Updated, updatedComment, }; } //############################################################### //################### toggle the variant status ################# async activateVariantS(ownerId: string, activateDto: ActivateVariantDTO, productId: number) { await this.validateProduct(productId, ownerId); const productVariant = await this.productVariantRepo.model.findById(activateDto.variantId); if (!productVariant) throw new BadRequestError(ProductMessage.ProductVariantNotFound); const market_status = activateDto.isActive /*&& productVariant.stock > 0 */ ? ProductMarketStatus.Marketable : ProductMarketStatus.stop_production; await this.productVariantRepo.model.findByIdAndUpdate(activateDto.variantId, { market_status }); return { message: CommonMessage.Updated, }; } //############################################################### async setFreeShipping(ownerId: string, productId: number, variantId: string) { await this.validateProduct(productId, ownerId); const productVariant = await this.productVariantRepo.model.findById(variantId); if (!productVariant) throw new BadRequestError(ProductMessage.ProductVariantNotFound); productVariant.isFreeShip = !productVariant.isFreeShip; await productVariant.save(); return { message: CommonMessage.Updated, }; } //############################################################### //################### product report ############################# async getReportQuestions() { const questions = await this.reportQuestionRepo.findAll(); return { questions, }; } async createReportQuestion(createDto: CreateReportQuestionDTO) { const createdQuestion = await this.reportQuestionRepo.model.create({ title: createDto.title, type: ReportQuestionType.CHECKBOX }); return { message: CommonMessage.Created, reportQuestion: createdQuestion, }; } //************************************************************** //************************************************************** async addReportS(userId: string, createReportDto: CreateReportDTO, productId: number) { await this.validateProduct(productId); const questionsId = createReportDto.answers.map((answer) => answer.questionId); const newReport = await this.productReportRepo.model.create({ user: userId, product: productId, reason: questionsId, description: createReportDto.description, }); return { message: CommonMessage.Created, newReport, }; } async getProductReportForAdmin() { const reports = await this.productReportRepo.model.find().populate([{ path: "user" }, { path: "product" }, { path: "reason" }]); return { reports }; } //############################################################### //########################helper methods######################### //************************************************************** //************************************************************** //****method to check if product belong to current sessions user\ private async validateProduct(productId: number, ownerId?: string) { //if product id is falsy like NaN if (!productId) throw new BadRequestError(ProductMessage.ProductNotFound); //check if product exist const product = await this.productRepo.findById(productId); if (!product) throw new BadRequestError(ProductMessage.ProductNotFound); //check if product belong to seller if (ownerId) { const shop = await this.shopeRepo.model.findOne({ owner: ownerId }); if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); if (product.shop._id.toString() !== shop._id.toString()) throw new ForbiddenError(ProductMessage.CanNotUpdate); } return product; } //************************************************************** //************************************************************** //****method to check if attribute is correct based on theme private async ensureCategoryTheme(categoryId: string, addVariantDto: AddVariantDTO) { const category = await this.categoryRepo.findById(categoryId); if (!category) { throw new BadRequestError(CategoryMessage.NotValidId); } // Check if the category theme is color, size, or meterage if (category.theme === CategoryThemeEnum.Colored) { if (addVariantDto.sizeId || addVariantDto.meterageId) { throw new BadRequestError(AttributeMessage.SizeNotAllowedForColoredTheme); } if (!addVariantDto.colorId) { throw new BadRequestError(AttributeMessage.ColorRequiredForColoredTheme); } } else if (category.theme === CategoryThemeEnum.Sized) { if (addVariantDto.colorId || addVariantDto.meterageId) { throw new BadRequestError(AttributeMessage.ColorNotAllowedForSizedTheme); } if (!addVariantDto.sizeId) { throw new BadRequestError(AttributeMessage.SizeRequiredForSizedTheme); } } else if (category.theme === CategoryThemeEnum.Meterage) { if (addVariantDto.colorId || addVariantDto.sizeId) { throw new BadRequestError(AttributeMessage.ColorOrSizeNotAllowedForMeterageTheme); } if (!addVariantDto.meterageId) { throw new BadRequestError(AttributeMessage.MeterageRequiredForMeterageTheme); } } else { // For other themes that are neither color, size, nor meterage if (addVariantDto.sizeId || addVariantDto.colorId || addVariantDto.meterageId) { throw new BadRequestError(AttributeMessage.ThemeIsNoColorNoSizeNoMeterage); } } return category; } //************************************************************** //************************************************************** //****method to check for duplicate product private async checkForDuplicateProduct(model: string) { const exist = await this.productRepo.model.exists({ model, deleted: false }); if (exist) throw new BadRequestError(ProductMessage.DuplicateName); return true; } //************************************************************** //************************************************************** //****method to check the product is not in draft status private ensureProductIsNotInDraft(product: IProduct) { if (product.status === ProductStatus.Draft) { throw new BadRequestError(ProductMessage.ProductIsInDraftStep); } return true; } //************************************************************** //************************************************************** private ensureDraftStatus(status: ProductStatus) { if (status !== ProductStatus.Draft) { throw new BadRequestError(ProductMessage.ProductMustBeInDraft); } } //************************************************************** //************************************************************** //****method to check the product is in correct step private ensureCorrectStep(currentStep: CreateProductStep, expectedStep: CreateProductStep) { // const currentStepIndex = this.stepOrder.indexOf(currentStep); // const expectedStepIndex = this.stepOrder.indexOf(expectedStep); if (currentStep >= expectedStep) { throw new BadRequestError(ProductMessage.ProductIsInNextStep); } } //************************************************************** //************************************************************** //****method to map the product for send it in response private mapToDraftProductDTO(product: IProduct, nextStep: string) { return { id: product._id, title_fa: product.title_fa, title_en: product.title_en, model: product.model, nextStep, }; } //************************************************************** //************************************************************** //****method to create variant on product private async createProductVariant(addVariantDto: AddVariantDTO, productId: number, shopId: string, session: ClientSession) { const dimensions: IDimensions = { package_weight: addVariantDto.package_weight, package_height: addVariantDto.package_height, package_length: addVariantDto.package_length, package_width: addVariantDto.package_width, }; const product_price: Partial = { order_limit: addVariantDto.order_limit, retailPrice: addVariantDto.retail_price, }; return await this.productVariantRepo.model.create( [ { product: productId, shop: shopId, warranty: addVariantDto.warrantyId, // shipmentMethod: addVariantDto.shipmentsId, color: addVariantDto.colorId, size: addVariantDto.sizeId, meterage: addVariantDto.meterageId, dimensions, market_status: ProductMarketStatus.Marketable, price: product_price, stock: addVariantDto.stock, postingTime: addVariantDto.postingTime, sellerSpecialCode: addVariantDto.sellerSpecialCode, }, ], { session }, ); } //************************************************************** //************************************************************** //****method to calculate Discount private calculateDiscount(updateVariantDto: UpdateVariantDTO): { sellingPrice: number; discountPercent: number } { const { retailPrice, discount_type, discount_value, discount_percent } = updateVariantDto; let sellingPrice = retailPrice; let discountPercent = 0; if (discount_type === ProductDiscountType.Fixed && discount_value) { sellingPrice = retailPrice - discount_value; discountPercent = (discount_value / retailPrice) * 100; } else if (discount_type === ProductDiscountType.Percent && discount_percent) { discountPercent = discount_percent; sellingPrice = retailPrice - (retailPrice * discount_percent) / 100; } return { sellingPrice, discountPercent }; } //************************************************************** //************************************************************** //****method to calculate total cost of the ad till now private getTotalCostForAd(ad: IProductAd): number { let totalCost = 0; if (ad.type === AdType.Click) { totalCost = (ad.clickCount || 0) * ad.cost; } else if (ad.type === AdType.Daily) { const currentDate = TimeService.getCurrentPersianDate(); // convert ad.startDate and ad.endDate from Persian (Jalali) to Gregorian const startDateGregorian = TimeService.convertPersianToGregorian(ad.startDate); const endDateGregorian = TimeService.convertPersianToGregorian(ad.endDate); // Calculate the number of days the ad has been active, using Gregorian dates const startDate = new Date(startDateGregorian); const endDate = new Date(endDateGregorian); const currentDateObj = new Date(TimeService.convertPersianToGregorian(currentDate)); // Calculate the number of active days const daysActive = Math.ceil((Math.min(currentDateObj.getTime(), endDate.getTime()) - startDate.getTime()) / (1000 * 60 * 60 * 24)); // Total cost is the number of active days multiplied by the daily cost totalCost = daysActive * ad.cost; } return totalCost; } private async checkVariantCount(product: IProduct, categoryTheme: CategoryThemeEnum) { if (categoryTheme !== CategoryThemeEnum.No_color_No_sized) return true; const variantCount = product.variants.length; console.log("variantCount", variantCount); if (!variantCount) return true; console.log("pro", product, categoryTheme); throw new BadRequestError(ProductMessage.ProductWithNoVariantCategoryTheme); } //############################################################### //###################### popular product ############################ async addPopularProduct(productId: number) { await this.validateProduct(productId); const newPopular = await this.popularProductRepo.model.create({ product: productId, }); return { message: CommonMessage.Created, newPopular, }; } async addIncredibleOffers(paramDto: AddIncredibleOffersParamDto) { const deleteIncredible = await this.incredibleOffersRepo.model.findOneAndDelete({ product: paramDto.id, variant: paramDto.variantId }); if (deleteIncredible) { return { message: CommonMessage.Deleted, deleteIncredible, }; } const incredibleOffer = await this.incredibleOffersRepo.model.create({ product: paramDto.id, variant: paramDto.variantId }); return { message: CommonMessage.Created, incredibleOffer, }; } async deletePopularProduct(productId: number) { await this.validateProduct(productId); await this.popularProductRepo.model.findOneAndDelete({ product: productId, }); return { message: CommonMessage.Deleted, }; } async getPopularProducts(userId?: string) { const popularProducts = await this.popularProductRepo.getPopularProducts(userId); return { popularProducts, }; } async getIncredibleOffers(paginationDto: PaginationDTO) { const { skip, limit } = paginationUtils(paginationDto); const { count, incredibleOffers } = await this.incredibleOffersRepo.getIncredibleOffersForAdmin(skip, limit); return { incredibleOffers, count, }; } async getSpecialProductsForAdmin(paginationDto: PaginationDTO) { const { skip, limit } = paginationUtils(paginationDto); const { specialSaleProducts, count } = await this.productRepo.getIncredibleOffers(skip, limit); return { specialSaleProducts, count, }; } async getProductCount() { return await this.productRepo.getProductCount(); } async getPendingProductCount() { return await this.productRepo.getPendingProductCount(); } async updateProduct(productId: number, updateDto: UpdateProductDTO) { const product = await this.validateProduct(productId); const updatedProduct = await this.productRepo.model.findByIdAndUpdate({ _id: product._id }, { ...updateDto }, { new: true }); return { updatedProduct }; } async deleteProductQuestion(questionId: string) { const question = await this.questionRepo.model.findByIdAndDelete(questionId); if (!question) { throw new BadRequestError(ProductMessage.CanNotDeleteQuestion); } return { message: CommonMessage.Deleted, }; } async deleteProductComment(commentId: string) { const comment = await this.commentRepo.model.findByIdAndDelete(commentId); if (!comment) { throw new BadRequestError(ProductMessage.CanNotDeleteComment); } return { message: CommonMessage.Deleted, }; } //************************************************************** //************************************************************** //****method to calculate search cost // private calculateCostForSearch(ad: IProductAd): number { // let searchCost = 0; // if (ad.type === AdType.Click) { // if (ad.clickCount && ad.maxClicks && ad.clickCount >= ad.maxClicks) { // searchCost = 0; // console.log("Max clicks reached, no more cost will be incurred for this ad."); // } else { // searchCost = ad.cost; // ad.clickCount = (ad.clickCount || 0) + 1; // } // } else if (ad.type === AdType.Daily) { // searchCost = ad.cost; // } // return searchCost; // } } export { ProductService };