1749 lines
65 KiB
TypeScript
1749 lines
65 KiB
TypeScript
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<IProductVariant> = { 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<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({
|
|
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<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);
|
|
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<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,
|
|
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 };
|