This commit is contained in:
mahyargdz
2025-09-14 15:15:03 +03:30
commit be82059172
534 changed files with 51310 additions and 0 deletions
@@ -0,0 +1,212 @@
import { Expose, Type } from "class-transformer";
import { ArrayMinSize, IsArray, IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId } from "../../../common/decorator/validation.decorator";
import { AttributeMessage } from "../../../common/enums/message.enum";
import { ProductSource } from "../../../common/enums/product.enum";
import { BrandModel } from "../../brand/models/brand.model";
import { CategoryModel } from "../../category/models/category.model";
import { CategoryAttributeModel } from "../../category/models/CategoryAttribute.model";
import { ProductModel } from "../models/product.model";
// export class SpecificationsDTO {
// @Expose()
// @IsNotEmpty()
// @IsString()
// title: string;
// @Expose()
// @IsNotEmpty()
// @IsArray()
// @ArrayMinSize(1)
// @ValidateNested({ each: true })
// @Type(() => SpecificationsAttr)
// attributes: SpecificationsAttr[];
// }
// export class SpecificationsAttr {
// @Expose()
// @IsNotEmpty()
// @IsString()
// title: string;
// @Expose()
// @IsNotEmpty()
// @IsArray()
// values: string[];
// }
//===========>
export class ProductStepOneDTO {
@Expose()
@IsNotEmpty()
@IsValidId(CategoryModel)
@ApiProperty({ type: "string", description: "category id of the product", example: "66ab8a37d09606beb22eb0d1" })
category!: string; // Category ID
@Expose()
@IsNotEmpty()
@IsValidId(BrandModel)
@ApiProperty({ type: "string", description: "brand id of the product", example: "66ab8a37d09606beb22eb0d1" })
brand!: string; // Brand ID
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({
type: "string",
description: "the model of the product",
example: "Galaxy S24 Ultra",
})
model!: string;
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({
type: "string",
description: "the title in farsi",
example: "گوشی موبایل سامسونگ مدل Galaxy S24 Ultra دو سیم کارت ظرفیت 256 گیگابایت و رم 12 گیگابایت - ویتنام ",
})
title_fa: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@ApiProperty({
type: "string",
description: "the title in english",
example: "Samsung Galaxy S24 Ultra Dual SIM 256GB And 12GB RAM Mobile Phone - Vietnam",
})
title_en?: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@ApiProperty({
type: "string",
description: "the seo title of the product",
example: "گوشی موبایل سامسونگ مدل Galaxy S24 Ultra دو سیم کارت ظرفیت 256 گیگابایت و رم 12 گیگابایت - ویتنام ",
})
seoTitle?: string;
@Expose()
@IsNotEmpty()
@IsEnum(ProductSource)
@ApiProperty({ type: "string", description: "source of the product", example: "local" })
source: ProductSource;
@Expose()
@IsNotEmpty()
@IsBoolean()
@ApiProperty({ type: "boolean", description: "determine if product is fake or not", example: "false" })
isFake: boolean;
}
export class Attributes {
@Expose()
@IsNotEmpty()
@IsValidId(CategoryAttributeModel, { message: AttributeMessage.AttributeIdIsIncorrect })
id: number;
@Expose()
@IsNotEmpty()
@IsArray()
values: number[];
}
export class ProductStepTwoDTO {
@Expose()
@IsNotEmpty()
@IsNumber()
@IsValidId(ProductModel)
@ApiProperty({ type: "number", description: "the id of product ", example: "100002" })
productId: number;
@Expose()
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => Attributes)
@ApiProperty({
type: "Array",
description: "send attribute id to create a specification of product",
example: [{ id: 2, values: [874] }],
})
attributes: Attributes[];
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({
type: "string",
description: "description of the product",
example:
"همواره گوشی‌های هوشمند پرچمدار سامسونگ توانسته‌اند با بهره بردن از مشخصات فنی قدرتمند، توجه هر بیننده‌ای را به خود جلب کنند و همواره رقیبان سرسختی برای برند‌های دیگر فعال در این عرصه بودند و این بار نوبت به قدرت‌نمایی سامسونگ Galaxy S24 Ultra رسیده است.",
})
description: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@ApiProperty({
type: "string",
description: "metaDescription of the product",
example:
"همواره گوشی‌های هوشمند پرچمدار سامسونگ توانسته‌اند با بهره بردن از مشخصات فنی قدرتمند، توجه هر بیننده‌ای را به خود جلب کنند و همواره رقیبان سرسختی برای برند‌های دیگر فعال در این عرصه بودند و این بار نوبت به قدرت‌نمایی سامسونگ Galaxy S24 Ultra رسیده است.",
})
metaDescription: string;
@Expose()
@IsNotEmpty()
@IsArray()
@ApiProperty({ type: "Array", description: "the array of product tags", example: ["mobile", "samsung"] })
tags: string[];
@Expose()
@IsOptional()
@IsNotEmpty()
@IsArray()
@ApiProperty({
type: "Array",
description: "advantages about product quality and features",
example: ["شارژدهی بالا", "دوربین عالی"],
})
advantages: string[];
@Expose()
@IsOptional()
@IsNotEmpty()
@IsArray()
@ApiProperty({
type: "Array",
description: "disAdvantages about product quality and features",
example: ["قیمت بالا", "بدون جک هدفون"],
})
disAdvantages: string[];
}
export class ProductFinalStepDTO {
@Expose()
@IsNotEmpty()
@IsNumber()
@IsValidId(ProductModel)
@ApiProperty({ type: "number", description: "the id of product ", example: "100002" })
productId: number;
@Expose()
@IsNotEmpty()
@IsArray()
@IsString({ each: true })
@ApiProperty({ type: "Array", description: "addresses of product images", example: ["https://cdnUrl.com", "https://cdnUrl.com"] })
imagesList: string[];
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "url address of product cover images", example: "https://cdnUrl.com" })
coverImage: string;
}
@@ -0,0 +1,155 @@
import { Expose } from "class-transformer";
import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId } from "../../../common/decorator/validation.decorator";
import { ProductSource } from "../../../common/enums/product.enum";
import { BrandModel } from "../../brand/models/brand.model";
import { CategoryModel } from "../../category/models/category.model";
// import { ProductRequestModel } from "../models/productRequest.model";
export class RequestProductDTO {
// @Expose()
// @IsNotEmpty()
// @IsValidId(ProductRequestModel)
// @ApiProperty({ type: "string", description: "payment id of the successful payment", example: "88ab8a37d09606beb22eb0d1" })
// payment: string;
@Expose()
@IsNotEmpty()
@IsValidId(CategoryModel)
@ApiProperty({ type: "string", description: "category id of the product", example: "66ab8a37d09606beb22eb2d1" })
category: string;
@Expose()
@IsNotEmpty()
@IsValidId(BrandModel)
@ApiProperty({ type: "string", description: "brand id of the product", example: "66ab8a37d09606beb22eb0d1" })
brand: string;
@Expose()
@IsNotEmpty()
@IsEnum(ProductSource)
@ApiProperty({ type: "string", description: "source of the product", example: "local" })
source: ProductSource;
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "name of product", example: "iphone13" })
productName: string;
// @Expose()
// @IsNotEmpty()
// @IsString()
// @ApiProperty({ type: "string", description: "type of product", example: "test" })
// productType: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@ApiProperty({
type: "string",
description: "description of the product",
example:
"همواره گوشی‌های هوشمند پرچمدار سامسونگ توانسته‌اند با بهره بردن از مشخصات فنی قدرتمند، توجه هر بیننده‌ای را به خود جلب کنند و همواره رقیبان سرسختی برای برند‌های دیگر فعال در این عرصه بودند و این بار نوبت به قدرت‌نمایی سامسونگ Galaxy S24 Ultra رسیده است.",
})
description?: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsArray()
@IsString({ each: true })
@ApiProperty({
type: "Array",
description: "advantages about product quality and features",
example: ["شارژدهی بالا", "دوربین عالی"],
})
advantages: string[];
@Expose()
@IsOptional()
@IsNotEmpty()
@IsArray()
@IsString({ each: true })
@ApiProperty({
type: "Array",
description: "disAdvantages about product quality and features",
example: ["قیمت بالا", "بدون جک هدفون"],
})
disAdvantages: string[];
//dimensions
//#############################
@Expose()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", description: "the weight of product package in gram", example: 250 })
package_weight: number;
@Expose()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", description: "the height of product package in cm", example: 25 })
package_height: number;
@Expose()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", description: "the length of product package in cm", example: 45 })
package_length: number;
@Expose()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", description: "the width of product package in cm", example: 10 })
package_width: number;
//#############################
@Expose()
@IsOptional()
@IsNotEmpty()
@IsBoolean()
@ApiProperty({ type: "boolean", description: "specify if request to take photo from admin", example: true })
requestPhotography: boolean;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", description: "the requested photos count", example: 5 })
requestPhotosCount: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsBoolean()
@ApiProperty({ type: "boolean", description: "specify if request to take unboxing video from admin", example: true })
unboxingVideo: boolean;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsBoolean()
@ApiProperty({ type: "boolean", description: "specify if request to review of product from admin", example: true })
expertReview: boolean;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "url address of product cover images", example: "https://cdnUrl.com" })
coverImage: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsArray()
@IsString({ each: true })
@ApiProperty({ type: "Array", description: "addresses of product images", example: ["https://cdnUrl.com", "https://cdnUrl.com"] })
imagesList?: string[];
}
@@ -0,0 +1,66 @@
import { Expose } from "class-transformer";
import { IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId, IsValidPersianDate } from "../../../common/decorator/validation.decorator";
import { AdType } from "../models/Abstraction/IProductAd";
import { ProductVariantModel } from "../models/productVariant.model";
export class ProductAdDTO {
@Expose()
@IsNotEmpty()
@IsString()
@IsValidId(ProductVariantModel)
@ApiProperty({ type: "string", description: "the variant id of product variant", example: "66ba2461d64d63a2494df185" })
variantId: string;
@Expose()
@IsNotEmpty()
@IsEnum(AdType)
@ApiProperty({ type: "string", description: "The type of advertisement (click or daily)", example: "click" })
type: AdType;
@Expose()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", description: "Cost per click or per day, depending on the ad type", example: 50 })
cost: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsNumber()
@ApiProperty({
type: "number",
description: "max allowed clicks for click-based ads",
example: 100,
required: false,
})
maxClicks?: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@IsValidPersianDate()
@ApiProperty({
type: "string",
description: "start date for daily-based ads ",
example: "1403/08/01 for => daily based",
required: false,
})
startDate?: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@IsValidPersianDate()
@ApiProperty({
type: "string",
description: "End date for daily-based ads => daily based ",
example: "1403/08/09",
required: false,
})
endDate?: string;
}
+106
View File
@@ -0,0 +1,106 @@
import { Expose } from "class-transformer";
import { IsNotEmpty, IsNumber, IsOptional, Min } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId } from "../../../common/decorator/validation.decorator";
import { ColorModel } from "../../category/models/color.model";
import { MeterageModel } from "../../category/models/meterage.model";
import { SizeModel } from "../../category/models/size.model";
import { WarrantyModel } from "../../warranty/models/warranty.model";
// import { ShipmentModel } from "../../../models/shipment.model";
export class AddVariantDTO {
// @Expose()
// @IsNotEmpty()
// @IsValidId(WarrantyModel)
// @ApiProperty({ type: "number", description: "product id", example: 100002 })
// productId: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsValidId(ColorModel)
@ApiProperty({ type: "number", description: "color id ==> one of color,size or meterage should send", example: 1 })
colorId?: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsValidId(SizeModel)
@ApiProperty({ type: "number", description: "size id ==> one of color,size or meterage should send", example: 5 })
sizeId?: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsValidId(MeterageModel)
@ApiProperty({ type: "number", description: "meterage id ==> one of color,size or meterage should send", example: 5 })
meterageId?: number;
@Expose()
@IsNotEmpty()
@IsValidId(WarrantyModel)
@ApiProperty({ type: "number", description: "warranty id of the product", example: 102 })
warrantyId: number;
@Expose()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", description: "maximum number of product can purchase in one order", example: 2 })
order_limit: number;
//
@Expose()
@IsOptional()
@IsNotEmpty()
@ApiProperty({ type: "string", description: "special code of product for seller only", example: "FPHK10" })
sellerSpecialCode?: string;
@Expose()
@IsNotEmpty()
@Min(10000, { message: "حداقل قیمت برای کالا ۱۰۰۰۰ تومان می باشد " })
@IsNumber()
@ApiProperty({ type: "number", description: "the price of product in toman", example: 5432510 })
retail_price: number;
//Dimensions
@Expose()
@IsNotEmpty()
@ApiProperty({ type: "number", description: "the weight of product package in gram", example: 250 })
package_weight: number;
@Expose()
@IsNotEmpty()
@ApiProperty({ type: "number", description: "the height of product package in cm", example: 25 })
package_height: number;
@Expose()
@IsNotEmpty()
@ApiProperty({ type: "number", description: "the length of product package in cm", example: 45 })
package_length: number;
@Expose()
@IsNotEmpty()
@ApiProperty({ type: "number", description: "the width of product package in cm", example: 10 })
package_width: number;
// @Expose()
// @IsNotEmpty()
// @IsArray()
// @IsNumber({}, { each: true })
// // @IsValidId(ShipmentModel)
// @ApiProperty({ type: "Array", description: "the array of shipment provider", example: [101, 102] })
// shipmentsId: number[];
//
@Expose()
@IsNotEmpty()
@ApiProperty({ type: "number", description: "the time take to ship product quantity of product in day", example: 10 })
postingTime: number;
//
@Expose()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", description: "the stock quantity of product", example: 10 })
stock: number;
}
@@ -0,0 +1,23 @@
import { Expose } from "class-transformer";
import { IsNotEmpty, IsString } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId } from "../../../common/decorator/validation.decorator";
import { ProductVariantModel } from "../models/productVariant.model";
export class AddWishlistDTO {
@Expose()
@IsNotEmpty()
@IsString()
@IsValidId(ProductVariantModel)
@ApiProperty({ type: "string", description: "the variant id of product", example: "66eadea49f6f5524f31f5b6f" })
variantId: string;
}
export class RemoveWishlistDTO {
@Expose()
@IsNotEmpty()
@IsString()
@IsValidId(ProductVariantModel)
@ApiProperty({ type: "string", description: "the variant id of product", example: "66eadea49f6f5524f31f5b6f" })
variantId: string;
}
@@ -0,0 +1,89 @@
import { Expose, Type } from "class-transformer";
import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString, Length, ValidateNested } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
export class CreateQuestionDTO {
@Expose()
@IsNotEmpty()
@IsString()
@Length(8)
@ApiProperty({ type: "string", description: "content of question", example: "جنسش جیه؟" })
content: string;
}
class AnswerContentDTO {
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({
type: "string",
description: "Content of the answer",
example: "جنس این محصول چرم است",
})
content: string;
}
export class AnswerQuestionDTO {
@Expose()
@IsArray()
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AnswerContentDTO)
@ApiProperty({
type: "Array",
description: "List of answers",
example: [{ content: "جنس این محصول چرم است" }],
required: false, // Optional field
})
answers?: AnswerContentDTO[];
}
export class CreateAnswerQuestionDTO {
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({
type: "string",
description: "Content of the answer",
example: "جنس این محصول چرم است",
})
content: string;
}
export class CreateCommentDTO {
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "the title of review", example: "عالی" })
title: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsArray()
@IsString({ each: true })
@ApiProperty({ type: "Array", description: "advantage", example: ["عالی", "پر سرعت", "ارزان"] })
advantage: string[];
@Expose()
@IsOptional()
@IsNotEmpty()
@IsArray()
@IsString({ each: true })
@ApiProperty({ type: "Array", description: "disAdvantage", example: ["بد", "کند", "گران"] })
disAdvantage: string[];
@Expose()
@IsNotEmpty()
@IsString()
@Length(8)
@ApiProperty({ type: "string", description: "content of comment", example: "عالی بود" })
content: string;
@Expose()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", description: "rate of product", example: 5 })
rate: number;
}
@@ -0,0 +1,47 @@
import { Expose, Type } from "class-transformer";
import { ArrayMinSize, IsArray, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId } from "../../../common/decorator/validation.decorator";
import { ReportQuestionModel } from "../models/reportQuestion.model";
class AnswerDTO {
@Expose()
@IsNotEmpty()
@IsNumber()
@IsValidId(ReportQuestionModel)
questionId: number;
}
export class CreateReportDTO {
@Expose()
@IsNotEmpty()
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => AnswerDTO)
@ApiProperty({
type: "Array",
description: "specify the answer of report questions",
example: [
{
questionId: 1,
},
{
questionId: 4,
},
],
})
answers: AnswerDTO[];
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@ApiProperty({
type: "string",
description: "description of the report ",
example: " کالا فیک می‌باشد اما در توضیحات اصلی نوشته شده است",
})
description: string;
}
@@ -0,0 +1,17 @@
import { IsNotEmpty, IsString, MinLength } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
export class CreateReportQuestionDTO {
@IsNotEmpty()
@IsString()
@MinLength(3)
@ApiProperty({ type: "string", description: "Title of the question", example: "نام کالا نادرست است" })
title: string;
// @IsNotEmpty()
// @IsString()
// @MinLength(3)
// @ApiProperty({ type: "string", description: "placeholder of the question", example: "" })
// placeholder: string;
}
+23
View File
@@ -0,0 +1,23 @@
import { Expose } from "class-transformer";
import { IsNotEmpty, IsString } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId } from "../../../common/decorator/validation.decorator";
import { ProductVariantModel } from "../models/productVariant.model";
export class AddObserverDTO {
@Expose()
@IsNotEmpty()
@IsString()
@IsValidId(ProductVariantModel)
@ApiProperty({ type: "string", description: "the variant id of product", example: "66eadea49f6f5524f31f5b6f" })
variantId: string;
}
export class RemoveObserverDTO {
@Expose()
@IsNotEmpty()
@IsString()
@IsValidId(ProductVariantModel)
@ApiProperty({ type: "string", description: "the variant id of product", example: "66eadea49f6f5524f31f5b6f" })
variantId: string;
}
+438
View File
@@ -0,0 +1,438 @@
import { Expose, Type, plainToInstance } from "class-transformer";
import { IsString } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { ProductStatus } from "../../../common/enums/product.enum";
import { BrandDTO } from "../../brand/DTO/brand.dto";
import { CategoryTreeDTO, ColorDTO, Size_MeterageDTO } from "../../category/DTO/category.dto";
import { ShipmentMethodDTO } from "../../shipment/DTO/shipment.dto";
import { ShopDTO } from "../../shop/DTO/shop.dto";
import { WarrantyDTO } from "../../warranty/DTO/warranty.dto";
import { IncredibleOffers } from "../models/Abstraction/IncredibleOffers";
import { IProduct } from "../models/Abstraction/IProduct";
import { IProductVariant } from "../models/Abstraction/IProductVariant";
//*********************************
class SpecificationsDTO {
@Expose()
title: string;
@Expose()
values: string[];
}
//*********************************
class ImagesUrlDTO {
@Expose()
cover: string;
@Expose()
list: string[];
}
//*********************************
class ProductPrice {
@Expose()
order_limit: number;
@Expose()
retailPrice: number;
@Expose()
selling_price: number;
// @Expose()
// shipping_fee: number;
//specialSale
@Expose()
is_specialSale: boolean;
@Expose()
discount_percent: number;
@Expose()
specialSale_order_limit: number;
@Expose()
specialSale_quantity: number;
@Expose()
specialSale_endDate: string;
}
//*********************************
class WholeSaleRangeDTO {
@Expose()
from: number;
@Expose()
to: number;
@Expose()
price: number;
}
//*********************************
class SaleFormatDTO {
// @Expose()
// isWholeSale: boolean;
@Expose()
@Type(() => WholeSaleRangeDTO)
wholeSale: WholeSaleRangeDTO[];
}
//#################################
export class ProductDetailVariantDTO {
@Expose()
_id: string;
@Expose()
market_status: string;
@Expose()
@Type(() => ProductPrice)
price: ProductPrice;
@Expose()
stock: number;
@Expose()
postingTime: number;
@Expose()
rate: number;
@Expose()
isFreeShip: boolean;
@Expose()
isWholeSale: boolean;
@Expose()
@Type(() => ShopDTO)
shop: ShopDTO;
@Expose()
@Type(() => SaleFormatDTO)
saleFormat: SaleFormatDTO;
@Expose()
@Type(() => ShipmentMethodDTO)
shipmentMethod: ShipmentMethodDTO[];
@Expose()
@Type(() => WarrantyDTO)
warranty: WarrantyDTO;
@Expose()
@Type(() => ColorDTO)
color?: ColorDTO;
@Expose()
@Type(() => Size_MeterageDTO)
size?: Size_MeterageDTO;
@Expose()
@Type(() => Size_MeterageDTO)
meterage?: Size_MeterageDTO;
}
//#################################
//#################################
export class SellerPanelProductDTO {
@Expose()
_id: number;
@Expose()
title_fa: string;
@Expose()
title_en: string;
@Expose()
seoTitle: string;
@Expose()
seoDescription: string;
@Expose()
description: string;
@Expose()
metaDescription: string;
@Expose()
shopName: string;
@Expose()
shopCode: string;
@Expose()
category_title_fa: string;
@Expose()
category_id: string;
@Expose()
brand_title_fa: string;
@Expose()
status: ProductStatus;
@Expose()
adminComments: string;
@Expose()
step: number;
@Expose()
isFake: boolean;
@Expose()
@Type(() => ImagesUrlDTO)
imagesUrl: ImagesUrlDTO;
@Expose()
variantCount: number;
@Expose()
@Type(() => ProductDetailVariantDTO)
default_variant: ProductDetailVariantDTO;
@Expose()
popular: boolean;
@Expose()
incredible: boolean;
@Expose()
voice: string;
public static transformProduct(data: IProduct): SellerPanelProductDTO {
const productDTO = plainToInstance(SellerPanelProductDTO, data, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
return productDTO;
}
}
//#################################
//#################################
export class ProductVariantDTO {
@Expose()
_id: string;
@Expose()
product_id: number;
@Expose()
title_fa: string;
@Expose()
title_en: string;
@Expose()
model: string;
@Expose()
market_status: string;
@Expose()
productCode: string;
@Expose()
category_title_fa: string;
@Expose()
is_variant_active: boolean;
@Expose()
coverImage: string;
@Expose()
@Type(() => ProductPrice)
price: ProductPrice;
@Expose()
stock: number;
@Expose()
sellerSpecialCode: string;
@Expose()
@Type(() => ShipmentMethodDTO)
shipmentMethod: ShipmentMethodDTO[];
@Expose()
product_status: string;
@Expose()
isFreeShip: boolean;
@Expose()
isWholeSale: boolean;
@Expose()
@Type(() => SaleFormatDTO)
saleFormat: SaleFormatDTO;
@Expose()
@Type(() => ColorDTO)
color?: ColorDTO;
@Expose()
@Type(() => Size_MeterageDTO)
size?: Size_MeterageDTO;
@Expose()
@Type(() => Size_MeterageDTO)
meterage?: Size_MeterageDTO;
public static transformProduct(data: IProductVariant): ProductVariantDTO {
const productVariantDTO = plainToInstance(ProductVariantDTO, data, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
return productVariantDTO;
}
}
// @Transform((value) => new Types.ObjectId(value.obj._id.toString()))
// _id: Types.ObjectId;
//#################################
//#################################
export class ProductDTO {
@Expose()
_id: number;
@Expose()
url: string;
@Expose()
title_fa: string;
@Expose()
title_en: string;
@Expose()
seoTitle: string;
@Expose()
seoDescription: string;
model: string;
@Expose()
source: string;
@Expose()
description: string;
@Expose()
metaDescription: string;
@Expose()
tags: string[];
@Expose()
advantages: string[];
@Expose()
disAdvantages: string[];
@Expose()
@Expose()
@Type(() => ImagesUrlDTO)
imagesUrl: ImagesUrlDTO;
@Expose()
isFake: string;
@Expose()
isWished: boolean;
@Expose()
voice: string;
@Expose()
@Type(() => SpecificationsDTO)
specifications: SpecificationsDTO[];
@Expose()
@Type(() => BrandDTO)
brand: BrandDTO;
@Expose()
@Type(() => CategoryTreeDTO)
category: CategoryTreeDTO;
@Expose()
@Type(() => ProductDetailVariantDTO)
default_variant: ProductDetailVariantDTO;
@Expose()
@Type(() => ProductDetailVariantDTO)
variants: ProductDetailVariantDTO[];
// @Expose()
// categoryPath: string;
public static transformProduct(data: IProduct): ProductDTO {
const productDTO = plainToInstance(ProductDTO, data, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
return productDTO;
}
}
export class RejectCommentDTO {
@Expose()
@IsString()
@ApiProperty({ type: "string", description: "reject reason", example: "reject for this reason" })
adminComments: string;
}
export class IncredibleOffersDTO {
@Expose()
@Type(() => ProductDTO)
product: ProductDTO;
@Expose()
@Type(() => ProductVariantDTO)
variant: ProductVariantDTO;
public static transformIncredibleOffers(data: IncredibleOffers): IncredibleOffersDTO {
const incredibleOffersDTO = plainToInstance(IncredibleOffersDTO, data, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
return incredibleOffersDTO;
}
}
export class ProductListDTO {
@Expose()
_id: number;
@Expose()
title_fa: string;
@Expose()
title_en: string;
@Expose()
seoTitle: string;
@Expose()
seoDescription: string;
}
@@ -0,0 +1,20 @@
import { Expose } from "class-transformer";
import { IsNotEmpty } from "class-validator";
import { IsValidId } from "../../../common/decorator/validation.decorator";
import { ProductModel } from "../models/product.model";
import { ProductVariantModel } from "../models/productVariant.model";
export class ProductParamDto {
@Expose()
@IsNotEmpty()
@IsValidId(ProductModel)
id: number;
}
export class SetFreeShipParamDto extends ProductParamDto {
@Expose()
@IsNotEmpty()
@IsValidId(ProductVariantModel)
variantId: string;
}
@@ -0,0 +1,90 @@
import { PartialType } from "@nestjs/mapped-types";
import { Expose } from "class-transformer";
import { IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator";
import { ProductFinalStepDTO, ProductStepOneDTO, ProductStepTwoDTO } from "./CreateProduct.dto";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId } from "../../../common/decorator/validation.decorator";
import { ProductModel } from "../models/product.model";
export class UpdateProductStepOneDTO extends PartialType(ProductStepOneDTO) {
@Expose()
@IsNotEmpty()
@IsNumber()
@IsValidId(ProductModel)
@ApiProperty({ type: "number", description: "the product id", example: 100004 })
productId: number;
}
export class UpdateProductStepTwoDTO extends PartialType(ProductStepTwoDTO) {
@Expose()
@IsNotEmpty()
@IsNumber()
@IsValidId(ProductModel)
@ApiProperty({ type: "number", description: "the product id", example: 100004 })
productId: number;
}
export class UpdateProductFinalStepDTO extends PartialType(ProductFinalStepDTO) {
@Expose()
@IsNotEmpty()
@IsNumber()
@IsValidId(ProductModel)
@ApiProperty({ type: "number", description: "the product id", example: 100004 })
productId: number;
}
export class AddVoiceToProductDTO {
@Expose()
@IsNotEmpty()
@ApiProperty({ type: "string", description: "the voice url", example: "http://voice.com" })
voice: string;
}
export class UpdateProductDTO {
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@ApiProperty({
type: "string",
description: "the title in farsi",
example: "گوشی موبایل سامسونگ مدل Galaxy S24 Ultra دو سیم کارت ظرفیت 256 گیگابایت و رم 12 گیگابایت - ویتنام ",
})
title_fa?: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@ApiProperty({
type: "string",
description: "the title in english",
example: "Samsung Galaxy S24 Ultra Dual SIM 256GB And 12GB RAM Mobile Phone - Vietnam",
})
title_en?: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@ApiProperty({
type: "string",
description: "description of the product",
example:
"همواره گوشی‌های هوشمند پرچمدار سامسونگ توانسته‌اند با بهره بردن از مشخصات فنی قدرتمند، توجه هر بیننده‌ای را به خود جلب کنند و همواره رقیبان سرسختی برای برند‌های دیگر فعال در این عرصه بودند و این بار نوبت به قدرت‌نمایی سامسونگ Galaxy S24 Ultra رسیده است.",
})
description?: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@ApiProperty({
type: "string",
description: "metaDescription of the product",
example:
"همواره گوشی‌های هوشمند پرچمدار سامسونگ توانسته‌اند با بهره بردن از مشخصات فنی قدرتمند، توجه هر بیننده‌ای را به خود جلب کنند و همواره رقیبان سرسختی برای برند‌های دیگر فعال در این عرصه بودند و این بار نوبت به قدرت‌نمایی سامسونگ Galaxy S24 Ultra رسیده است.",
})
metaDescription?: string;
}
@@ -0,0 +1,154 @@
import { Expose, Type } from "class-transformer";
import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, Min, ValidateNested } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId, IsValidPersianDate } from "../../../common/decorator/validation.decorator";
import { ProductDiscountType } from "../../../common/enums/product.enum";
import { ProductVariantModel } from "../models/productVariant.model";
//this is for saleFormat
class WholeSaleRangeDTO {
@Expose()
@IsNotEmpty()
@IsNumber()
from: number;
@Expose()
@IsNotEmpty()
@IsNumber()
to: number;
@Expose()
@IsNotEmpty()
@IsNumber()
@Min(10000, { message: "حداقل قیمت برای کالا ۱۰۰۰۰ تومان می باشد " })
price: number;
}
class SaleFormatDTO {
// @Expose()
// @IsNotEmpty()
// @IsBoolean()
// isWholeSale: boolean;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsArray()
@ValidateNested({ each: true })
@Type(() => WholeSaleRangeDTO)
wholeSale: WholeSaleRangeDTO[];
}
//this is for special sale
//======> this used for update product variant
export class UpdateVariantDTO {
@Expose()
@IsNotEmpty()
@IsString()
@IsValidId(ProductVariantModel)
@ApiProperty({ type: "string", description: "the variant id of product variant", example: "66ba2461d64d63a2494df185" })
variantId: string;
@Expose()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", description: "the price of product in toman", example: 5432510 })
retailPrice: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@Min(1)
@IsNumber()
@ApiProperty({ type: "number", description: "maximum number of product can purchase in one order", example: 2 })
order_limit: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@ApiProperty({ type: "string", description: "special code of product for seller only", example: "PKF120" })
sellerSpecialCode?: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", description: "the stock quantity of product", example: 10 })
stock: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@ValidateNested()
@Type(() => SaleFormatDTO)
@ApiProperty({
type: "Object",
description: "object to determine how seller want to sell product",
example: {
// isWholeSale: true,
wholeSale: [
{ from: 10, to: 50, price: 50000 },
{ from: 50, to: 100, price: 45000 },
],
},
})
saleFormat?: SaleFormatDTO;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsEnum(ProductDiscountType)
@ApiProperty({ type: "string", description: "the discount type for specialSale ==> fixed | percent", example: "fixed || percent" })
discount_type?: ProductDiscountType;
@Expose()
@IsOptional()
@Min(10000)
@IsNotEmpty()
@ApiProperty({ type: "number", description: "the discount value for specialSale if type is fixed", example: 50000 })
discount_value?: number;
@Expose()
@IsOptional()
@Min(1)
@IsNotEmpty()
@ApiProperty({ type: "number", description: "the discount percent for specialSale if type is percent", example: 40 })
discount_percent?: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@Min(1)
@ApiProperty({ type: "number", description: "the specialSale order limit", example: 2 })
specialSale_order_limit?: number;
@Expose()
@IsOptional()
@Min(1)
@IsNotEmpty()
@ApiProperty({ type: "number", description: "the product quantity for specialSale", example: 20 })
specialSale_quantity?: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsValidPersianDate()
@ApiProperty({ type: "string", description: "duration for special sale ", example: "1403/08/12" })
specialSale_endDate?: string;
}
export class ActivateVariantDTO {
@Expose()
@IsNotEmpty()
@IsString()
@IsValidId(ProductVariantModel)
@ApiProperty({ type: "string", description: "the variant id of product variant", example: "66ba2461d64d63a2494df185" })
variantId: string;
@Expose()
@IsNotEmpty()
@IsBoolean()
@ApiProperty({ type: "boolean", description: "Active status of the product variant", example: true })
isActive: boolean;
}
+109
View File
@@ -0,0 +1,109 @@
import { Types } from "mongoose";
import { BaseRepository } from "../../../common/base/repository";
import { ProductsCommentsQueries } from "../../../common/types/query.type";
import { IComment } from "../models/Abstraction/IComment";
import { CommentModel } from "../models/comment.model";
class CommentRepository extends BaseRepository<IComment> {
constructor() {
super(CommentModel);
}
async getUserComments(userId: string) {
return this.model.aggregate([
{
$match: {
user: new Types.ObjectId(userId),
},
},
{
$lookup: {
from: "products",
localField: "product",
foreignField: "_id",
as: "product",
},
},
{
$unwind: "$product",
},
{
$project: {
__v: 0,
updatedAt: 0,
product: {
comments: 0,
adminComments: 0,
brand: 0,
},
},
},
]);
}
async getProductsComments(queries: ProductsCommentsQueries) {
const page = queries.page || 1;
const limit = queries.limit || 10;
const skip = (page - 1) * limit;
const comments = await this.model.aggregate([
{
$match: {
...(queries.productId && {
product: queries.productId,
}),
...(queries.status && {
status: queries.status,
}),
},
},
{
$lookup: {
from: "products",
localField: "product",
foreignField: "_id",
as: "product",
},
},
{
$unwind: "$product",
},
{
$lookup: {
from: "users",
localField: "user",
foreignField: "_id",
as: "user",
},
},
{
$unwind: "$product",
},
{
$facet: {
data: [{ $skip: skip }, { $limit: limit }],
totalCount: [{ $count: "count" }],
},
},
{
$addFields: {
totalCount: { $arrayElemAt: ["$totalCount.count", 0] },
},
},
]);
return {
count: (comments[0]?.totalCount as number) || 0,
docs: comments[0]?.data || [],
};
}
async getCommentsForReport() {
return await this.model.find({ status: "pending" }).countDocuments();
}
}
function createCommentRepo(): CommentRepository {
return new CommentRepository();
}
export { createCommentRepo, CommentRepository };
@@ -0,0 +1,321 @@
import { Types } from "mongoose";
import { BaseRepository } from "../../../common/base/repository";
import { IncredibleOffers } from "../models/Abstraction/IncredibleOffers";
import { IncredibleOffersModel } from "../models/IncredibleOffers.model";
export class IncredibleOffersRepo extends BaseRepository<IncredibleOffers> {
constructor() {
super(IncredibleOffersModel);
}
async getIncredibleOffers(userId?: string) {
return this.model.aggregate([
{
$lookup: {
from: "products",
localField: "product",
foreignField: "_id",
as: "product",
},
},
...(userId
? [
{
$lookup: {
from: "wishlists",
let: { user: new Types.ObjectId(userId), productId: "$_id" },
pipeline: [
{
$match: {
$expr: {
$and: [{ $eq: ["$user", "$$user"] }, { $eq: ["$product", "$$productId"] }],
},
},
},
],
as: "isWished",
},
},
{
$addFields: {
isWished: { $gt: [{ $size: { $ifNull: ["$isWished", []] } }, 0] },
},
},
]
: []),
{
$unwind: "$product",
},
{
$lookup: {
from: "productvariants",
localField: "variant",
foreignField: "_id",
as: "product.default_variant",
},
},
{
$unwind: "$product.default_variant",
},
{
$lookup: {
from: "shops",
localField: "product.shop",
foreignField: "_id",
as: "product.shop",
},
},
{
$unwind: "$product.shop",
},
{
$lookup: {
from: "categories",
localField: "product.category",
foreignField: "_id",
as: "product.category",
},
},
{
$unwind: "$product.category",
},
{
$lookup: {
from: "brands",
localField: "product.brand",
foreignField: "_id",
as: "product.brand",
},
},
{
$unwind: "$product.brand",
},
{
$lookup: {
from: "shipments",
localField: "product.shop.shipmentMethod",
foreignField: "_id",
as: "product.default_variant.shipmentMethod",
},
},
{
$lookup: {
from: "warranties",
localField: "product.default_variant.warranty",
foreignField: "_id",
as: "product.default_variant.warranty",
},
},
{
$unwind: {
path: "$product.default_variant.warranty",
preserveNullAndEmptyArrays: true,
},
},
{
$lookup: {
from: "colors",
localField: "product.default_variant.color",
foreignField: "_id",
as: "product.default_variant.color",
},
},
{
$unwind: {
path: "$product.default_variant.color",
preserveNullAndEmptyArrays: true,
},
},
{
$lookup: {
from: "sizes",
localField: "product.default_variant.size",
foreignField: "_id",
as: "product.default_variant.size",
},
},
{
$unwind: {
path: "$product.default_variant.size",
preserveNullAndEmptyArrays: true,
},
},
{
$lookup: {
from: "meterages",
localField: "product.default_variant.meterage",
foreignField: "_id",
as: "product.default_variant.meterage",
},
},
{
$unwind: {
path: "$product.variant.meterage",
preserveNullAndEmptyArrays: true,
},
},
{
$limit: 10,
},
{
$sort: {
createdAt: -1,
},
},
]);
}
async getIncredibleOffersForAdmin(skip: number, limit: number) {
const docs = await this.model.aggregate([
{
$lookup: {
from: "products",
localField: "product",
foreignField: "_id",
as: "product",
},
},
{
$unwind: "$product",
},
{
$lookup: {
from: "productvariants",
localField: "variant",
foreignField: "_id",
as: "variant",
},
},
{
$unwind: "$variant",
},
{
$lookup: {
from: "shops",
localField: "product.shop",
foreignField: "_id",
as: "product.shop",
},
},
{
$unwind: "$product.shop",
},
{
$lookup: {
from: "categories",
localField: "product.category",
foreignField: "_id",
as: "product.category",
},
},
{
$unwind: "$product.category",
},
{
$lookup: {
from: "brands",
localField: "product.brand",
foreignField: "_id",
as: "product.brand",
},
},
{
$unwind: "$product.brand",
},
{
$lookup: {
from: "shipments",
localField: "product.shop.shipmentMethod",
foreignField: "_id",
as: "variant.shipmentMethod",
},
},
{
$lookup: {
from: "warranties",
localField: "variant.warranty",
foreignField: "_id",
as: "variant.warranty",
},
},
{
$unwind: {
path: "$variant.warranty",
preserveNullAndEmptyArrays: true,
},
},
{
$lookup: {
from: "colors",
localField: "variant.color",
foreignField: "_id",
as: "variant.color",
},
},
{
$unwind: {
path: "$variant.color",
preserveNullAndEmptyArrays: true,
},
},
{
$lookup: {
from: "sizes",
localField: "variant.size",
foreignField: "_id",
as: "variant.size",
},
},
{
$unwind: {
path: "$variant.size",
preserveNullAndEmptyArrays: true,
},
},
{
$lookup: {
from: "meterages",
localField: "variant.meterage",
foreignField: "_id",
as: "variant.meterage",
},
},
{
$unwind: {
path: "$variant.meterage",
preserveNullAndEmptyArrays: true,
},
},
{
$facet: {
data: [{ $skip: skip }, { $limit: limit }],
totalCount: [{ $count: "count" }],
},
},
{
$addFields: {
totalCount: { $arrayElemAt: ["$totalCount.count", 0] },
},
},
{
$sort: {
createdAt: -1,
},
},
]);
return {
count: (docs?.[0]?.totalCount as number) || 0,
incredibleOffers: docs?.[0]?.data || [],
};
}
}
export function createIncredibleOffersRepo(): IncredibleOffersRepo {
return new IncredibleOffersRepo();
}
@@ -0,0 +1,138 @@
import { Types } from "mongoose";
import { BaseRepository } from "../../../common/base/repository";
import { IPopularProduct } from "../models/Abstraction/IPopular";
import { PopularProductModel } from "../models/popularProducts.model";
class PopularProductRepo extends BaseRepository<IPopularProduct> {
constructor() {
super(PopularProductModel);
}
async getPopularProducts(userId?: string) {
if (userId) {
return this.model.aggregate([
{
$lookup: {
from: "products",
localField: "product",
foreignField: "_id",
as: "product",
},
},
{
$unwind: "$product",
},
{
$lookup: {
from: "shops",
localField: "product.shop",
foreignField: "_id",
as: "product.shop",
},
},
{
$lookup: {
from: "wishlists",
let: { user: new Types.ObjectId(userId), productId: "$product._id" },
pipeline: [
{
$match: {
$expr: {
$and: [{ $eq: ["$user", "$$user"] }, { $eq: ["$product", "$$productId"] }],
},
},
},
],
as: "isWished",
},
},
{
$addFields: {
isWished: { $gt: [{ $size: { $ifNull: ["$isWished", []] } }, 0] },
},
},
{
$project: {
__v: 0,
updatedAt: 0,
product: {
comments: 0,
adminComments: 0,
brand: 0,
category: 0,
variants: 0,
description: 0,
metaDescription: 0,
tags: 0,
advantages: 0,
disAdvantages: 0,
specifications: 0,
__v: 0,
shop: {
telephoneNumber: 0,
shopNationalId: 0,
shopPostalCode: 0,
shipmentMethod: 0,
__v: 0,
},
},
},
},
]);
}
return this.model.aggregate([
{
$lookup: {
from: "products",
localField: "product",
foreignField: "_id",
as: "product",
},
},
{
$unwind: "$product",
},
{
$lookup: {
from: "shops",
localField: "product.shop",
foreignField: "_id",
as: "product.shop",
},
},
{
$project: {
__v: 0,
updatedAt: 0,
product: {
comments: 0,
adminComments: 0,
brand: 0,
category: 0,
variants: 0,
description: 0,
metaDescription: 0,
tags: 0,
advantages: 0,
disAdvantages: 0,
specifications: 0,
__v: 0,
shop: {
telephoneNumber: 0,
shopNationalId: 0,
shopPostalCode: 0,
shipmentMethod: 0,
__v: 0,
},
},
},
},
]);
}
}
function createPopularProductRepo(): PopularProductRepo {
return new PopularProductRepo();
}
export { PopularProductRepo, createPopularProductRepo };
@@ -0,0 +1,65 @@
import { BaseRepository } from "../../../common/base/repository";
import { IPriceHistory } from "../models/Abstraction/IPriceHistory";
import { PriceHistoryModel } from "../models/priceHistory.model";
class PriceHistoryRepo extends BaseRepository<IPriceHistory> {
constructor() {
super(PriceHistoryModel);
}
async getPriceHistory(productId: number, since?: string) {
return this.model.aggregate([
{
$match: {
product: productId,
},
},
{
$lookup: {
from: "shops",
localField: "history.shop",
foreignField: "_id",
as: "shop",
},
},
{
$unwind: "$shop",
},
{
$addFields: {
"history.shop": "$shop.shopName",
// "history.date": {
// $dateFromString: {
// dateString: "$history.date", // Convert string to Date
// },
// },
},
},
...(since
? [
{
$match: {
"history.date": {
$gte: since,
},
},
},
]
: []),
{
$project: {
createdAt: 0,
updatedAt: 0,
shop: 0,
__v: 0,
},
},
]);
}
}
function createPriceHistoryRepo(): PriceHistoryRepo {
return new PriceHistoryRepo();
}
export { createPriceHistoryRepo, PriceHistoryRepo };
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
import { BaseRepository } from "../../../common/base/repository";
import { IProductAd } from "../models/Abstraction/IProductAd";
import { ProductAdModel } from "../models/productAd.model";
class ProductAdRepo extends BaseRepository<IProductAd> {
constructor() {
super(ProductAdModel);
}
}
function createProductAdRepo(): ProductAdRepo {
return new ProductAdRepo();
}
export { ProductAdRepo, createProductAdRepo };
@@ -0,0 +1,15 @@
import { BaseRepository } from "../../../common/base/repository";
import { IProductObserve } from "../models/Abstraction/IProductObserve";
import { ProductObserveModel } from "../models/productObserve.model";
class ProductObserveRepo extends BaseRepository<IProductObserve> {
constructor() {
super(ProductObserveModel);
}
}
function createProductObserveRepo(): ProductObserveRepo {
return new ProductObserveRepo();
}
export { ProductObserveRepo, createProductObserveRepo };
@@ -0,0 +1,15 @@
import { BaseRepository } from "../../../common/base/repository";
import { IProductReport } from "../models/Abstraction/IProductReport";
import { ProductReportModel } from "../models/productReport.model";
class ProductReportRepo extends BaseRepository<IProductReport> {
constructor() {
super(ProductReportModel);
}
}
function createProductReportRepo(): ProductReportRepo {
return new ProductReportRepo();
}
export { ProductReportRepo, createProductReportRepo };
@@ -0,0 +1,17 @@
import { BaseRepository } from "../../../common/base/repository";
import { IProductRequest } from "../models/Abstraction/IProductRequest";
import { ProductRequestModel } from "../models/productRequest.model";
class ProductRequestRepo extends BaseRepository<IProductRequest> {
constructor() {
super(ProductRequestModel);
}
}
function createProductRequestRepo(): ProductRequestRepo {
return new ProductRequestRepo();
}
export { createProductRequestRepo, ProductRequestRepo };
// { $group: { _id: null, count: { $sum: 1 }, items: { $push: "$$ROOT" } } },
@@ -0,0 +1,486 @@
import { Types, isValidObjectId } from "mongoose";
import { BaseRepository } from "../../../common/base/repository";
import { ProductMarketStatus } from "../../../common/enums/product.enum";
import { VariantQueries } from "../../../common/types/query.type";
import { IProductVariant } from "../models/Abstraction/IProductVariant";
import { ProductVariantModel } from "../models/productVariant.model";
export class ProductVariantRepository extends BaseRepository<IProductVariant> {
constructor() {
super(ProductVariantModel);
}
async getProductVariants(shopId: string, productId: number, queries: VariantQueries) {
const page = queries.page || 1;
const limit = queries.limit || 10;
const skip = (page - 1) * limit;
const matchQuery: any = {
shop: new Types.ObjectId(shopId),
product: productId,
};
const sortBy: any = {
createdAt: 1,
};
if (queries.q) {
matchQuery.$or = [{ _id: isValidObjectId(queries.q) ? new Types.ObjectId(queries.q) : undefined }, { sellerSpecialCode: queries.q }];
}
if (queries.includeAd) {
matchQuery.searchAd = { $exists: true };
}
// if (queries.shipmentMethod) {
// matchQuery.shipmentMethod = { $eq: queries.shipmentMethod };
// }
if (queries.specialSale) {
matchQuery["price.is_specialSale"] = true;
}
if (queries.stock) {
matchQuery.stock = { $gt: 0 };
}
if (queries.variantStatus) {
matchQuery.market_status = ProductMarketStatus.Marketable;
}
if (Array.isArray(queries.sort)) {
queries.sort.forEach((value) => (sortBy[value] = -1));
} else if (queries.sort) {
sortBy[queries.sort] = -1;
}
const docs = await this.model.aggregate([
{
$match: matchQuery,
},
{
$lookup: {
from: "products",
localField: "product",
foreignField: "_id",
as: "product",
},
},
{
$unwind: "$product",
},
{
$lookup: {
from: "shops",
localField: "shop",
foreignField: "_id",
as: "shop",
},
},
{
$unwind: "$shop",
},
{
$lookup: {
from: "categories",
localField: "product.category",
foreignField: "_id",
as: "category",
},
},
{
$unwind: "$category",
},
{
$lookup: {
from: "colors",
localField: "color",
foreignField: "_id",
as: "color",
},
},
{
$lookup: {
from: "sizes",
localField: "size",
foreignField: "_id",
as: "size",
},
},
{
$lookup: {
from: "meterages",
localField: "meterage",
foreignField: "_id",
as: "meterage",
},
},
{
$lookup: {
from: "shipments",
localField: "shop.shipmentMethod",
foreignField: "_id",
as: "shipmentMethod",
},
},
{
$unwind: {
path: "$color",
preserveNullAndEmptyArrays: true,
},
},
{
$unwind: {
path: "$size",
preserveNullAndEmptyArrays: true,
},
},
{
$unwind: {
path: "$meterage",
preserveNullAndEmptyArrays: true,
},
},
{
$addFields: {
product_id: "$product._id",
productCode: { $concat: ["SHP-", { $toString: "$product._id" }] },
product_status: "$product.status",
title_fa: "$product.title_fa",
title_en: "$product.title_en",
model: "$product.model",
category_theme: "$category.theme",
category_title_fa: "$category.title_fa",
coverImage: "$product.imagesUrl.cover",
is_variant_active: {
$cond: {
if: { $eq: ["$market_status", ProductMarketStatus.Marketable] },
then: true,
else: false,
},
},
},
},
{
$sort: sortBy,
},
{
$facet: {
data: [{ $skip: skip }, { $limit: limit }],
totalCount: [{ $count: "count" }],
},
},
{
$addFields: {
totalCount: { $arrayElemAt: ["$totalCount.count", 0] },
},
},
{
$project: {
category: 0,
product: 0,
shop: 0,
},
},
]);
return {
count: (docs[0]?.totalCount as number) || 0,
docs: docs[0]?.data || [],
};
}
async getAllProductsVariants(shopId: string, queries: VariantQueries) {
const page = queries.page || 1;
const limit = queries.limit || 10;
const skip = (page - 1) * limit;
const matchQuery: any = {
shop: new Types.ObjectId(shopId),
};
const sortBy: any = {
createdAt: 1,
};
if (queries.q) {
matchQuery.$or = [{ _id: isValidObjectId(queries.q) ? new Types.ObjectId(queries.q) : undefined }, { sellerSpecialCode: queries.q }];
}
if (queries.includeAd) {
matchQuery.searchAd = { $exists: true };
}
// if (queries.shipmentMethod) {
// matchQuery.shipmentMethod = { $eq: queries.shipmentMethod };
// }
if (queries.specialSale) {
matchQuery["price.is_specialSale"] = true;
}
if (queries.stock) {
matchQuery.stock = { $gt: 0 };
}
if (queries.variantStatus) {
matchQuery.market_status = ProductMarketStatus.Marketable;
}
if (Array.isArray(queries.sort)) {
queries.sort.forEach((value) => (sortBy[value] = -1));
} else if (queries.sort) {
sortBy[queries.sort] = -1;
}
const docs = await this.model.aggregate([
{
$match: matchQuery,
},
{
$lookup: {
from: "products",
localField: "product",
foreignField: "_id",
as: "product",
},
},
{
$unwind: "$product",
},
{
$lookup: {
from: "shops",
localField: "shop",
foreignField: "_id",
as: "shop",
},
},
{
$unwind: "$shop",
},
{
$lookup: {
from: "categories",
localField: "product.category",
foreignField: "_id",
as: "category",
},
},
{
$unwind: "$category",
},
{
$lookup: {
from: "colors",
localField: "color",
foreignField: "_id",
as: "color",
},
},
{
$lookup: {
from: "sizes",
localField: "size",
foreignField: "_id",
as: "size",
},
},
{
$lookup: {
from: "meterages",
localField: "meterage",
foreignField: "_id",
as: "meterage",
},
},
{
$lookup: {
from: "shipments",
localField: "shop.shipmentMethod",
foreignField: "_id",
as: "shipmentMethod",
},
},
{
$unwind: {
path: "$color",
preserveNullAndEmptyArrays: true,
},
},
{
$unwind: {
path: "$size",
preserveNullAndEmptyArrays: true,
},
},
{
$unwind: {
path: "$meterage",
preserveNullAndEmptyArrays: true,
},
},
{
$addFields: {
product_id: "$product._id",
productCode: { $concat: ["SHP-", { $toString: "$product._id" }] },
product_status: "$product.status",
title_fa: "$product.title_fa",
title_en: "$product.title_en",
model: "$product.model",
category_theme: "$category.theme",
category_title_fa: "$category.title_fa",
coverImage: "$product.imagesUrl.cover",
is_variant_active: {
$cond: {
if: { $eq: ["$market_status", ProductMarketStatus.Marketable] },
then: true,
else: false,
},
},
},
},
{
$sort: sortBy,
},
{
$facet: {
data: [{ $skip: skip }, { $limit: limit }],
totalCount: [{ $count: "count" }],
},
},
{
$addFields: {
totalCount: { $arrayElemAt: ["$totalCount.count", 0] },
},
},
{
$project: {
category: 0,
product: 0,
shop: 0,
},
},
]);
return {
count: (docs[0]?.totalCount as number) || 0,
docs: docs[0]?.data || [],
};
}
async getSingleVariant(shopId: string, productId: number, variantId: string) {
return this.model.aggregate([
{
$match: {
_id: new Types.ObjectId(variantId),
shop: new Types.ObjectId(shopId),
product: productId,
},
},
{
$lookup: {
from: "products",
localField: "product",
foreignField: "_id",
as: "product",
},
},
{
$unwind: "$product",
},
{
$lookup: {
from: "shops",
localField: "shop",
foreignField: "_id",
as: "shop",
},
},
{
$unwind: "$shop",
},
{
$lookup: {
from: "categories",
localField: "product.category",
foreignField: "_id",
as: "category",
},
},
{
$unwind: "$category",
},
{
$lookup: {
from: "colors",
localField: "color",
foreignField: "_id",
as: "color",
},
},
{
$lookup: {
from: "sizes",
localField: "size",
foreignField: "_id",
as: "size",
},
},
{
$lookup: {
from: "meterages",
localField: "meterage",
foreignField: "_id",
as: "meterage",
},
},
{
$lookup: {
from: "shipments",
localField: "shop.shipmentMethod",
foreignField: "_id",
as: "shipmentMethod",
},
},
{
$unwind: {
path: "$color",
preserveNullAndEmptyArrays: true,
},
},
{
$unwind: {
path: "$size",
preserveNullAndEmptyArrays: true,
},
},
{
$unwind: {
path: "$meterage",
preserveNullAndEmptyArrays: true,
},
},
{
$addFields: {
product_id: "$product._id",
productCode: { $concat: ["SHP-", { $toString: "$product._id" }] },
product_status: "$product.status",
category_theme: "$category.theme",
title_fa: "$product.title_fa",
title_en: "$product.title_en",
model: "$product.model",
category_title_fa: "$category.title_fa",
coverImage: "$product.imagesUrl.cover",
is_variant_active: {
$cond: {
if: { $eq: ["$market_status", ProductMarketStatus.Marketable] },
then: true,
else: false,
},
},
},
},
{
$project: {
category: 0,
product: 0,
shop: 0,
},
},
]);
}
}
export function createProductVariantRepo(): ProductVariantRepository {
return new ProductVariantRepository();
}
@@ -0,0 +1,90 @@
import { Types } from "mongoose";
import { BaseRepository } from "../../../common/base/repository";
import { Question_CommentStatus } from "../../../common/enums/question_comment.enum";
import { ProductsQuestionsQueries } from "../../../common/types/query.type";
import { IQuestion } from "../models/Abstraction/IQuestion";
import { QuestionModel } from "../models/question.model";
class QuestionRepository extends BaseRepository<IQuestion> {
constructor() {
super(QuestionModel);
}
async getUserQuestions(userId: string) {
return this.model.aggregate([
{
$match: {
user: new Types.ObjectId(userId),
},
},
]);
}
async getProductsQuestions(queries: ProductsQuestionsQueries) {
const page = queries.page || 1;
const limit = queries.limit || 10;
const skip = (page - 1) * limit;
const questions = await this.model.aggregate([
{
$match: {
...(queries.productId && {
product: queries.productId,
}),
...(queries.status && {
status: queries.status,
}),
},
},
{
$lookup: {
from: "products",
localField: "product",
foreignField: "_id",
as: "product",
},
},
{
$unwind: "$product",
},
{
$lookup: {
from: "users",
localField: "user",
foreignField: "_id",
as: "user",
},
},
{
$unwind: "$product",
},
{
$facet: {
data: [{ $skip: skip }, { $limit: limit }],
totalCount: [{ $count: "count" }],
},
},
{
$addFields: {
totalCount: { $arrayElemAt: ["$totalCount.count", 0] },
},
},
]);
return {
count: (questions[0]?.totalCount as number) || 0,
docs: questions[0]?.data || [],
};
}
async getQuestionsForReport() {
const questions = await this.model.find({ status: Question_CommentStatus.Pending }).countDocuments();
return questions;
}
}
function createQuestionRepo(): QuestionRepository {
return new QuestionRepository();
}
export { QuestionRepository, createQuestionRepo };
@@ -0,0 +1,15 @@
import { BaseRepository } from "../../../common/base/repository";
import { IReportQuestion } from "../models/Abstraction/IReportQuestion";
import { ReportQuestionModel } from "../models/reportQuestion.model";
class ReportQuestionRepo extends BaseRepository<IReportQuestion> {
constructor() {
super(ReportQuestionModel);
}
}
function createReportQuestionRepo(): ReportQuestionRepo {
return new ReportQuestionRepo();
}
export { ReportQuestionRepo, createReportQuestionRepo };
@@ -0,0 +1,14 @@
import { Types } from "mongoose";
import { Question_CommentStatus } from "../../../../common/enums/question_comment.enum";
export interface IComment {
title: string;
advantage: string[];
disAdvantage: string[];
content: string;
status: Question_CommentStatus;
user: Types.ObjectId;
product: number;
rate: number;
}
@@ -0,0 +1,4 @@
export interface IPopularProduct {
product: number;
isActive: boolean; // Whether the popular is currently active
}
@@ -0,0 +1,15 @@
import { Types } from "mongoose";
export interface IHistory {
shop: Types.ObjectId;
selling_price: number;
retail_price: number;
date: string;
}
export interface IPriceHistory {
_id: number;
product: number;
title: string; //variant title
history: IHistory[];
}
@@ -0,0 +1,42 @@
import { Types } from "mongoose";
import { CreateProductStep, ProductSource, ProductStatus } from "../../../../common/enums/product.enum";
export interface IProduct {
_id: number;
title_fa: string;
title_en: string;
seoTitle: string;
seoDescription: string;
model: string;
source: ProductSource;
description: string;
metaDescription: string;
advantages: Array<string>;
disAdvantages: Array<string>;
rate: number;
totalRate: number;
tags: string[];
specifications: ISpecifications[];
category: Types.ObjectId;
brand: Types.ObjectId;
shop: Types.ObjectId;
imagesUrl: IProductImage;
status: ProductStatus;
adminComments: string;
step: CreateProductStep;
isFake: boolean;
//TODO:delete this field later
variants: Types.ObjectId[];
voice?: string;
deleted: boolean;
}
export interface IProductImage {
cover: string;
list: string[];
}
export interface ISpecifications {
title: string;
values: string[];
}
@@ -0,0 +1,19 @@
import { Types } from "mongoose";
export enum AdType {
Click = "click",
Daily = "daily",
}
export interface IProductAd {
product: number;
variant: Types.ObjectId;
seller: Types.ObjectId;
type: AdType; // type of ad: click-based or duration-based
cost: number; // Ad cost per click or for the entire duration
clickCount: number; // Number of clicks (only for click-based ads)
maxClicks: number; // Max allowed clicks (for click-based ads)
startDate: string; // Start date (for duration-based ads)
endDate: string; // End date (for duration-based ads)
isActive: boolean; // Whether the ad is currently active
}
@@ -0,0 +1,8 @@
import { Types } from "mongoose";
export interface IProductObserve {
user: Types.ObjectId;
product: number;
variant: Types.ObjectId;
registeredPrice: number;
}
@@ -0,0 +1,11 @@
import { Types } from "mongoose";
import { StatusEnum } from "../../../../common/enums/status.enum";
export interface IProductReport {
user: Types.ObjectId;
product: number;
reason: number[];
description: string;
status: StatusEnum;
}
@@ -0,0 +1,28 @@
import { Types } from "mongoose";
import { IProductImage } from "./IProduct";
import { IDimensions } from "./IProductVariant";
import { ProductSource, ProductStatus } from "../../../../common/enums/product.enum";
export interface IProductRequest {
seller: Types.ObjectId;
shop: Types.ObjectId;
brand: Types.ObjectId;
category: Types.ObjectId;
source: ProductSource;
productName: string;
productType: string;
description: string;
dimensions: IDimensions;
advantages: Array<string>;
disAdvantages: Array<string>;
//
requestPhotography: boolean;
requestPhotosCount: number;
expertReview: boolean;
unboxingVideo: boolean;
imagesUrl: IProductImage;
payment: Types.ObjectId;
requestStatus: ProductStatus;
adminComments: string;
}
@@ -0,0 +1,64 @@
import { Types } from "mongoose";
import { ProductDiscountType, ProductMarketStatus } from "../../../../common/enums/product.enum";
export interface IProductVariant {
_id: Types.ObjectId;
product: number;
statistics: IStatistics;
warranty: number;
shop: Types.ObjectId;
sellerCode: string;
market_status: ProductMarketStatus;
postingTime: number;
price: IPrice;
stock: number;
order_limit: number;
isWholeSale: boolean;
isFreeShip: boolean;
saleFormat: ISaleFormat;
dimensions: IDimensions;
sellerSpecialCode: string;
color?: number;
size?: number;
meterage?: number;
}
export interface IPrice {
order_limit: number;
retailPrice: number;
selling_price: number;
//specialSale
is_specialSale: boolean;
discount_percent: number;
specialSale_order_limit: number;
specialSale_quantity: number;
specialSale_endDate: string;
}
//TODO:check for type
export interface ISpecialSale {
endDate: string;
type: ProductDiscountType;
discountValue: number;
order_limit: number;
quantity: number;
priceAfterDiscount: number;
}
export interface IStatistics {
satisfied: { rate: number; count: number };
dissatisfied: { rate: number; count: number };
}
export interface ISaleFormat {
// isWholeSale: boolean;
wholeSale: Array<{ from: number; to: number; price: number }>;
}
export interface IDimensions {
package_weight: number;
package_height: number;
package_length: number;
package_width: number;
}
@@ -0,0 +1,18 @@
import { Types } from "mongoose";
import { Question_CommentStatus } from "../../../../common/enums/question_comment.enum";
import { OwnerRef } from "../../../shop/models/Abstraction/IShop";
export interface IQuestion {
content: string;
status: Question_CommentStatus;
user: Types.ObjectId;
product: number;
answers?: IAnswer[];
}
export interface IAnswer {
content: string;
owner: Types.ObjectId;
ownerRef: OwnerRef;
}
@@ -0,0 +1,14 @@
export enum ReportQuestionType {
TEXT = "text",
SELECT = "select",
RADIO = "radio",
CHECKBOX = "checkbox",
}
export interface IReportQuestion {
_id: number;
title: string;
type: ReportQuestionType;
placeholder: string;
// related_questions: number[];
}
@@ -0,0 +1,6 @@
import { Types } from "mongoose";
export interface IncredibleOffers {
product: number;
variant: Types.ObjectId;
}
@@ -0,0 +1,14 @@
import { Schema, model } from "mongoose";
import { IncredibleOffers } from "./Abstraction/IncredibleOffers";
const IncredibleOffersSchema = new Schema<IncredibleOffers>(
{
product: { type: Number, ref: "Product", required: true },
variant: { type: Schema.Types.ObjectId, ref: "ProductVariant", required: true },
},
{ timestamps: true, toJSON: { versionKey: false, virtuals: true }, id: false },
);
const IncredibleOffersModel = model<IncredibleOffers>("IncredibleOffers", IncredibleOffersSchema);
export { IncredibleOffersModel };
@@ -0,0 +1,39 @@
import { Schema, model } from "mongoose";
import { IComment } from "./Abstraction/IComment";
import { Question_CommentStatus } from "../../../common/enums/question_comment.enum";
const CommentSchema = new Schema<IComment>(
{
title: { type: String, required: true },
advantage: { type: [String] },
disAdvantage: { type: [String] },
content: { type: String, required: true },
status: { type: String, enum: Question_CommentStatus, default: Question_CommentStatus.Pending },
user: { type: Schema.Types.ObjectId, ref: "User", required: true },
product: { type: Number, ref: "Product", required: true },
rate: { type: Number, required: true, min: 1, max: 5 },
},
{
timestamps: true,
toJSON: { virtuals: true, versionKey: false },
id: false,
},
);
CommentSchema.pre(["find", "findOne"], function (next) {
this.populate({ path: "user" });
next();
});
CommentSchema.pre(["find", "findOne"], function (next) {
this.populate({
path: "product",
populate: ["brand", "category", "shop"],
select: { adminComments: 0, variants: 0 },
});
next();
});
const CommentModel = model<IComment>("Comment", CommentSchema);
export { CommentModel };
@@ -0,0 +1,14 @@
import { Schema, model } from "mongoose";
import { IPopularProduct } from "./Abstraction/IPopular";
const PopularProductSchema = new Schema<IPopularProduct>(
{
product: { type: Number, ref: "Product", required: true },
isActive: { type: Boolean, default: true }, // Whether popular is currently active
},
{ timestamps: true, toJSON: { versionKey: false, virtuals: true }, id: false },
);
const PopularProductModel = model<IPopularProduct>("PopularProduct", PopularProductSchema);
export { PopularProductModel };
@@ -0,0 +1,31 @@
import mongoose, { Schema, model } from "mongoose";
import { IHistory, IPriceHistory } from "./Abstraction/IPriceHistory";
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const AutoIncrement = require("mongoose-sequence")(mongoose);
const HistorySchema = new Schema<IHistory>(
{
shop: { type: Schema.Types.ObjectId, ref: "Shop", required: true },
selling_price: { type: Number, required: true },
retail_price: { type: Number, required: true },
date: { type: String },
},
{ _id: false },
);
const PriceHistorySchema = new Schema<IPriceHistory>(
{
_id: Number,
product: { type: Number, ref: "Product", required: true },
title: { type: String, default: "" },
history: { type: [HistorySchema], default: [] },
},
{ timestamps: true, toJSON: { versionKey: false, virtuals: true }, _id: false, id: false },
);
PriceHistorySchema.plugin(AutoIncrement, { id: "priceHistory_id", inc_field: "_id" });
const PriceHistoryModel = model<IPriceHistory>("PriceHistory", PriceHistorySchema);
export { PriceHistoryModel };
@@ -0,0 +1,70 @@
import mongoose, { Schema, model } from "mongoose";
import { IProduct, IProductImage, ISpecifications } from "./Abstraction/IProduct";
import { CreateProductStep, ProductSource, ProductStatus } from "../../../common/enums/product.enum";
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const AutoIncrement = require("mongoose-sequence")(mongoose);
const productSpecifications = new Schema<ISpecifications>(
{
title: String,
values: { type: [String] },
},
{ id: false, _id: false },
);
export const productImageSchema = new Schema<IProductImage>(
{
cover: String,
list: { type: [String] },
},
{ id: false, _id: false },
);
const productSchema = new Schema<IProduct>(
{
_id: Number,
title_fa: { type: String, required: true },
title_en: { type: String, required: true },
seoTitle: { type: String, default: null },
seoDescription: { type: String, default: null },
model: { type: String, required: true },
source: { type: String, enum: ProductSource, default: ProductSource.LOCAL, required: true },
description: { type: String, default: null },
metaDescription: { type: String, default: null },
tags: { type: [String], default: [] },
advantages: { type: [String], default: [] },
disAdvantages: { type: [String], default: [] },
rate: { type: Number },
totalRate: { type: Number, default: 0 },
specifications: { type: [productSpecifications], default: [] },
category: { type: Schema.Types.ObjectId, ref: "Category", required: true },
shop: { type: Schema.Types.ObjectId, ref: "Shop", required: true },
brand: { type: Schema.Types.ObjectId, ref: "Brand", required: true },
imagesUrl: { type: productImageSchema },
status: { type: String, enum: ProductStatus, default: ProductStatus.Draft },
adminComments: { type: String, default: null },
step: { type: Number, enum: CreateProductStep, default: CreateProductStep.Detail },
isFake: { type: Boolean, required: true },
variants: { type: [Schema.Types.ObjectId], ref: "ProductVariant" },
voice: { type: String, default: null },
deleted: { type: Boolean, default: false },
},
{ timestamps: true, toJSON: { virtuals: true, versionKey: false }, _id: false, id: false },
);
productSchema.virtual("url").get(function () {
return `/product/SHP-${this._id}/${this.title_fa}`;
});
// productSchema.pre("findOneAndDelete", async function (next) {
// const productId = this.getQuery()["_id"];
// await mongoose.model("ProductVariant").deleteMany({ product: productId });
// next();
// });
productSchema.plugin(AutoIncrement, { id: "product_id", start_seq: 100000 });
const ProductModel = model<IProduct>("Product", productSchema);
export { ProductModel };
@@ -0,0 +1,22 @@
import { Schema, model } from "mongoose";
import { AdType, IProductAd } from "./Abstraction/IProductAd";
const ProductAdSchema = new Schema<IProductAd>(
{
product: { type: Number, ref: "Product", required: true },
variant: { type: Schema.Types.ObjectId, ref: "ProductVariant", required: true },
seller: { type: Schema.Types.ObjectId, ref: "Seller", required: true },
type: { type: String, enum: AdType, required: true },
cost: { type: Number, required: true },
clickCount: { type: Number, default: 0 }, // for click-based ads
maxClicks: { type: Number, default: null }, // max clicks (for click-based)
startDate: { type: String }, // for duration-based ads
endDate: { type: String }, // for duration-based ads
isActive: { type: Boolean, default: true }, // Whether ad is currently active
},
{ timestamps: true, toJSON: { versionKey: false, virtuals: true }, id: false },
);
const ProductAdModel = model<IProductAd>("ProductAd", ProductAdSchema);
export { ProductAdModel };
@@ -0,0 +1,21 @@
import { Schema, model } from "mongoose";
import { IProductObserve } from "./Abstraction/IProductObserve";
const ProductObserveSchema = new Schema<IProductObserve>(
{
user: { type: Schema.Types.ObjectId, required: true },
product: { type: Number, required: true, ref: "Product" },
variant: { type: Schema.Types.ObjectId, required: true, ref: "ProductVariant" },
registeredPrice: { type: Number, required: true },
},
{
timestamps: true,
toJSON: { versionKey: false, virtuals: true },
id: false,
},
);
const ProductObserveModel = model<IProductObserve>("ProductObserve", ProductObserveSchema);
export { ProductObserveModel };
@@ -0,0 +1,19 @@
import { Schema, model } from "mongoose";
import { IProductReport } from "./Abstraction/IProductReport";
import { StatusEnum } from "../../../common/enums/status.enum";
const ProductReportSchema = new Schema<IProductReport>(
{
user: { type: Schema.Types.ObjectId, ref: "User", required: true },
product: { type: Number, ref: "Product", required: true },
reason: { type: [Number], ref: "ReportQuestion", required: true },
description: { type: String, default: "" },
status: { type: String, enum: StatusEnum, default: StatusEnum.Pending },
},
{ timestamps: true, toJSON: { versionKey: false, virtuals: true }, id: false },
);
const ProductReportModel = model<IProductReport>("ProductReport", ProductReportSchema);
export { ProductReportModel };
@@ -0,0 +1,40 @@
import { Schema, model } from "mongoose";
import { IProductRequest } from "./Abstraction/IProductRequest";
import { productImageSchema } from "./product.model";
import { productDimensionsSchema } from "./productVariant.model";
import { ProductSource, ProductStatus } from "../../../common/enums/product.enum";
const ProductRequestSchema = new Schema<IProductRequest>(
{
seller: { type: Schema.Types.ObjectId, ref: "Seller", required: true },
shop: { type: Schema.Types.ObjectId, ref: "Shop", required: true },
brand: { type: Schema.Types.ObjectId, ref: "Brand", required: true },
category: { type: Schema.Types.ObjectId, ref: "Category", required: true },
source: { type: String, enum: ProductSource, default: ProductSource.LOCAL, required: true },
productName: { type: String, required: true },
// productType: { type: String, required: true },
description: { type: String, required: true },
dimensions: { type: productDimensionsSchema, required: true },
advantages: { type: [String], default: [] },
disAdvantages: { type: [String], default: [] },
//
requestPhotography: { type: Boolean, default: false },
requestPhotosCount: { type: Number, default: null },
expertReview: { type: Boolean, default: false },
unboxingVideo: { type: Boolean, default: false },
imagesUrl: { type: productImageSchema },
payment: { type: Schema.Types.ObjectId, ref: "PRPayment", default: null },
requestStatus: { type: String, enum: ProductStatus, default: ProductStatus.Pending },
adminComments: { type: String },
},
{
timestamps: true,
toJSON: { virtuals: true, versionKey: false },
id: false,
},
);
const ProductRequestModel = model<IProductRequest>("ProductRequest", ProductRequestSchema);
export { ProductRequestModel };
@@ -0,0 +1,118 @@
import { Schema, model } from "mongoose";
import { IDimensions, IPrice, IProductVariant, ISaleFormat, IStatistics } from "./Abstraction/IProductVariant";
import { ProductMarketStatus } from "../../../common/enums/product.enum";
const productStatisticsSchema = new Schema<IStatistics>(
{
satisfied: { type: { rate: Number, count: Number } },
dissatisfied: { type: { rate: Number, count: Number } },
},
{ _id: false },
);
const productSaleFormat = new Schema<ISaleFormat>(
{
// isWholeSale: Boolean,
wholeSale: { type: [{ from: Number, to: Number, price: Number }] },
},
{ _id: false },
);
export const productDimensionsSchema = new Schema<IDimensions>(
{
package_weight: Number,
package_height: Number,
package_length: Number,
package_width: Number,
},
{ _id: false },
);
const productPriceSchema = new Schema<IPrice>(
{
order_limit: { type: Number, required: true },
retailPrice: { type: Number },
selling_price: { type: Number },
//specialSale
is_specialSale: { type: Boolean, default: false },
discount_percent: { type: Number, default: 0 },
specialSale_order_limit: { type: Number, default: null },
specialSale_quantity: { type: Number, default: null },
specialSale_endDate: { type: String, default: null },
},
{ _id: false },
);
const productVariantSchema = new Schema<IProductVariant>(
{
product: { type: Number, ref: "Product", required: true },
statistics: { type: productStatisticsSchema },
warranty: { type: Number, ref: "Warranty", required: true },
shop: { type: Schema.Types.ObjectId, ref: "Shop", required: true },
market_status: { type: String, enum: ProductMarketStatus, required: true },
postingTime: { type: Number, required: true },
price: { type: productPriceSchema, required: true },
stock: { type: Number, required: true },
isFreeShip: { type: Boolean, default: false },
isWholeSale: { type: Boolean, default: false },
saleFormat: { type: productSaleFormat },
dimensions: { type: productDimensionsSchema, required: true },
sellerSpecialCode: { type: String },
color: { type: Number, ref: "Color", required: false },
size: { type: Number, ref: "Size", required: false },
meterage: { type: Number, ref: "Meterage", required: false },
},
{ timestamps: true, toObject: { virtuals: true, versionKey: false }, toJSON: { virtuals: true, versionKey: false }, id: false },
);
productVariantSchema.pre("save", function (next) {
if (this.price) {
if (this.price.retailPrice) {
this.price.retailPrice = Math.round(this.price.retailPrice / 10000) * 10000;
const sellingPrice =
this.price.discount_percent > 0 ? this.price.retailPrice * ((100 - this.price.discount_percent) / 100) : this.price.retailPrice;
this.price.selling_price = Math.round(sellingPrice / 10000) * 10000;
}
}
next();
});
productVariantSchema.pre(["findOneAndUpdate", "updateOne", "updateMany"], function (next) {
const update = this.getUpdate() as IProductVariant;
if (!update) return next();
// ensure price object exists in the update object
if (update.price && update.price.retailPrice !== undefined) {
const retailPrice = Math.round(update.price.retailPrice / 10000) * 10000;
const discountPercent = Math.round(update.price.discount_percent) || 0;
// calculate selling price based on retailPrice and discount_percent
const sellingPrice = discountPercent > 0 ? retailPrice * ((100 - discountPercent) / 100) : retailPrice;
// set the calculated selling price in the update object
update.price.retailPrice = retailPrice;
update.price.selling_price = Math.round(sellingPrice / 10000) * 10000;
update.price.discount_percent = discountPercent;
}
next();
});
const ProductVariantModel = model<IProductVariant>("ProductVariant", productVariantSchema);
export { ProductVariantModel };
// const productSpecialSale = new Schema<ISpecialSale>(
// {
// endDate: String,
// type: { type: String, enum: ProductDiscountType, required: true },
// discountValue: Number,
// order_limit: Number,
// quantity: Number,
// priceAfterDiscount: Number,
// },
// { _id: false },
// );
@@ -0,0 +1,44 @@
import { Schema, model } from "mongoose";
import { IQuestion } from "./Abstraction/IQuestion";
import { Question_CommentStatus } from "../../../common/enums/question_comment.enum";
import { OwnerRef } from "../../shop/models/Abstraction/IShop";
const QuestionSchema = new Schema<IQuestion>(
{
content: { type: String, required: true },
status: { type: String, enum: Question_CommentStatus, default: Question_CommentStatus.Pending },
user: { type: Schema.Types.ObjectId, ref: "User", required: true },
product: { type: Number, ref: "Product", required: true },
answers: [
{
content: { type: String, required: true },
owner: { type: Schema.Types.ObjectId, required: true, refPath: "ownerRef" },
ownerRef: { type: String, required: true, enum: OwnerRef },
},
],
},
{
timestamps: true,
toJSON: { virtuals: true, versionKey: false },
id: false,
},
);
QuestionSchema.pre(["find", "findOne"], function (next) {
this.populate({ path: "user" });
next();
});
QuestionSchema.pre(["find", "findOne"], function (next) {
this.populate({
path: "product",
populate: ["brand", "category", "shop"],
select: { adminComments: 0, variants: 0 },
});
next();
});
const QuestionModel = model<IQuestion>("Question", QuestionSchema);
export { QuestionModel };
@@ -0,0 +1,22 @@
import mongoose, { Schema, model } from "mongoose";
import { IReportQuestion, ReportQuestionType } from "./Abstraction/IReportQuestion";
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const AutoIncrement = require("mongoose-sequence")(mongoose);
const ReportQuestionSchema = new Schema<IReportQuestion>(
{
_id: Number,
title: { type: String, required: true },
type: { type: String, enum: ReportQuestionType, required: true },
placeholder: { type: String, default: null },
// related_questions: [Number],
},
{ timestamps: true, toJSON: { versionKey: false }, _id: false, id: false },
);
ReportQuestionSchema.plugin(AutoIncrement, { id: "reportQues_id", inc_field: "_id" });
const ReportQuestionModel = model<IReportQuestion>("ReportQuestion", ReportQuestionSchema);
export { ReportQuestionModel };
+683
View File
@@ -0,0 +1,683 @@
import { Request } from "express";
import { inject } from "inversify";
import {
controller,
httpDelete,
httpGet,
httpPatch,
httpPost,
queryParam,
request,
requestBody,
requestParam,
} from "inversify-express-utils";
import { ProductFinalStepDTO, ProductStepOneDTO, ProductStepTwoDTO } from "./DTO/CreateProduct.dto";
import { ProductService } from "./providers/product.service";
import { HttpStatus } from "../../common";
import { ProductAdDTO } from "./DTO/addProductAd.dto";
import { AddVariantDTO } from "./DTO/addVariant.dto";
import { AddWishlistDTO, RemoveWishlistDTO } from "./DTO/addwishlist.dto";
import { CreateAnswerQuestionDTO, CreateCommentDTO, CreateQuestionDTO } from "./DTO/createQuestion.dto";
import { CreateReportDTO } from "./DTO/createReport.dto";
import { AddObserverDTO, RemoveObserverDTO } from "./DTO/observer.dto";
import { ProductParamDto, SetFreeShipParamDto } from "./DTO/productParam.dto";
import { RequestProductDTO } from "./DTO/RequestProduct.dto";
import { UpdateProductFinalStepDTO, UpdateProductStepOneDTO, UpdateProductStepTwoDTO } from "./DTO/updateProduct.dto";
import { ActivateVariantDTO, UpdateVariantDTO } from "./DTO/updateVariant.dto";
import { ProductRequestService } from "./providers/product-request.service";
import { BaseController } from "../../common/base/controller";
import { ApiAuth, ApiFile, ApiModel, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "../../common/decorator/swggerDocs";
import { PaginationDTO } from "../../common/dto/pagination.dto";
import { Guard } from "../../core/middlewares/guard.middleware";
import { ValidationMiddleware } from "../../core/middlewares/validator.middleware";
import { IOCTYPES } from "../../IOC/ioc.types";
import { UploadService } from "../../utils/upload.service";
import { ISeller } from "../seller/models/Abstraction/ISeller";
import { OwnerRef } from "../shop/models/Abstraction/IShop";
import { IUser } from "../user/models/Abstraction/IUser";
@controller("/product")
@ApiTags("Product")
class ProductController extends BaseController {
@inject(IOCTYPES.ProductService) productService: ProductService;
@inject(IOCTYPES.ProductRequestService) productRequestService: ProductRequestService;
//###############################################################
//###############################################################
//create product
@ApiOperation("first step to create a single product ==> need to login as seller")
@ApiResponse("successfully", HttpStatus.Created)
@ApiModel(ProductStepOneDTO)
@ApiAuth()
@httpPost("/creation/detail", Guard.authSeller(), ValidationMiddleware.validateInput(ProductStepOneDTO))
public async createProduct(@requestBody() createProductStepOneDto: ProductStepOneDTO, @request() req: Request) {
const seller = req.user as ISeller;
const data = await this.productService.createProductS(createProductStepOneDto, seller._id.toString());
return this.response({ data }, HttpStatus.Created);
}
@ApiOperation("second step to create a single product ==> need to login as seller")
@ApiResponse("successfully", HttpStatus.Created)
@ApiModel(ProductStepTwoDTO)
@ApiAuth()
@httpPost("/creation/attribute", Guard.authSeller(), ValidationMiddleware.validateInput(ProductStepTwoDTO))
public async createProductStepTwo(@requestBody() createProductStepTwoDto: ProductStepTwoDTO, @request() req: Request) {
const seller = req.user as ISeller;
const data = await this.productService.createProductStepTwoS(createProductStepTwoDto, seller._id.toString());
return this.response({ data }, HttpStatus.Created);
}
@ApiOperation("final step to create a single product ==> need to login as seller")
@ApiResponse("successfully", HttpStatus.Created)
@ApiModel(ProductFinalStepDTO)
@ApiAuth()
@httpPost("/creation/save", Guard.authSeller(), ValidationMiddleware.validateInput(ProductFinalStepDTO))
public async createProductFinalStep(@requestBody() createProductFinalStepDto: ProductFinalStepDTO, @request() req: Request) {
const seller = req.user as ISeller;
const data = await this.productService.createProductFinalStepS(createProductFinalStepDto, seller._id.toString());
return this.response({ data }, HttpStatus.Created);
}
//delete product of seller if not approved
@ApiOperation("mark a product as draft")
@ApiResponse("successful", HttpStatus.Ok)
@ApiParam("id", "id of product")
@ApiAuth()
@httpPost("/:id/draft", Guard.authSeller())
public async draftProduct(@requestParam("id") id: string) {
const data = await this.productService.draft(+id);
return this.response(data);
}
@ApiOperation("delete a product with its id ==> login as seller")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiAuth()
@ApiParam("id", "id of product", true)
@httpDelete("/:id/delete", Guard.authSeller(), ValidationMiddleware.validateParameter(ProductParamDto))
public async deleteProduct(@request() req: Request, @requestParam() paramDto: ProductParamDto) {
const seller = req.user as ISeller;
const data = await this.productService.deleteProduct(seller._id.toString(), +paramDto.id);
return this.response(data);
}
@ApiOperation("clone exist products ==> login as seller")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiParam("id", "id of a product", true)
@ApiAuth()
@httpPost("/:id/clone", Guard.authSeller(), ValidationMiddleware.validateParameter(ProductParamDto))
public async cloneProduct(@request() req: Request, @requestParam() paramDto: ProductParamDto) {
const seller = req.user as ISeller;
const data = await this.productService.cloneProduct(seller._id.toString(), paramDto.id);
return this.response(data);
}
//update product if it is in draft status
@ApiOperation("first step to update a single product ==> need to login as seller")
@ApiResponse("successfully", HttpStatus.Created)
@ApiModel(UpdateProductStepOneDTO)
@ApiAuth()
@httpPost("/update/detail", Guard.authSeller(), ValidationMiddleware.validateInput(UpdateProductStepOneDTO))
public async updateProduct(@requestBody() updateProductStepOneDto: UpdateProductStepOneDTO, @request() req: Request) {
const seller = req.user as ISeller;
const data = await this.productService.updateProductStepOneS(updateProductStepOneDto, seller._id.toString());
return this.response({ data }, HttpStatus.Created);
}
@ApiOperation("second step to update a single product ==> need to login as seller")
@ApiResponse("successfully", HttpStatus.Created)
@ApiModel(UpdateProductStepTwoDTO)
@ApiAuth()
@httpPost("/update/attribute", Guard.authSeller(), ValidationMiddleware.validateInput(UpdateProductStepTwoDTO))
public async updateProductStepTwo(@requestBody() updateProductStepTwoDto: UpdateProductStepTwoDTO, @request() req: Request) {
const seller = req.user as ISeller;
const data = await this.productService.updateProductStepTwoS(updateProductStepTwoDto, seller._id.toString());
return this.response({ data }, HttpStatus.Created);
}
@ApiOperation("final step to update a single product ==> need to login as seller")
@ApiResponse("successfully", HttpStatus.Created)
@ApiModel(UpdateProductFinalStepDTO)
@ApiAuth()
@httpPost("/update/save", Guard.authSeller(), ValidationMiddleware.validateInput(UpdateProductFinalStepDTO))
public async updateProductFinalStep(@requestBody() updateProductFinalStepDto: UpdateProductFinalStepDTO, @request() req: Request) {
const seller = req.user as ISeller;
const data = await this.productService.updateProductFinalStepS(updateProductFinalStepDto, seller._id.toString());
return this.response({ data }, HttpStatus.Created);
}
// request to create a product by admin
@ApiOperation("request to add product by admin ===> need to login as seller")
@ApiResponse("successfully", HttpStatus.Created)
@ApiModel(RequestProductDTO)
@ApiAuth()
@httpPost("/request/addBy-admin", Guard.authSeller(), ValidationMiddleware.validateInput(RequestProductDTO))
public async requestProduct(@requestBody() requestProductDto: RequestProductDTO, @request() req: Request) {
const seller = req.user as ISeller;
const data = await this.productRequestService.requestProductS(requestProductDto, seller._id.toString());
return this.response(data, HttpStatus.Created);
}
//###############################################################
//###############################################################
@ApiOperation("check if product is in user wishlist")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiParam("id", "productId", true)
@ApiParam("variantId", "variantId", true)
@ApiAuth()
@httpGet("/:id/wishlist/:variantId", Guard.authUser())
public async checkProductInWishlist(
@requestParam("id") productId: string,
@requestParam("variantId") variantId: string,
@request() req: Request,
) {
const user = req.user as IUser;
const data = await this.productService.checkProductInWishlist(user._id.toString(), +productId, variantId);
return this.response(data);
}
//product wishlist
@ApiOperation("add product to the user wishlist ===> need to login as user")
@ApiResponse("successfully", HttpStatus.Created)
@ApiParam("id", "productId", true)
@ApiModel(AddWishlistDTO)
@ApiAuth()
@httpPost("/:id/wishlist/add", Guard.authUser(), ValidationMiddleware.validateInput(AddWishlistDTO))
public async addToWishlist(@requestParam("id") productId: string, @request() req: Request, @requestBody() addDto: AddWishlistDTO) {
const user = req.user as IUser;
const data = await this.productService.addToWishlist(user._id.toString(), +productId, addDto);
return this.response(data);
}
@ApiOperation("remove product from the user wishlist ===> need to login as user")
@ApiResponse("successfully", HttpStatus.Created)
@ApiParam("id", "productId", true)
@ApiModel(RemoveWishlistDTO)
@ApiAuth()
@httpPost("/:id/wishlist/remove", Guard.authUser(), ValidationMiddleware.validateInput(RemoveWishlistDTO))
public async removeWishlist(@requestParam("id") productId: string, @request() req: Request, @requestBody() removeDto: RemoveWishlistDTO) {
const user = req.user as IUser;
const data = await this.productService.removeWishlist(user._id.toString(), +productId, removeDto);
return this.response(data);
}
//###############################################################
//###############################################################
//product observe
@ApiOperation("add user to the product observer ===> need to login as user")
@ApiResponse("successfully", HttpStatus.Created)
@ApiParam("id", "productId", true)
@ApiModel(AddObserverDTO)
@ApiAuth()
@httpPost("/:id/observe/add", Guard.authUser(), ValidationMiddleware.validateInput(AddObserverDTO))
public async addObserverToProduct(
@requestParam("id") productId: string,
@request() req: Request,
@requestBody() addObserverDto: AddObserverDTO,
) {
const user = req.user as IUser;
const data = await this.productService.addObserver(user._id.toString(), +productId, addObserverDto);
return this.response(data);
}
@ApiOperation("remove user from the product observer ===> need to login as user")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiParam("id", "productId", true)
@ApiModel(RemoveObserverDTO)
@ApiAuth()
@httpPost("/:id/observe/remove", Guard.authUser(), ValidationMiddleware.validateInput(RemoveObserverDTO))
public async removeObserverFromProduct(
@requestParam("id") productId: string,
@request() req: Request,
@requestBody() removeObserverDto: RemoveObserverDTO,
) {
const user = req.user as IUser;
const data = await this.productService.removeObserver(user._id.toString(), +productId, removeObserverDto);
return this.response(data);
}
//###############################################################
//###############################################################
//product search ad
@ApiOperation("add product search ad ===> need to login as seller")
@ApiResponse("successfully", HttpStatus.Created)
@ApiParam("id", "id of product", true)
@ApiModel(ProductAdDTO)
@ApiAuth()
@httpPost("/:id/search-ad", Guard.authSeller(), ValidationMiddleware.validateInput(ProductAdDTO))
public async addProductAd(@requestBody() createDto: ProductAdDTO, @request() req: Request, @requestParam("id") productId: string) {
const seller = req.user as ISeller;
const data = await this.productService.addProductAdS(seller._id.toString(), createDto, +productId);
return this.response(data, HttpStatus.Created);
}
@ApiOperation("get a product search ad ===> need to login as seller")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiParam("id", "id of product", true)
@ApiParam("variantId", "id of product variant", true)
@ApiAuth()
@httpGet("/:id/search-ad/:variantId", Guard.authSeller())
public async getProductAd(@request() req: Request, @requestParam("id") productId: string, @requestParam("variantId") variantId: string) {
const seller = req.user as ISeller;
const data = await this.productService.getProductAds(seller._id.toString(), +productId, variantId);
return this.response(data);
}
//###############################################################
//###############################################################
//product variant
@ApiOperation("add a variant to product ==> need to login as seller")
@ApiResponse("successfully", HttpStatus.Created)
@ApiModel(AddVariantDTO)
@ApiParam("id", "id of product", true)
@ApiAuth()
@httpPost("/:id/variants", Guard.authSeller(), ValidationMiddleware.validateInput(AddVariantDTO))
public async addVariant(@requestBody() addVariantDto: AddVariantDTO, @request() req: Request, @requestParam("id") productId: string) {
const seller = req.user as ISeller;
const data = await this.productService.addVariantS(addVariantDto, seller._id.toString(), +productId);
return this.response({ data }, HttpStatus.Created);
}
@ApiOperation("Activate/Deactivate a product variant status ==> need to login as seller")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiParam("id", "id of product", true)
@ApiModel(ActivateVariantDTO)
@ApiAuth()
@httpPost("/:id/variants/activate", Guard.authSeller(), ValidationMiddleware.validateInput(ActivateVariantDTO))
public async activateVariant(
@requestBody() activateDto: ActivateVariantDTO,
@request() req: Request,
@requestParam("id") productId: string,
) {
const seller = req.user as ISeller;
const data = await this.productService.activateVariantS(seller._id.toString(), activateDto, +productId);
return this.response(data);
}
@ApiOperation("set free shipping for a product variant ==> need to login as seller")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiParam("id", "id of product", true)
@ApiParam("variantId", "id of product variant", true)
@ApiAuth()
@httpPost("/:id/variants/:variantId/free-shipping", Guard.authSeller(), ValidationMiddleware.validateParameter(SetFreeShipParamDto))
public async setFreeShipping(@queryParam() paramDto: SetFreeShipParamDto, @request() req: Request) {
const seller = req.user as ISeller;
const data = await this.productService.setFreeShipping(seller._id.toString(), paramDto.id, paramDto.variantId);
return this.response(data);
}
@ApiOperation("update a variant of product ==> need to login as seller")
@ApiResponse("successfully", HttpStatus.Created)
@ApiModel(UpdateVariantDTO)
@ApiParam("id", "id of product", true)
@ApiAuth()
@httpPatch("/:id/variants", Guard.authSeller(), ValidationMiddleware.validateInput(UpdateVariantDTO))
public async updateVariant(
@requestBody() updateVariantDto: UpdateVariantDTO,
@request() req: Request,
@requestParam("id") productId: string,
) {
const seller = req.user as ISeller;
const data = await this.productService.updateVariantS(updateVariantDto, seller._id.toString(), +productId);
return this.response({ data }, HttpStatus.Created);
}
@ApiOperation("get all variants of a product ==> need to login as seller")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiParam("id", "id of product", true)
@ApiQuery("page", "the page want to get")
@ApiQuery("limit", "the limit of return data")
@ApiQuery("variant_status", "status of variant ==> value = 1 if want to include those with this status")
@ApiQuery("shipment_method", "shipment method id of product variant")
@ApiQuery("stock", "stock status of variant ==> value 1 if want to include those with this status")
@ApiQuery("include_ad", "include_ad status ==> value = 1 if want to include those with this status ")
@ApiQuery("special_sale", "special_sale status ==> value = 1 if want to include those with this status")
@ApiQuery("sort", "sort variant ==> value = createdAt", false, "array")
@ApiQuery("q", "q is the product variant id or special code of variant that seller specify")
@ApiAuth()
@httpGet("/:id/variants", Guard.authSeller())
public async getAllVariant(
@request() req: Request,
@requestParam("id") productId: string,
@queryParam("page") page: string,
@queryParam("limit") limit: string,
@queryParam("variant_status") variantStatus: string,
@queryParam("shipment_method") shipmentMethod: string,
@queryParam("stock") stock: string,
@queryParam("include_ad") includeAd: string,
@queryParam("special_sale") specialSale: string,
@queryParam("sort") sort: string[],
@queryParam("q") q: string,
) {
const queries = {
page: parseInt(page),
limit: parseInt(limit),
variantStatus: !!variantStatus,
shipmentMethod: +shipmentMethod,
stock: !!stock,
includeAd: !!includeAd,
specialSale: !!specialSale,
sort,
q,
};
const seller = req.user as ISeller;
const { count, ...data } = await this.productService.getAllVariantS(seller._id.toString(), +productId, queries);
const { pager } = this.paginate(count);
return this.response({ pager, productVariants: data.productVariants });
}
@ApiOperation("get all variants ==> need to login as seller")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiQuery("page", "the page want to get")
@ApiQuery("limit", "the limit of return data")
@ApiQuery("variant_status", "status of variant ==> value = 1 if want to include those with this status")
@ApiQuery("shipment_method", "shipment method id of product variant")
@ApiQuery("stock", "stock status of variant ==> value 1 if want to include those with this status")
@ApiQuery("include_ad", "include_ad status ==> value = 1 if want to include those with this status ")
@ApiQuery("special_sale", "special_sale status ==> value = 1 if want to include those with this status")
@ApiQuery("sort", "sort variant ==> value = createdAt", false, "array")
@ApiQuery("q", "q is the product variant id or special code of variant that seller specify")
@ApiAuth()
@httpGet("/variants", Guard.authSeller())
public async getAllVariants(
@request() req: Request,
@queryParam("page") page: string,
@queryParam("limit") limit: string,
@queryParam("variant_status") variantStatus: string,
@queryParam("shipment_method") shipmentMethod: string,
@queryParam("stock") stock: string,
@queryParam("include_ad") includeAd: string,
@queryParam("special_sale") specialSale: string,
@queryParam("sort") sort: string[],
@queryParam("q") q: string,
) {
const queries = {
page: parseInt(page),
limit: parseInt(limit),
variantStatus: !!variantStatus,
shipmentMethod: +shipmentMethod,
stock: !!stock,
includeAd: !!includeAd,
specialSale: !!specialSale,
sort,
q,
};
const seller = req.user as ISeller;
const { count, ...data } = await this.productService.getAllProductsVariantsS(seller._id.toString(), queries);
const { pager } = this.paginate(count);
return this.response({ pager, productVariants: data.productsVariants });
}
/** */
@ApiOperation("get a single product variant ==> need to login as seller")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiParam("id", "id of product", true)
@ApiParam("variantId", "id of product variant", true)
@ApiAuth()
@httpGet("/:id/variants/:variantId", Guard.authSeller())
public async getSingleVariant(
@request() req: Request,
@requestParam("id") productId: string,
@requestParam("variantId") variantId: string,
) {
const seller = req.user as ISeller;
const data = await this.productService.getSingleVariant(seller._id.toString(), +productId, variantId);
return this.response(data);
}
//###############################################################
//###############################################################
//compare product
@ApiOperation("search to get all product to compare based on the product category")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiQuery("productId", "id of a product", true)
@ApiQuery("limit", "the limit of return data")
@ApiQuery("page", "the page want to get")
@httpGet("/compare/search")
public async searchForCompareProducts(
@queryParam("productId") productId: string,
@queryParam("limit") limit: string,
@queryParam("page") page: string,
) {
const queries = {
limit: parseInt(limit),
page: parseInt(page),
productId: +productId,
};
const { count, ...data } = await this.productService.searchForCompareProductsS(queries);
const { pager } = this.paginate(count);
return this.response({ pager, products: data.products });
}
@ApiOperation("get product specification to compare")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiQuery("productIds", "Array of product IDs to compare", true, "array")
@httpGet("/compare")
public async compareProducts(@queryParam("productIds") productIds: string[]) {
const data = await this.productService.compareProducts(productIds);
return this.response(data);
}
//###############################################################
//###############################################################
// get product and delete product
@ApiOperation("search the product with query")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiQuery("q", "search query", true)
@httpGet("/search")
public async productSearch(@queryParam("q") q: string) {
const data = await this.productService.searchProducts(q);
return this.response(data);
}
@ApiOperation("get product details for product page")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiParam("productId", "id of a product", true)
@httpGet("/:productId")
public async getProductDetails(@requestParam("productId") productId: string) {
const data = await this.productService.getProductDetails(+productId);
return this.response(data);
}
@ApiOperation("get product price-history")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiParam("productId", "id of a product", true)
@ApiQuery("since", "search since date ==> 1403/01/15")
@httpGet("/:productId/price-history")
public async getProductPriceHistory(@requestParam("productId") productId: string, @queryParam("since") since: string) {
const data = await this.productService.getProductPriceHistoryS(+productId, since);
return this.response(data);
}
//###############################################################
//###############################################################
//product questions and comments
@ApiOperation("add a question to a product with its id ====> need to login as user")
@ApiResponse("successfully", HttpStatus.Created)
@ApiParam("id", "id of product", true)
@ApiModel(CreateQuestionDTO)
@ApiAuth()
@httpPost("/:id/questions", Guard.authUser(), ValidationMiddleware.validateInput(CreateQuestionDTO))
public async addQuestion(
@request() req: Request,
@requestParam("id") productId: string,
@requestBody() createQuestionDto: CreateQuestionDTO,
) {
const user = req.user as IUser;
const data = await this.productService.addQuestionS(user._id.toString(), +productId, createQuestionDto);
return this.response(data, HttpStatus.Created);
}
@ApiOperation("get all questions of a product with its id")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiParam("id", "id of product", true)
@httpGet("/:id/questions")
public async getQuestions(@requestParam("id") productId: string) {
const data = await this.productService.getQuestions(+productId);
return this.response(data);
}
@ApiOperation("delete a question with its id ====> need to login as admin")
@ApiResponse("successful", HttpStatus.Ok)
@ApiParam("id", "id of question")
@ApiAuth()
@httpDelete("/questions/:id/delete")
public async deleteQuestion(@requestParam("id") id: string) {
const data = await this.productService.deleteProductQuestion(id);
return this.response(data);
}
@ApiOperation("delete a comment with its id ====> need to login as admin")
@ApiResponse("successful", HttpStatus.Ok)
@ApiParam("id", "id of comment")
@ApiAuth()
@httpDelete("/comments/:id/delete")
public async deleteComment(@requestParam("id") id: string) {
const data = await this.productService.deleteProductComment(id);
return this.response(data);
}
@ApiOperation("answer to a question with its id ====> need to login as seller")
@ApiResponse("successful", HttpStatus.Ok)
@ApiParam("id", "id of question")
@ApiModel(CreateAnswerQuestionDTO)
@ApiAuth()
@httpPost("/questions/:id/answer", Guard.authSeller(), ValidationMiddleware.validateInput(CreateAnswerQuestionDTO))
public async answerQuestion(@requestParam("id") id: string, @requestBody() createDto: CreateAnswerQuestionDTO, @request() req: Request) {
const user = req.user as ISeller;
const data = await this.productService.answerQuestion(id, createDto, user._id.toString(), OwnerRef.SELLER);
return this.response(data);
}
@ApiOperation("add a comment to a product with its id ====> need to login as user")
@ApiResponse("successfully", HttpStatus.Created)
@ApiParam("id", "id of product", true)
@ApiModel(CreateCommentDTO)
@ApiAuth()
@httpPost("/:id/comments", Guard.authUser(), ValidationMiddleware.validateInput(CreateCommentDTO))
public async addComment(
@request() req: Request,
@requestParam("id") productId: string,
@requestBody() createCommentDto: CreateCommentDTO,
) {
const user = req.user as IUser;
const data = await this.productService.addCommentS(user._id.toString(), +productId, createCommentDto);
return this.response(data, HttpStatus.Created);
}
@ApiOperation("get all comments of a product with its id")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiParam("id", "id of product", true)
@httpGet("/:id/comments")
public async getComments(@requestParam("id") productId: string) {
const data = await this.productService.getComments(+productId);
return this.response(data);
}
//###############################################################
//###############################################################
//product report
@ApiOperation("get a report question of a product")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiParam("id", "id of product", true)
@httpGet("/:id/report/questions")
public async getReportQuestion() {
const data = await this.productService.getReportQuestions();
return this.response(data);
}
@ApiOperation("create a report on a product ====> need to login as user")
@ApiResponse("successfully", HttpStatus.Created)
@ApiParam("id", "id of product", true)
@ApiModel(CreateReportDTO)
@ApiAuth()
@httpPost("/:id/report/save", Guard.authUser(), ValidationMiddleware.validateInput(CreateReportDTO))
public async addReport(@request() req: Request, @requestBody() createReportDto: CreateReportDTO, @requestParam("id") productId: string) {
const user = req.user as IUser;
const data = await this.productService.addReportS(user._id.toString(), createReportDto, +productId);
return this.response(data, HttpStatus.Created);
}
//###############################################################
//###############################################################
//popular & incredible product
@ApiOperation("popular products")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiAuth()
@httpGet("/popular/all", Guard.authOptional())
public async getPopularProducts(@request() req: Request) {
const user = req.user as IUser;
const data = await this.productService.getPopularProducts(user?._id.toString());
return this.response(data, HttpStatus.Ok);
}
@ApiOperation("get all incredible offer that added for admin")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiQuery("page", "the page want to get")
@ApiQuery("limit", "the limit of return data")
@ApiAuth()
@httpGet("/incredible/all", Guard.authAdmin(), ValidationMiddleware.validateQuery(PaginationDTO))
public async getIncredibleProducts(@queryParam() paginationDto: PaginationDTO) {
const { count, incredibleOffers } = await this.productService.getIncredibleOffers(paginationDto);
const { pager } = this.paginate(count);
return this.response({ pager, incredibleOffers }, HttpStatus.Ok);
}
@ApiOperation("get all special sales product for admin")
@ApiResponse("successfully", HttpStatus.Ok)
@ApiQuery("page", "the page want to get")
@ApiQuery("limit", "the limit of return data")
@ApiAuth()
@httpGet("/special-sale/all", Guard.authAdmin(), ValidationMiddleware.validateQuery(PaginationDTO))
public async getAllProductWithSpecialSale(@queryParam() paginationDto: PaginationDTO) {
const { count, specialSaleProducts } = await this.productService.getSpecialProductsForAdmin(paginationDto);
const { pager } = this.paginate(count);
return this.response({ pager, specialSaleProducts }, HttpStatus.Ok);
}
//###############################################################
//###############################################################
//product uploader
@ApiOperation("Upload a product image ==> need to login as seller")
@ApiResponse("File uploaded successfully", HttpStatus.Accepted)
@ApiFile("image")
@ApiAuth()
@httpPost("/image/upload/single", Guard.authSeller(), UploadService.single("image", "product-image"))
public async uploadImage(@request() req: Request) {
// eslint-disable-next-line no-undef
const file = req.file as Express.MulterS3.File;
const data = {
message: "file uploaded!",
url: {
originalname: file.originalname,
size: file.size,
url: file.location,
type: file.mimetype,
},
};
return this.response({ data }, HttpStatus.Accepted);
}
@ApiOperation("Upload a multiple product images ==> need to login as seller")
@ApiResponse("File uploaded successfully", HttpStatus.Accepted)
@ApiFile("image", true, true)
@ApiAuth()
@httpPost("/image/upload/multiple", Guard.authSeller(), UploadService.multiple("image", 10, "product-image"))
public async uploadImages(@request() req: Request) {
// eslint-disable-next-line no-undef
const files = req.files as Express.MulterS3.File[];
const data = {
message: "files uploaded!",
urls: files.map((file) => ({
originalname: file.originalname,
size: file.size,
url: file.location,
type: file.mimetype,
})),
};
return this.response(data, HttpStatus.Accepted);
}
}
export { ProductController };
@@ -0,0 +1,117 @@
import { inject, injectable } from "inversify";
import { isValidObjectId, startSession } from "mongoose";
import { CommonMessage, ProductMessage, ProductRequestMessage, ShopMessage } from "../../../common/enums/message.enum";
import { BadRequestError } from "../../../core/app/app.errors";
import { IOCTYPES } from "../../../IOC/ioc.types";
import { OwnerRef } from "../../shop/models/Abstraction/IShop";
import { ShopRepo } from "../../shop/shop.repository";
import { RequestProductDTO } from "../DTO/RequestProduct.dto";
import { IDimensions } from "../models/Abstraction/IProductVariant";
import { ProductRequestRepo } from "../Repository/productRequest";
@injectable()
export class ProductRequestService {
@inject(IOCTYPES.ProductRequestRepo) productRequestRepo: ProductRequestRepo;
@inject(IOCTYPES.ShopRepo) private readonly shopRepo: ShopRepo;
//#############################
async requestProductS(requestProductDto: RequestProductDTO, sellerId: string) {
const session = await startSession();
session.startTransaction();
try {
if (requestProductDto.requestPhotography && (!requestProductDto.requestPhotosCount || requestProductDto.requestPhotosCount <= 0)) {
throw new BadRequestError(ProductRequestMessage.RequestPhotoInvalid);
}
if (!requestProductDto.requestPhotography && (!requestProductDto.coverImage || requestProductDto.coverImage.trim() === "")) {
throw new BadRequestError(ProductRequestMessage.PhotoShouldSent);
}
const shop = await this.shopRepo.model.findOne({ ownerRef: OwnerRef.SELLER, owner: sellerId });
if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound);
const dimensions: IDimensions = {
package_weight: requestProductDto.package_weight,
package_height: requestProductDto.package_height,
package_length: requestProductDto.package_length,
package_width: requestProductDto.package_width,
};
const imagesUrl = { cover: requestProductDto.coverImage, list: requestProductDto.imagesList };
const newProductRequest = await this.productRequestRepo.model.create(
[
{
seller: sellerId,
shop: shop._id,
imagesUrl,
dimensions,
...requestProductDto,
},
],
{ session },
);
await session.commitTransaction();
return {
message: ProductMessage.ProductRequested,
newProductRequest: newProductRequest[0],
};
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
}
//#############################
async getProductRequests(sellerId: string) {
const productRequests = await this.productRequestRepo.model
.find({ seller: sellerId })
.populate([
{ path: "brand", select: { _id: 1, title_fa: 1, title_en: 1 } },
{ path: "category", select: { _id: 1, title_fa: 1, title_en: 1 } },
{ path: "payment" },
]);
return {
productRequests,
};
}
//#############################
async getProductRequestForAdmin() {
const productRequests = await this.productRequestRepo.model
.find({})
.populate([
{ path: "seller", select: { _id: 1, fullName: 1 } },
{ path: "shop", select: { _id: 1, shopCode: 1, shopName: 1 } },
{ path: "brand", select: { _id: 1, title_fa: 1, title_en: 1 } },
{ path: "category", select: { children: 0, variant: 0 } },
{ path: "payment" },
]);
return {
productRequests,
};
}
//#############################
async getProductRequestDetails(sellerId: string, requestId: string) {
if (!isValidObjectId(requestId)) throw new BadRequestError(CommonMessage.NotValidId);
const productRequest = await this.productRequestRepo.model
.findOne({ _id: requestId, seller: sellerId })
.populate([
{ path: "brand", select: { _id: 1, title_fa: 1, title_en: 1 } },
{ path: "category", select: { _id: 1, title_fa: 1, title_en: 1 } },
{ path: "payment" },
]);
if (!productRequest) throw new BadRequestError(CommonMessage.NotValidId);
return {
productRequest,
};
}
}
File diff suppressed because it is too large Load Diff