Files
shop-api/src/modules/product/providers/product.service.ts
T
2026-02-13 19:12:10 +03:30

1714 lines
64 KiB
TypeScript

import { inject, injectable } from "inversify";
import { ClientSession, Types, isValidObjectId, startSession } from "mongoose";
import slugify from "slugify";
import { PaginationDTO } from "../../../common/dto/pagination.dto";
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, ThemeValueRepository } 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;
@inject(IOCTYPES.ThemeValueRepository) themeValueRepo: ThemeValueRepository;
//###############################################################
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);
await this.ensureCategoryTheme(product.category.toString(), addVariantDto);
// await this.checkVariantCount(product, category.theme);
// Check if a variant with the same themeValue already exists
// const query: FilterQuery<IProductVariant> = { product: product._id };
if (addVariantDto.themeValueId) {
const existingVariant = await this.productVariantRepo.model.findOne({
product: product._id,
themeValue: addVariantDto.themeValueId,
});
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 });
PriceChangeEvent.emitPriceChange({
themeValueId: addVariantDto.themeValueId,
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<IPrice> = {
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({
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<void> {
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);
const product = ProductDTO.transformProduct(docs[0]);
const stats = await this.commentRepo.getProductStats(productId);
const categoryPath = await this.categoryRepo.getCategoryPath(product.category._id.toString());
return { product, categoryPath, stats };
}
//**************************************************************
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 softDeleteProductVariant(variantId: string) {
await this.productVariantRepo.model.findOneAndUpdate({ _id: variantId }, { deleted: true });
return { message: 'Deleted successfully' }
}
//################################################################
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);
}
if (addVariantDto.themeValueId) {
const themeValue = await this.themeValueRepo.findById(addVariantDto.themeValueId);
if (!themeValue) throw new BadRequestError(CommonMessage.NotFoundById);
category.variants = [themeValue._id];
}
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<IPrice> = {
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,
themeValue: new Types.ObjectId(addVariantDto.themeValueId),
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;
}
//###############################################################
//###################### 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;
// }
// private async checkVariantCount(product: IProduct, categoryTheme: Types.ObjectId | undefined) {
// if (categoryTheme) 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);
// }
}
export { ProductService };