diff --git a/src/IOC/ioc.config.ts b/src/IOC/ioc.config.ts index 956d5a7..0fb9b01 100644 --- a/src/IOC/ioc.config.ts +++ b/src/IOC/ioc.config.ts @@ -39,17 +39,16 @@ import { AttributeValueRepo, CategoryAttributeRepo, CategoryRepository, - ColorRepository, - MeterageRepository, - SizeRepository, + ThemeRepository, + ThemeValueRepository, createAttributeValueRepo, createCategoryAttributeRepo, createCategoryRepo, - createColorRepo, - createMeterageRepo, - createSizeRepo, + createThemeRepo, + createThemeValueRepo, } from "../modules/category/category.repository"; import { CategoryService } from "../modules/category/category.service"; +import { ThemeService } from "../modules/category/theme.service"; import { ChatGateway } from "../modules/chat/chat.gateway"; import { ChatService } from "../modules/chat/chat.service"; import { ChatRepo, createChatRepo } from "../modules/chat/repository/chat.repository"; @@ -158,6 +157,7 @@ const containerModules = new AsyncContainerModule(async (bind) => { bind(IOCTYPES.AsanPardakhtGateway).to(AsanPardakhtGateway).inSingletonScope(); // #region services bind(IOCTYPES.CategoryService).to(CategoryService).inSingletonScope(); + bind(IOCTYPES.ThemeService).to(ThemeService).inSingletonScope(); bind(IOCTYPES.AuthService).to(AuthService).inSingletonScope(); bind(IOCTYPES.ChatGateway).to(ChatGateway).inSingletonScope(); bind(IOCTYPES.ChatService).to(ChatService).inSingletonScope(); @@ -217,9 +217,8 @@ const containerModules = new AsyncContainerModule(async (bind) => { bind(IOCTYPES.ProductRepository).toDynamicValue(createProductRepo).inSingletonScope(); bind(IOCTYPES.ShipmentRepository).toDynamicValue(createShipmentRepository).inSingletonScope(); bind(IOCTYPES.ProductVariantRepository).toDynamicValue(createProductVariantRepo).inSingletonScope(); - bind(IOCTYPES.ColorRepository).toDynamicValue(createColorRepo).inSingletonScope(); - bind(IOCTYPES.SizeRepository).toDynamicValue(createSizeRepo).inSingletonScope(); - bind(IOCTYPES.MeterageRepository).toDynamicValue(createMeterageRepo).inSingletonScope(); + bind(IOCTYPES.ThemeRepository).toDynamicValue(createThemeRepo).inSingletonScope(); + bind(IOCTYPES.ThemeValueRepository).toDynamicValue(createThemeValueRepo).inSingletonScope(); bind(IOCTYPES.QuestionRepository).toDynamicValue(createQuestionRepo).inSingletonScope(); bind(IOCTYPES.CommentRepository).toDynamicValue(createCommentRepo).inSingletonScope(); bind(IOCTYPES.CartRepository).toDynamicValue(createCartRepo).inSingletonScope(); diff --git a/src/IOC/ioc.types.ts b/src/IOC/ioc.types.ts index 518b732..a328336 100644 --- a/src/IOC/ioc.types.ts +++ b/src/IOC/ioc.types.ts @@ -5,6 +5,7 @@ export const IOCTYPES = { PassportAuth: Symbol.for("PassportAuth"), AuthService: Symbol.for("AuthService"), CategoryService: Symbol.for("CategoryService"), + ThemeService: Symbol.for("ThemeService"), UserService: Symbol.for("UserService"), AdminService: Symbol.for("AdminService"), SellerService: Symbol.for("SellerService"), @@ -68,6 +69,8 @@ export const IOCTYPES = { ColorRepository: Symbol.for("ColorRepository"), SizeRepository: Symbol.for("SizeRepository"), MeterageRepository: Symbol.for("MeterageRepository"), + ThemeRepository: Symbol.for("ThemeRepository"), + ThemeValueRepository: Symbol.for("ThemeValueRepository"), QuestionRepository: Symbol.for("QuestionRepository"), CommentRepository: Symbol.for("CommentRepository"), ProductRequestRepo: Symbol.for("ProductRequestRepo"), diff --git a/src/common/enums/category.enum.ts b/src/common/enums/category.enum.ts index 7c84c0e..231488d 100644 --- a/src/common/enums/category.enum.ts +++ b/src/common/enums/category.enum.ts @@ -1,10 +1,3 @@ -export enum CategoryThemeEnum { - Sized = "Size", - Colored = "Color", - Meterage = "Meterage", - No_color_No_sized = "noColor_noSize", -} - export enum categoryAttType { Text = "text", Select = "select", diff --git a/src/db/seeders/catVariantSeeders.ts b/src/db/seeders/catVariantSeeders.ts index e984abb..c9d20d9 100644 --- a/src/db/seeders/catVariantSeeders.ts +++ b/src/db/seeders/catVariantSeeders.ts @@ -1,53 +1,57 @@ import { Logger } from "../../core/logging/logger"; -import { ColorModel } from "../../modules/category/models/color.model"; -import { MeterageModel } from "../../modules/category/models/meterage.model"; -import { SizeModel } from "../../modules/category/models/size.model"; +import { InputTypeEnum, ThemeModel } from "../../modules/category/models/theme.model"; +import { ThemeValueModel } from "../../modules/category/models/themeValue.model"; const colors = [ - { name: "قرمز", hexColor: "#FF0000" }, - { name: "سبز", hexColor: "#00FF00" }, - { name: "آبی", hexColor: "#0000FF" }, - { name: "مشکی", hexColor: "#000000" }, - { name: "سفید", hexColor: "#FFFFFF" }, - { name: "زرد", hexColor: "#FFFF00" }, - { name: "بنفش", hexColor: "#800080" }, - { name: "نارنجی", hexColor: "#FFA500" }, - { name: "صورتی", hexColor: "#FFC0CB" }, - { name: "خاکستری", hexColor: "#808080" }, + { name: "قرمز", value: "#FF0000" }, + { name: "سبز", value: "#00FF00" }, + { name: "آبی", value: "#0000FF" }, + { name: "مشکی", value: "#000000" }, + { name: "سفید", value: "#FFFFFF" }, + { name: "زرد", value: "#FFFF00" }, + { name: "بنفش", value: "#800080" }, + { name: "نارنجی", value: "#FFA500" }, + { name: "صورتی", value: "#FFC0CB" }, + { name: "خاکستری", value: "#808080" }, ]; const sizes = [ - { value: "XS" }, - { value: "S" }, - { value: "M" }, - { value: "L" }, - { value: "XL" }, - { value: "XXL" }, - { value: "3XL" }, - { value: "4XL" }, - { value: "5XL" }, - { value: "6XL" }, + { name: "XS", value: "XS" }, + { name: "S", value: "S" }, + { name: "M", value: "M" }, + { name: "L", value: "L" }, + { name: "XL", value: "XL" }, + { name: "XXL", value: "XXL" }, + { name: "3XL", value: "3XL" }, + { name: "4XL", value: "4XL" }, + { name: "5XL", value: "5XL" }, + { name: "6XL", value: "6XL" }, ]; const meterages = [ - { value: "1" }, - { value: "2" }, - { value: "3" }, - { value: "4" }, - { value: "5" }, - { value: "10" }, - { value: "15" }, - { value: "20" }, - { value: "25" }, - { value: "30" }, + { name: "1", value: "1" }, + { name: "2", value: "2" }, + { name: "3", value: "3" }, + { name: "4", value: "4" }, + { name: "5", value: "5" }, + { name: "10", value: "10" }, + { name: "15", value: "15" }, + { name: "20", value: "20" }, + { name: "25", value: "25" }, + { name: "30", value: "30" }, ]; const seedColors = async () => { try { console.log("Seeding colors..."); - await ColorModel.deleteMany({}); + let colorTheme = await ThemeModel.findOne({ title: "رنگ" }); + if (!colorTheme) { + colorTheme = await ThemeModel.create({ title: "رنگ", inputType: InputTypeEnum.Color }); + } + + await ThemeValueModel.deleteMany({ theme: colorTheme._id }); for (const color of colors) { - await ColorModel.create(color); + await ThemeValueModel.create({ theme: colorTheme._id, name: color.name, value: color.value }); } console.log("Colors seeded successfully"); } catch (error) { @@ -58,10 +62,14 @@ const seedColors = async () => { const seedSizes = async () => { try { console.log("Seeding sizes..."); - await SizeModel.deleteMany({}); + let sizeTheme = await ThemeModel.findOne({ title: "سایز" }); + if (!sizeTheme) { + sizeTheme = await ThemeModel.create({ title: "سایز", inputType: InputTypeEnum.Text }); + } + await ThemeValueModel.deleteMany({ theme: sizeTheme._id }); for (const size of sizes) { - await SizeModel.create(size); + await ThemeValueModel.create({ theme: sizeTheme._id, name: size.name, value: size.value }); } console.log("Sizes seeded successfully"); } catch (error) { @@ -72,9 +80,14 @@ const seedSizes = async () => { const seedMeterages = async () => { try { console.log("Seeding meterages..."); - await MeterageModel.deleteMany({}); + let meterageTheme = await ThemeModel.findOne({ title: "متراژ" }); + if (!meterageTheme) { + meterageTheme = await ThemeModel.create({ title: "متراژ", inputType: InputTypeEnum.Number }); + } + + await ThemeValueModel.deleteMany({ theme: meterageTheme._id }); for (const meterage of meterages) { - await MeterageModel.create(meterage); + await ThemeValueModel.create({ theme: meterageTheme._id, name: meterage.name, value: meterage.value }); } console.log("Meterages seeded successfully"); } catch (error) { diff --git a/src/db/seeders/categorySeeder.ts b/src/db/seeders/categorySeeder.ts index a7f4b33..68b85f6 100644 --- a/src/db/seeders/categorySeeder.ts +++ b/src/db/seeders/categorySeeder.ts @@ -2,14 +2,12 @@ import { faker } from "@faker-js/faker"; import { Types } from "mongoose"; faker.locale = "fa"; -import { CategoryThemeEnum } from "../../common/enums/category.enum"; import { Logger } from "../../core/logging/logger"; import { AttributeValueModel } from "../../modules/category/models/attributeValue.model"; import { CategoryModel } from "../../modules/category/models/category.model"; import { CategoryAttributeModel } from "../../modules/category/models/CategoryAttribute.model"; -import { ColorModel } from "../../modules/category/models/color.model"; -import { MeterageModel } from "../../modules/category/models/meterage.model"; -import { SizeModel } from "../../modules/category/models/size.model"; +import { ThemeModel } from "../../modules/category/models/theme.model"; +import { ThemeValueModel } from "../../modules/category/models/themeValue.model"; interface ICategorySeed { title_fa: string; @@ -18,7 +16,7 @@ interface ICategorySeed { imageUrl: string; description: string; leaf: boolean; - theme?: CategoryThemeEnum; + themeType?: "color" | "size" | "meterage" | null; parent?: Types.ObjectId; } @@ -123,7 +121,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { title_en: "mobile-phones", leaf: true, imageUrl: faker.image.imageUrl(), - theme: CategoryThemeEnum.Colored, + themeType: "color", }, { title_fa: "تبلت", @@ -131,7 +129,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع تبلت ها", title_en: "tablets", leaf: true, - theme: CategoryThemeEnum.Colored, + themeType: "color", imageUrl: faker.image.imageUrl(), }, { @@ -140,7 +138,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع ساعت هوشمند", title_en: "smart-watch", leaf: true, - theme: CategoryThemeEnum.Colored, + themeType: "color", imageUrl: faker.image.imageUrl(), }, { @@ -149,7 +147,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع پاور بانک", title_en: "power-banks", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, ], @@ -160,7 +158,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع خودکار و خودنویس", title_en: "pens", leaf: true, - theme: CategoryThemeEnum.Colored, + themeType: "color", imageUrl: faker.image.imageUrl(), }, { @@ -169,7 +167,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع دفتر و کاغذ", title_en: "paper-notebooks", leaf: true, - theme: CategoryThemeEnum.Colored, + themeType: "color", imageUrl: faker.image.imageUrl(), }, ], @@ -180,7 +178,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع لامپ ها", title_en: "bulbs", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, { @@ -189,7 +187,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع کابل و سیم", title_en: "cables-wires", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, ], @@ -200,7 +198,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع آچار و پیچ گوشتی", title_en: "wrenches-screwdrivers", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, { @@ -209,7 +207,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع دریل ها", title_en: "drills", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, ], @@ -220,7 +218,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع تلویزیون", title_en: "televisions", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, { @@ -229,7 +227,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع اسپیکر", title_en: "speakers", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, { @@ -238,7 +236,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع هدفون", title_en: "headphones", leaf: true, - theme: CategoryThemeEnum.Colored, + themeType: "color", imageUrl: faker.image.imageUrl(), }, { @@ -247,7 +245,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع دوربین عکاسی", title_en: "cameras", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, ], @@ -258,7 +256,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع لپ تاپ", title_en: "laptops", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, { @@ -267,7 +265,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع موس و کیبورد", title_en: "mouse-keyboards", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, ], @@ -278,7 +276,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع پلی استیشن", title_en: "playstations", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, { @@ -287,7 +285,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع ایکس باکس", title_en: "xbox", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, ], @@ -298,7 +296,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع تجهیزات ورزشی", title_en: "sports-equipment", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, { @@ -307,7 +305,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع لوازم سفر", title_en: "travel-equipment", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, ], @@ -318,7 +316,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع لباس زنانه", title_en: "women-clothing", leaf: true, - theme: CategoryThemeEnum.Sized, + themeType: "size", imageUrl: faker.image.imageUrl(), }, { @@ -327,7 +325,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع لباس مردانه", title_en: "men-clothing", leaf: true, - theme: CategoryThemeEnum.Sized, + themeType: "size", imageUrl: faker.image.imageUrl(), }, ], @@ -338,7 +336,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع لوازم آشپزخانه", title_en: "kitchen-appliances", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, { @@ -347,7 +345,7 @@ const subcategories: { [key: string]: ICategorySeed[] } = { description: "انواع لوازم نظافت", title_en: "cleaning-appliances", leaf: true, - theme: CategoryThemeEnum.No_color_No_sized, + themeType: null, imageUrl: faker.image.imageUrl(), }, ], @@ -461,9 +459,13 @@ export const seedCategories = async (logger: Logger) => { try { logger.info("Categories seeding started"); - const colors = (await ColorModel.find()).map((color) => color._id); - const sizes = (await SizeModel.find()).map((size) => size._id); - const meterages = (await MeterageModel.find()).map((meterage) => meterage._id); + const colorTheme = await ThemeModel.findOne({ title: "رنگ" }); + const sizeTheme = await ThemeModel.findOne({ title: "سایز" }); + const meterageTheme = await ThemeModel.findOne({ title: "متراژ" }); + + const colors = colorTheme ? (await ThemeValueModel.find({ theme: colorTheme._id })).map((color) => color._id) : []; + const sizes = sizeTheme ? (await ThemeValueModel.find({ theme: sizeTheme._id })).map((size) => size._id) : []; + const meterages = meterageTheme ? (await ThemeValueModel.find({ theme: meterageTheme._id })).map((meterage) => meterage._id) : []; const createdCategories: { [key: string]: Types.ObjectId } = {}; const createdSubCategories: { [key: string]: Types.ObjectId } = {}; @@ -483,16 +485,22 @@ export const seedCategories = async (logger: Logger) => { const parentId = createdCategories[rootName]; if (parentId) { for (const subcategory of subs) { - const { title_fa, icon, description, title_en, leaf, theme, imageUrl } = subcategory; + const { title_fa, icon, description, title_en, leaf, themeType, imageUrl } = subcategory; - const catVariants = - theme === CategoryThemeEnum.Colored - ? [...colors] - : theme === CategoryThemeEnum.Meterage - ? [...meterages] - : theme === CategoryThemeEnum.Sized - ? [...sizes] - : []; + // Map themeType to theme ID and variants + let themeId: Types.ObjectId | undefined; + let catVariants: Types.ObjectId[] = []; + + if (themeType === "color" && colorTheme) { + themeId = colorTheme._id; + catVariants = [...colors]; + } else if (themeType === "size" && sizeTheme) { + themeId = sizeTheme._id; + catVariants = [...sizes]; + } else if (themeType === "meterage" && meterageTheme) { + themeId = meterageTheme._id; + catVariants = [...meterages]; + } const subCategoryDoc = new CategoryModel({ title_fa, @@ -501,7 +509,7 @@ export const seedCategories = async (logger: Logger) => { title_en, description, leaf, - theme, + theme: themeId, variants: catVariants, parent: parentId, }); diff --git a/src/db/seeders/productSeeders.ts b/src/db/seeders/productSeeders.ts index ff34864..8ff4974 100644 --- a/src/db/seeders/productSeeders.ts +++ b/src/db/seeders/productSeeders.ts @@ -7,9 +7,8 @@ import { BrandModel } from "../../modules/brand/models/brand.model"; import { AttributeValueModel } from "../../modules/category/models/attributeValue.model"; import { CategoryModel } from "../../modules/category/models/category.model"; import { CategoryAttributeModel } from "../../modules/category/models/CategoryAttribute.model"; -import { ColorModel } from "../../modules/category/models/color.model"; -import { MeterageModel } from "../../modules/category/models/meterage.model"; -import { SizeModel } from "../../modules/category/models/size.model"; +import { ThemeModel } from "../../modules/category/models/theme.model"; +import { ThemeValueModel } from "../../modules/category/models/themeValue.model"; import { ISpecifications } from "../../modules/product/models/Abstraction/IProduct"; import { ProductModel } from "../../modules/product/models/product.model"; import { ProductVariantModel } from "../../modules/product/models/productVariant.model"; @@ -205,22 +204,13 @@ const getBrandAndCategoryForProduct = async (product: (typeof fakeProductsData)[ return { brand, category }; }; -const createFakeProductVariant = ( - productId: number, - shopId: string, - warrantyId: number, - colorId?: number, - sizeId?: number, - meterageId?: number, -) => { +const createFakeProductVariant = (productId: number, shopId: string, warrantyId: number, themeValueId?: string) => { return new ProductVariantModel({ product: productId, rate: faker.datatype.float({ min: 1, max: 5 }), warranty: warrantyId, shop: shopId, - color: colorId, - size: sizeId, - meterage: meterageId, + themeValue: themeValueId, sellerSpecialCode: crypto.randomUUID().slice(0, 6).toUpperCase(), market_status: faker.helpers.arrayElement([ProductMarketStatus.Marketable]), postingTime: faker.datatype.number({ min: 1, max: 10 }), @@ -258,9 +248,14 @@ export const seedProduct = async (logger: Logger) => { // Fetch warranties and shipment methods const warranties = await WarrantyModel.find().limit(10); const shipments = (await ShipmentModel.find()).map((shipper) => shipper._id); - const colors = (await ColorModel.find()).map((color) => color._id); - const sizes = (await SizeModel.find()).map((size) => size._id); - const meterages = (await MeterageModel.find()).map((meterage) => meterage._id); + + const colorTheme = await ThemeModel.findOne({ title: "رنگ" }); + const sizeTheme = await ThemeModel.findOne({ title: "سایز" }); + const meterageTheme = await ThemeModel.findOne({ title: "متراژ" }); + + const colors = colorTheme ? (await ThemeValueModel.find({ theme: colorTheme._id })).map((color) => color._id) : []; + const sizes = sizeTheme ? (await ThemeValueModel.find({ theme: sizeTheme._id })).map((size) => size._id) : []; + const meterages = meterageTheme ? (await ThemeValueModel.find({ theme: meterageTheme._id })).map((meterage) => meterage._id) : []; if (!warranties.length) { throw new Error("No warranties found in the database. Please seed them first."); @@ -302,13 +297,21 @@ export const seedProduct = async (logger: Logger) => { }).save(); for (let k = 0; k < 3; k++) { + // Select a theme value based on product data + let themeValueId: string | undefined; + if (productData.color && colors.length > 0) { + themeValueId = faker.helpers.arrayElement(colors).toString(); + } else if (productData.size && sizes.length > 0) { + themeValueId = faker.helpers.arrayElement(sizes).toString(); + } else if (productData.meterage && meterages.length > 0) { + themeValueId = faker.helpers.arrayElement(meterages).toString(); + } + const variant = await createFakeProductVariant( product._id, shop._id.toString(), faker.helpers.arrayElement(warranties)._id, - productData.color ? faker.helpers.arrayElement(colors) : undefined, - productData.size ? faker.helpers.arrayElement(sizes) : undefined, - productData.meterage ? faker.helpers.arrayElement(meterages) : undefined, + themeValueId, ).save(); product.variants.push(variant._id); await product.save(); diff --git a/src/events/priceChange.ts b/src/events/priceChange.ts index b2c8ae7..3a71915 100644 --- a/src/events/priceChange.ts +++ b/src/events/priceChange.ts @@ -2,9 +2,7 @@ import { EventEmitter } from "events"; import { HydratedDocument, Types } from "mongoose"; -import { ColorModel } from "../modules/category/models/color.model"; -import { MeterageModel } from "../modules/category/models/meterage.model"; -import { SizeModel } from "../modules/category/models/size.model"; +import { ThemeValueModel } from "../modules/category/models/themeValue.model"; import { IPriceHistory } from "../modules/product/models/Abstraction/IPriceHistory"; import { PriceHistoryModel } from "../modules/product/models/priceHistory.model"; import { ProductObserveModel } from "../modules/product/models/productObserve.model"; @@ -24,9 +22,7 @@ type PriceDetails = { retail_price: number; variantId: string; isSpecial?: boolean; - colorId?: number; - sizeId?: number; - meterageId?: number; + themeValueId?: string; }; type historyDetail = { shop: string; selling_price: number; retail_price: number }; @@ -66,7 +62,7 @@ class PriceChangeEventService extends EventEmitter { try { const title = await this.findVariantTitle(priceDetails); const now = TimeService.getCurrentPersianDate(); - const { colorId, sizeId, meterageId, product, ...history } = priceDetails; + const { themeValueId, product, ...history } = priceDetails; const existingHistory = await PriceHistoryModel.findOne({ title, product }); @@ -137,15 +133,9 @@ class PriceChangeEventService extends EventEmitter { } //**** */ private async findVariantTitle(priceDetails: PriceDetails): Promise { - if (priceDetails.colorId) { - const color = await ColorModel.findById(priceDetails.colorId).lean(); - return color?.name || "Unknown color"; - } else if (priceDetails.sizeId) { - const size = await SizeModel.findById(priceDetails.sizeId).lean(); - return size?.value || "Unknown size"; - } else if (priceDetails.meterageId) { - const meterage = await MeterageModel.findById(priceDetails.meterageId).lean(); - return meterage?.value || "Unknown meterage"; + if (priceDetails.themeValueId) { + const themeValue = await ThemeValueModel.findById(priceDetails.themeValueId).lean(); + return themeValue?.name || "Unknown variant"; } return ""; } diff --git a/src/modules/admin/controllers/category.controller.ts b/src/modules/admin/controllers/category.controller.ts index 539f716..e66a704 100644 --- a/src/modules/admin/controllers/category.controller.ts +++ b/src/modules/admin/controllers/category.controller.ts @@ -1,5 +1,5 @@ import { inject } from "inversify"; -import { controller, httpDelete, httpPatch, httpPost, requestBody, requestParam } from "inversify-express-utils"; +import { controller, httpDelete, httpGet, httpPatch, httpPost, httpPut, requestBody, requestParam } from "inversify-express-utils"; import { HttpStatus } from "../../../common"; import { BaseController } from "../../../common/base/controller"; @@ -12,14 +12,20 @@ import { CategoryService } from "../../category/category.service"; import { CreateCategoryDTO } from "../../category/DTO/CreateCategory.dto"; import { CreateCategoryAttDTO } from "../../category/DTO/createCategoryAtt.dto"; import { CreateCategoryThemeDTO, UpdateCategoryVariantDTO } from "../../category/DTO/createCategoryTheme.dto"; +import { CreateThemeDTO } from "../../category/DTO/CreateTheme.dto"; +import { CreateThemeValueDTO } from "../../category/DTO/CreateThemeValue.dto"; import { UpdateCategoryDTO } from "../../category/DTO/UpdateCategory.dto"; import { UpdateCategoryAttDTO } from "../../category/DTO/updateCategoryAtt.dto"; +import { UpdateThemeDTO } from "../../category/DTO/UpdateTheme.dto"; +import { UpdateThemeValueDTO } from "../../category/DTO/UpdateThemeValue.dto"; +import { ThemeService } from "../../category/theme.service"; import { PermissionEnum } from "../models/Abstraction/IPermission"; @ApiTags("Admin Category") @controller("/admin/category") export class AdminCategoryController extends BaseController { @inject(IOCTYPES.CategoryService) private categoryService: CategoryService; + @inject(IOCTYPES.ThemeService) private themeService: ThemeService; @ApiOperation("create a category ") @ApiResponse("return created category", HttpStatus.Created) @@ -134,4 +140,127 @@ export class AdminCategoryController extends BaseController { const data = await this.categoryService.updateCategoryAttributeS(createAttributeDto); return this.response({ data }, HttpStatus.Created); } + + //#########################################theme endpoints + @ApiOperation("Get all themes") + @ApiResponse("get all themes", HttpStatus.Ok) + @httpGet("/theme") + public async getAllThemes() { + const data = await this.themeService.getAllThemes(); + return this.response({ data }); + } + + @ApiOperation("Get a theme with id") + @ApiResponse("get a theme", HttpStatus.Ok) + @ApiParam("id", "id of the theme", true) + @httpGet("/theme/:id") + public async getThemeById(@requestParam("id") id: string) { + const data = await this.themeService.getThemeById(id); + return this.response({ data }); + } + + @ApiOperation("Create a new theme ==> need to login as admin") + @ApiResponse("theme created successfully", HttpStatus.Created) + @ApiModel(CreateThemeDTO) + @ApiAuth() + @httpPost( + "/theme", + Guard.authAdmin(), + Guard.checkAdminPermissions([PermissionEnum.ADMIN]), + ValidationMiddleware.validateInput(CreateThemeDTO), + ) + public async createTheme(@requestBody() createDto: CreateThemeDTO) { + const data = await this.themeService.createTheme(createDto); + return this.response({ data }, HttpStatus.Created); + } + + @ApiOperation("Update a theme ==> need to login as admin") + @ApiResponse("theme updated successfully", HttpStatus.Ok) + @ApiAuth() + @httpPut( + "/theme", + Guard.authAdmin(), + Guard.checkAdminPermissions([PermissionEnum.ADMIN]), + ValidationMiddleware.validateInput(UpdateThemeDTO), + ) + public async updateTheme(@requestBody() updateDto: UpdateThemeDTO) { + const data = await this.themeService.updateTheme(updateDto); + return this.response({ data }); + } + + @ApiOperation("Delete a theme ==> need to login as admin") + @ApiResponse("theme deleted successfully", HttpStatus.Ok) + @ApiParam("id", "id of the theme", true) + @ApiAuth() + @httpDelete("/theme/:id", Guard.authAdmin(), Guard.checkAdminPermissions([PermissionEnum.ADMIN])) + public async deleteTheme(@requestParam("id") id: string) { + const data = await this.themeService.deleteTheme(id); + return this.response({ data }); + } + + //#########################################theme value endpoints + @ApiOperation("Get all theme values") + @ApiResponse("get all theme values", HttpStatus.Ok) + @httpGet("/theme-value") + public async getAllThemeValues() { + const data = await this.themeService.getAllThemeValues(); + return this.response({ data }); + } + + @ApiOperation("Get theme values by theme id") + @ApiResponse("get theme values by theme id", HttpStatus.Ok) + @ApiParam("themeId", "id of the theme", true) + @httpGet("/theme-value/:themeId/values") + public async getThemeValuesByThemeId(@requestParam("themeId") themeId: string) { + const data = await this.themeService.getThemeValuesByThemeId(themeId); + return this.response({ data }); + } + + @ApiOperation("Get a theme value with id") + @ApiResponse("get a theme value", HttpStatus.Ok) + @ApiParam("id", "id of the theme value", true) + @httpGet("/theme-value/:id") + public async getThemeValueById(@requestParam("id") id: string) { + const data = await this.themeService.getThemeValueById(id); + return this.response({ data }); + } + + @ApiOperation("Create a new theme value ==> need to login as admin") + @ApiResponse("theme value created successfully", HttpStatus.Created) + @ApiAuth() + @ApiModel(CreateThemeValueDTO) + @httpPost( + "/theme-value", + Guard.authAdmin(), + Guard.checkAdminPermissions([PermissionEnum.ADMIN]), + ValidationMiddleware.validateInput(CreateThemeValueDTO), + ) + public async createThemeValue(@requestBody() createDto: CreateThemeValueDTO) { + const data = await this.themeService.createThemeValue(createDto); + return this.response({ data }, HttpStatus.Created); + } + + @ApiOperation("Update a theme value ==> need to login as admin") + @ApiResponse("theme value updated successfully", HttpStatus.Ok) + @ApiAuth() + @httpPut( + "/theme-value", + Guard.authAdmin(), + Guard.checkAdminPermissions([PermissionEnum.ADMIN]), + ValidationMiddleware.validateInput(UpdateThemeValueDTO), + ) + public async updateThemeValue(@requestBody() updateDto: UpdateThemeValueDTO) { + const data = await this.themeService.updateThemeValue(updateDto); + return this.response({ data }); + } + + @ApiOperation("Delete a theme value ==> need to login as admin") + @ApiResponse("theme value deleted successfully", HttpStatus.Ok) + @ApiParam("id", "id of the theme value", true) + @ApiAuth() + @httpDelete("/theme-value/:id", Guard.authAdmin(), Guard.checkAdminPermissions([PermissionEnum.ADMIN])) + public async deleteThemeValue(@requestParam("id") id: string) { + const data = await this.themeService.deleteThemeValue(id); + return this.response({ data }); + } } diff --git a/src/modules/category/DTO/CreateCategory.dto.ts b/src/modules/category/DTO/CreateCategory.dto.ts index 2a97f86..8d5b41d 100644 --- a/src/modules/category/DTO/CreateCategory.dto.ts +++ b/src/modules/category/DTO/CreateCategory.dto.ts @@ -1,10 +1,10 @@ import { Expose } from "class-transformer"; -import { IsEnum, IsNotEmpty, IsOptional } from "class-validator"; +import { IsNotEmpty, IsOptional } from "class-validator"; import { ApiProperty } from "../../../common/decorator/swggerDocs"; import { IsValidId } from "../../../common/decorator/validation.decorator"; -import { CategoryThemeEnum } from "../../../common/enums/category.enum"; import { CategoryModel } from "../models/category.model"; +import { ThemeModel } from "../models/theme.model"; export class CreateCategoryDTO { @Expose() @@ -40,12 +40,11 @@ export class CreateCategoryDTO { parent?: string; @Expose() - @IsNotEmpty() - @IsEnum(CategoryThemeEnum) + @IsOptional() + @IsValidId(ThemeModel) @ApiProperty({ type: "string", description: "the variation theme of a category ==> should be one of Color/Size/Meterage or noColor_noSize", - example: "Color", }) - theme: CategoryThemeEnum; + themeId: string; } diff --git a/src/modules/category/DTO/CreateTheme.dto.ts b/src/modules/category/DTO/CreateTheme.dto.ts new file mode 100644 index 0000000..68fc30d --- /dev/null +++ b/src/modules/category/DTO/CreateTheme.dto.ts @@ -0,0 +1,22 @@ +import { Expose } from "class-transformer"; +import { IsEnum, IsNotEmpty } from "class-validator"; + +import { ApiProperty } from "../../../common/decorator/swggerDocs"; +import { InputTypeEnum } from "../models/theme.model"; + +export class CreateThemeDTO { + @Expose() + @IsNotEmpty() + @ApiProperty({ type: "string", description: "the title of theme", example: "Color" }) + title: string; + + @Expose() + @IsNotEmpty() + @IsEnum(InputTypeEnum) + @ApiProperty({ + type: "string", + description: "the input type of theme", + example: InputTypeEnum.Color, + }) + inputType: InputTypeEnum; +} diff --git a/src/modules/category/DTO/CreateThemeValue.dto.ts b/src/modules/category/DTO/CreateThemeValue.dto.ts new file mode 100644 index 0000000..a6cfb2e --- /dev/null +++ b/src/modules/category/DTO/CreateThemeValue.dto.ts @@ -0,0 +1,26 @@ +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 { ThemeModel } from "../models/theme.model"; + +export class CreateThemeValueDTO { + @Expose() + @IsNotEmpty() + @IsString() + @IsValidId(ThemeModel) + @ApiProperty({ type: "string", description: "the theme id", example: "66f3bcaee566db722a044c62" }) + theme: string; + + @Expose() + @IsNotEmpty() + @IsString() + @ApiProperty({ type: "string", description: "the name of theme value", example: "Red" }) + name: string; + + @Expose() + @IsNotEmpty() + @ApiProperty({ type: "string", description: "the value of theme value (can be string, number, or color hex)", example: "#FF0000" }) + value: string | number; +} diff --git a/src/modules/category/DTO/UpdateCategory.dto.ts b/src/modules/category/DTO/UpdateCategory.dto.ts index 600109d..6325609 100644 --- a/src/modules/category/DTO/UpdateCategory.dto.ts +++ b/src/modules/category/DTO/UpdateCategory.dto.ts @@ -1,10 +1,10 @@ import { Expose } from "class-transformer"; -import { IsEnum, IsNotEmpty, IsOptional, IsString, Length } from "class-validator"; +import { IsNotEmpty, IsOptional, IsString, Length } from "class-validator"; import { ApiProperty } from "../../../common/decorator/swggerDocs"; import { IsValidId } from "../../../common/decorator/validation.decorator"; -import { CategoryThemeEnum } from "../../../common/enums/category.enum"; import { CategoryModel } from "../models/category.model"; +import { ThemeModel } from "../models/theme.model"; export class UpdateCategoryDTO { @Expose() @@ -54,12 +54,11 @@ export class UpdateCategoryDTO { @Expose() @IsOptional() - @IsNotEmpty() - @IsEnum(CategoryThemeEnum) + @IsValidId(ThemeModel) @ApiProperty({ type: "string", description: "the variation theme of a category ==> should be one of Color/Size/Meterage or noColor_noSize", example: "Color", }) - theme: CategoryThemeEnum; + themeId: string; } diff --git a/src/modules/category/DTO/UpdateTheme.dto.ts b/src/modules/category/DTO/UpdateTheme.dto.ts new file mode 100644 index 0000000..533d4f3 --- /dev/null +++ b/src/modules/category/DTO/UpdateTheme.dto.ts @@ -0,0 +1,33 @@ +import { Expose } from "class-transformer"; +import { IsEnum, IsNotEmpty, IsOptional, IsString, Length } from "class-validator"; + +import { ApiProperty } from "../../../common/decorator/swggerDocs"; +import { IsValidId } from "../../../common/decorator/validation.decorator"; +import { InputTypeEnum, ThemeModel } from "../models/theme.model"; + +export class UpdateThemeDTO { + @Expose() + @IsNotEmpty() + @IsString() + @Length(24, 24) + @IsValidId(ThemeModel) + @ApiProperty({ type: "string", description: "theme id", example: "66f3bcaee566db722a044c62" }) + themeId: string; + + @Expose() + @IsOptional() + @IsNotEmpty() + @ApiProperty({ type: "string", description: "the title of theme", example: "Color" }) + title?: string; + + @Expose() + @IsOptional() + @IsNotEmpty() + @IsEnum(InputTypeEnum) + @ApiProperty({ + type: "string", + description: "the input type of theme", + example: InputTypeEnum.Color, + }) + inputType?: InputTypeEnum; +} diff --git a/src/modules/category/DTO/UpdateThemeValue.dto.ts b/src/modules/category/DTO/UpdateThemeValue.dto.ts new file mode 100644 index 0000000..de0a4ed --- /dev/null +++ b/src/modules/category/DTO/UpdateThemeValue.dto.ts @@ -0,0 +1,29 @@ +import { Expose } from "class-transformer"; +import { IsNotEmpty, IsOptional, IsString, Length } from "class-validator"; + +import { ApiProperty } from "../../../common/decorator/swggerDocs"; +import { IsValidId } from "../../../common/decorator/validation.decorator"; +import { ThemeValueModel } from "../models/themeValue.model"; + +export class UpdateThemeValueDTO { + @Expose() + @IsNotEmpty() + @IsString() + @Length(24, 24) + @IsValidId(ThemeValueModel) + @ApiProperty({ type: "string", description: "theme value id", example: "66f3bcaee566db722a044c62" }) + themeValueId: string; + + @Expose() + @IsOptional() + @IsNotEmpty() + @IsString() + @ApiProperty({ type: "string", description: "the name of theme value", example: "Red" }) + name?: string; + + @Expose() + @IsOptional() + @IsNotEmpty() + @ApiProperty({ type: "string", description: "the value of theme value (can be string, number, or color hex)", example: "#FF0000" }) + value?: string | number; +} diff --git a/src/modules/category/DTO/createCategoryTheme.dto.ts b/src/modules/category/DTO/createCategoryTheme.dto.ts index 023eab9..23ffb53 100644 --- a/src/modules/category/DTO/createCategoryTheme.dto.ts +++ b/src/modules/category/DTO/createCategoryTheme.dto.ts @@ -1,9 +1,10 @@ -import { PickType } from "@nestjs/mapped-types"; +import { PartialType } from "@nestjs/mapped-types"; import { Expose, Type } from "class-transformer"; -import { ArrayMinSize, IsEnum, IsHexColor, IsNotEmpty, IsOptional, ValidateNested } from "class-validator"; +import { ArrayMinSize, IsHexColor, IsNotEmpty, IsOptional, ValidateNested } from "class-validator"; import { ApiProperty } from "../../../common/decorator/swggerDocs"; -import { CategoryThemeEnum } from "../../../common/enums/category.enum"; +import { IsValidId } from "../../../common/decorator/validation.decorator"; +import { ThemeModel } from "../models/theme.model"; export class CreateColorDTO { @Expose() @@ -30,14 +31,13 @@ export class CreateMeterageDTO { export class CreateCategoryThemeDTO { @Expose() - @IsNotEmpty() - @IsEnum(CategoryThemeEnum) + @IsOptional() + @IsValidId(ThemeModel) @ApiProperty({ type: "string", - description: "the variation theme of a category ==> should be one of Color/Size/Meterage or noColor_noSize", - example: "Color", + description: "theme id", }) - theme: CategoryThemeEnum; + themeId?: string; @Expose() @IsOptional() @@ -80,4 +80,4 @@ export class CreateCategoryThemeDTO { meterages?: CreateMeterageDTO[]; } -export class UpdateCategoryVariantDTO extends PickType(CreateCategoryThemeDTO, ["colors", "sizes", "meterages"] as const) {} +export class UpdateCategoryVariantDTO extends PartialType(CreateCategoryThemeDTO) {} diff --git a/src/modules/category/category.repository.ts b/src/modules/category/category.repository.ts index 5ada8d8..f373c7f 100644 --- a/src/modules/category/category.repository.ts +++ b/src/modules/category/category.repository.ts @@ -4,15 +4,13 @@ import { SellerCategorySearchDTO } from "./DTO/category-sarch.dto"; import { IAttributeValue } from "./models/Abstraction/IAttributeValue"; import { ICategory } from "./models/Abstraction/ICategory"; import { ICategoryAttribute } from "./models/Abstraction/ICategoryAttributes"; -import { IColor } from "./models/Abstraction/IColor"; -import { IMeterage } from "./models/Abstraction/IMeterage"; -import { ISize } from "./models/Abstraction/ISize"; +import { ITheme } from "./models/Abstraction/ITheme"; +import { IThemeValue } from "./models/Abstraction/IThemeValue"; import { AttributeValueModel } from "./models/attributeValue.model"; import { CategoryModel } from "./models/category.model"; import { CategoryAttributeModel } from "./models/CategoryAttribute.model"; -import { ColorModel } from "./models/color.model"; -import { MeterageModel } from "./models/meterage.model"; -import { SizeModel } from "./models/size.model"; +import { ThemeModel } from "./models/theme.model"; +import { ThemeValueModel } from "./models/themeValue.model"; import { BaseRepository } from "../../common/base/repository"; import { BadRequestError } from "../../core/app/app.errors"; const { ObjectId } = Types; @@ -167,21 +165,21 @@ class CategoryRepository extends BaseRepository { const result: Record = {}; // Step 4: Perform a single lookup per theme - for (const [theme, variantIdsSet] of themeVariantsMap.entries()) { - const variantIds = Array.from(variantIdsSet).map((id) => +id); + // for (const [theme, variantIdsSet] of themeVariantsMap.entries()) { + // const variantIds = Array.from(variantIdsSet).map((id) => +id); - if (variantIds.length > 0) { - let variantsData; - // Fetch the variants from the corresponding theme collection - if (theme === "colors") variantsData = await ColorModel.find({ _id: { $in: variantIds } }, { __v: 0 }).lean(); - if (theme === "sizes") variantsData = await SizeModel.find({ _id: { $in: variantIds } }, { __v: 0 }).lean(); - if (theme === "meterages") variantsData = await MeterageModel.find({ _id: { $in: variantIds } }, { __v: 0 }).lean(); - // throw new Error("theme not valid"); + // // if (variantIds.length > 0) { + // // let variantsData; + // // // Fetch the variants from the corresponding theme collection + // // if (theme === "colors") variantsData = await ColorModel.find({ _id: { $in: variantIds } }, { __v: 0 }).lean(); + // // if (theme === "sizes") variantsData = await SizeModel.find({ _id: { $in: variantIds } }, { __v: 0 }).lean(); + // // if (theme === "meterages") variantsData = await MeterageModel.find({ _id: { $in: variantIds } }, { __v: 0 }).lean(); + // // // throw new Error("theme not valid"); - // Store the result for this theme - result[theme] = variantsData as any; - } - } + // // // Store the result for this theme + // // result[theme] = variantsData as any; + // // } + // } return result; } @@ -356,48 +354,38 @@ function createAttributeValueRepo(): AttributeValueRepo { } //=====================>size model repository -class SizeRepository extends BaseRepository { + +//=====================>theme model repo +class ThemeRepository extends BaseRepository { constructor() { - super(SizeModel); + super(ThemeModel); } } -function createSizeRepo(): SizeRepository { - return new SizeRepository(); +function createThemeRepo(): ThemeRepository { + return new ThemeRepository(); } -//=====================>color model repo -class ColorRepository extends BaseRepository { +//=====================>theme value model repo +class ThemeValueRepository extends BaseRepository { constructor() { - super(ColorModel); + super(ThemeValueModel); } } -function createColorRepo(): ColorRepository { - return new ColorRepository(); -} - -//=====================>meterage model repo -class MeterageRepository extends BaseRepository { - constructor() { - super(MeterageModel); - } -} -function createMeterageRepo(): MeterageRepository { - return new MeterageRepository(); +function createThemeValueRepo(): ThemeValueRepository { + return new ThemeValueRepository(); } export { createCategoryRepo, createCategoryAttributeRepo, - createSizeRepo, - createMeterageRepo, - createColorRepo, createAttributeValueRepo, + createThemeRepo, + createThemeValueRepo, AttributeValueRepo, CategoryRepository, CategoryAttributeRepo, - SizeRepository, - ColorRepository, - MeterageRepository, + ThemeRepository, + ThemeValueRepository, }; // async getCategoryVariant(catId: string) { diff --git a/src/modules/category/category.service.ts b/src/modules/category/category.service.ts index 0511381..4f3b391 100644 --- a/src/modules/category/category.service.ts +++ b/src/modules/category/category.service.ts @@ -1,42 +1,31 @@ import { inject, injectable } from "inversify"; -import { ClientSession, FilterQuery, isValidObjectId, startSession } from "mongoose"; +import { FilterQuery, Types, isValidObjectId, startSession } from "mongoose"; import slugify from "slugify"; import { AttributeValueRepo, CategoryAttributeRepo, CategoryRepository, - ColorRepository, - MeterageRepository, - SizeRepository, + ThemeRepository, + ThemeValueRepository, } from "./category.repository"; import { BrandRepository } from "../brand/brand.repository"; import { CategoryQueryDto, CategorySearchQueryDTO, SellerCategorySearchDTO } from "./DTO/category-sarch.dto"; import { CategoryAttributeDTO, CategoryTreeDTO, CategoryVariantDTO } from "./DTO/category.dto"; import { CreateCategoryDTO } from "./DTO/CreateCategory.dto"; import { CreateCategoryAttDTO } from "./DTO/createCategoryAtt.dto"; -import { - CreateCategoryThemeDTO, - CreateColorDTO, - CreateMeterageDTO, - CreateSizeDTO, - UpdateCategoryVariantDTO, -} from "./DTO/createCategoryTheme.dto"; +import { CreateCategoryThemeDTO, UpdateCategoryVariantDTO } from "./DTO/createCategoryTheme.dto"; import { UpdateCategoryDTO } from "./DTO/UpdateCategory.dto"; import { UpdateCategoryAttDTO } from "./DTO/updateCategoryAtt.dto"; -import { IColor } from "./models/Abstraction/IColor"; -import { IMeterage } from "./models/Abstraction/IMeterage"; -import { ISize } from "./models/Abstraction/ISize"; -import { BaseRepository } from "../../common/base/repository"; -import { CategoryThemeEnum } from "../../common/enums/category.enum"; +import { ICategory } from "./models/Abstraction/ICategory"; +import { ThemeModel } from "./models/theme.model"; import { CategoryAttrMessage, CategoryMessage, CommonMessage } from "../../common/enums/message.enum"; import { BadRequestError, ForbiddenError } from "../../core/app/app.errors"; import { IOCTYPES } from "../../IOC/ioc.types"; +import { paginationUtils } from "../../utils/pagination.utils"; import { ProductDTO } from "../product/DTO/product.dto"; import { IProduct } from "../product/models/Abstraction/IProduct"; import { ProductRepository } from "../product/Repository/product"; -import { ICategory } from "./models/Abstraction/ICategory"; -import { paginationUtils } from "../../utils/pagination.utils"; @injectable() class CategoryService { @@ -45,9 +34,8 @@ class CategoryService { @inject(IOCTYPES.BrandRepository) brandRepo: BrandRepository; @inject(IOCTYPES.CategoryAttributeRepository) categoryAttRepo: CategoryAttributeRepo; @inject(IOCTYPES.AttributeValueRepository) attributeValueRepo: AttributeValueRepo; - @inject(IOCTYPES.ColorRepository) colorRepo: ColorRepository; - @inject(IOCTYPES.SizeRepository) sizeRepo: SizeRepository; - @inject(IOCTYPES.MeterageRepository) meterageRepo: MeterageRepository; + @inject(IOCTYPES.ThemeRepository) themeRepo: ThemeRepository; + @inject(IOCTYPES.ThemeValueRepository) themeValueRepo: ThemeValueRepository; async getCategories() { return this.categoryRepository.model.find({ parent: null }); @@ -102,8 +90,10 @@ class CategoryService { if (existTitle) throw new BadRequestError(CategoryMessage.DuplicateTitle); //TODO: check if parent is valid for update theme - if (updateDto.theme) { - await this.categoryRepository.model.findOneAndUpdate({ _id: category._id.toString() }, { theme: updateDto.theme, variants: [] }); + let theme: typeof ThemeModel | null = null; + if (updateDto.themeId) { + theme = await this.themeRepo.model.findById(updateDto.themeId); + if (!theme) throw new BadRequestError(CommonMessage.NotFoundById); } if (updateDto.parent) { // Set the parent category to leaf: false (since it now has a child) @@ -120,7 +110,12 @@ class CategoryService { updatedCategory, }; } - const updatedCategory = await this.categoryRepository.model.findByIdAndUpdate(category._id.toString(), updateDto, { new: true }); + + const updatedCategory = await this.categoryRepository.model.findByIdAndUpdate( + category._id.toString(), + { ...updateDto, theme: updateDto.themeId }, + { new: true }, + ); return { message: CategoryMessage.Updated, updatedCategory, @@ -133,13 +128,16 @@ class CategoryService { const existTitle = await this.categoryRepository.findByTitle(createDto.title_en); + const theme = await this.themeRepo.findById(createDto.themeId); + if (!theme) throw new BadRequestError(CommonMessage.NotFoundById); + if (existTitle) throw new BadRequestError(CategoryMessage.DuplicateTitle); //if parent exist first set parent leaf false if (createDto.parent) { await this.categoryRepository.model.findByIdAndUpdate(createDto.parent, { leaf: false }); } //we set the leaf to true - const category = await this.categoryRepository.model.create(createDto); + const category = await this.categoryRepository.model.create({ ...createDto, theme: theme._id }); return { message: CategoryMessage.Created, @@ -177,68 +175,44 @@ class CategoryService { //################################ async updateCategoryVariantS(updateDto: UpdateCategoryVariantDTO, id: string) { - const session = await startSession(); - session.startTransaction(); - try { - const { colors, sizes, meterages } = updateDto; + const { themeId } = updateDto; - const category = await this.checkCategoryId(id); + const category = await this.checkCategoryId(id); - // Ensure the theme matches the provided attributes and create variants - const variantRefs = await this.createVariantsBasedOnTheme(session, category.theme, colors, sizes, meterages); - - // Update the category with the theme and variants - category.variants = variantRefs; - await category.save({ session }); - - await session.commitTransaction(); - - return { - message: CommonMessage.Updated, - category, - }; - } catch (error) { - await session.abortTransaction(); - throw error; - } finally { - await session.endSession(); + if (themeId) { + const theme = await this.themeRepo.findById(themeId); + if (!theme) throw new BadRequestError(CommonMessage.NotFoundById); + category.theme = new Types.ObjectId(themeId); } + + return { + message: CommonMessage.Updated, + category, + }; } //################################ async createCategoryThemeS(createCategoryThemeDto: CreateCategoryThemeDTO, catId: string) { - const session = await startSession(); - session.startTransaction(); - try { - const { theme, colors, sizes, meterages } = createCategoryThemeDto; + const { themeId } = createCategoryThemeDto; - // Validate category ID - const category = await this.checkCategoryId(catId); - - // Check if the theme already exists - // if (category.theme) throw new BadRequestError(CategoryMessage.AlreadyExist); - - // Ensure the theme matches the provided attributes and create variants - const variantRefs = await this.createVariantsBasedOnTheme(session, theme, colors, sizes, meterages); - - // Update the category with the theme and variants - category.theme = theme; - category.variants = variantRefs; - await category.save({ session }); - - // Commit the transaction - await session.commitTransaction(); - session.endSession(); - - return { - message: CategoryMessage.VariantCreated, - variant: category, - }; - } catch (error) { - // Rollback any changes made in the transaction - await session.abortTransaction(); - session.endSession(); - throw error; // Re-throw the error to be handled by the calling errorHandler + // Validate category ID + const category = await this.checkCategoryId(catId); + // Check if the theme already exists + if (themeId) { + const theme = await this.themeRepo.findById(themeId); + if (!theme) throw new BadRequestError(CommonMessage.NotFoundById); } + + // Update the category with the theme and variants + if (themeId) { + // Cast themeId to ObjectId if necessary to satisfy category.theme type + category.theme = new Types.ObjectId(themeId); + } + await category.save(); + + return { + message: CategoryMessage.VariantCreated, + variant: category, + }; } async createCategoryAttributeS(createAttributeDto: CreateCategoryAttDTO, categoryId: string) { @@ -481,61 +455,6 @@ class CategoryService { } //Helper method to Create variants based on the provided theme - public async createVariantsBasedOnTheme( - session: ClientSession, - theme: CategoryThemeEnum, - colors?: CreateColorDTO[], - sizes?: CreateSizeDTO[], - meterages?: CreateMeterageDTO[], - ): Promise { - const variantRefs: number[] = []; - - if (theme === CategoryThemeEnum.Colored) { - if (colors && colors.length > 0) { - await this.processVariants(this.colorRepo, "name", colors, session, variantRefs); - } else { - throw new BadRequestError(CategoryMessage.MissingColors); - } - } else if (theme === CategoryThemeEnum.Sized) { - if (sizes && sizes.length > 0) { - await this.processVariants(this.sizeRepo, "value", sizes, session, variantRefs); - } else { - throw new BadRequestError(CategoryMessage.MissingSizes); - } - } else if (theme === CategoryThemeEnum.Meterage) { - if (meterages && meterages.length > 0) { - await this.processVariants(this.meterageRepo, "value", meterages, session, variantRefs); - } else { - throw new BadRequestError(CategoryMessage.MissingMeterages); - } - } else if (theme === CategoryThemeEnum.No_color_No_sized) { - return []; - } else { - throw new BadRequestError(CategoryMessage.InvalidTheme); - } - - return variantRefs; - } - - // Generic function to process create variants - private async processVariants( - repo: BaseRepository, - field: keyof T, - items: T[], - session: ClientSession, - variantRefs: number[], - ) { - for (const item of items) { - const query = { [field]: item[field] } as FilterQuery; - const existingItem = await repo.model.exists({ ...query }); - if (existingItem) { - variantRefs.push(existingItem._id); - } else { - const newItem = await repo.model.create([{ ...item }], { session }); - variantRefs.push(newItem[0]._id); - } - } - } } export { CategoryService }; diff --git a/src/modules/category/models/Abstraction/ICategory.ts b/src/modules/category/models/Abstraction/ICategory.ts index a593f5b..4e57fbb 100644 --- a/src/modules/category/models/Abstraction/ICategory.ts +++ b/src/modules/category/models/Abstraction/ICategory.ts @@ -1,7 +1,5 @@ import { Types } from "mongoose"; -import { CategoryThemeEnum } from "../../../../common/enums/category.enum"; - export interface ICategory { _id: Types.ObjectId; title_fa: string; @@ -9,8 +7,8 @@ export interface ICategory { icon: string; imageUrl: string; description: string; - theme: CategoryThemeEnum; - variants: number[]; + theme: Types.ObjectId; + variants: Types.ObjectId[]; leaf: boolean; parent: Types.ObjectId; url: string; diff --git a/src/modules/category/models/Abstraction/IColor.ts b/src/modules/category/models/Abstraction/IColor.ts deleted file mode 100644 index 8f8fbd9..0000000 --- a/src/modules/category/models/Abstraction/IColor.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface IColor { - _id: number; - name: string; - hexColor: string; -} diff --git a/src/modules/category/models/Abstraction/IMeterage.ts b/src/modules/category/models/Abstraction/IMeterage.ts deleted file mode 100644 index ecacb6f..0000000 --- a/src/modules/category/models/Abstraction/IMeterage.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IMeterage { - _id: number; - value: string; -} diff --git a/src/modules/category/models/Abstraction/ISize.ts b/src/modules/category/models/Abstraction/ISize.ts deleted file mode 100644 index ec3b362..0000000 --- a/src/modules/category/models/Abstraction/ISize.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ISize { - _id: number; - value: string; -} diff --git a/src/modules/category/models/Abstraction/ITheme.ts b/src/modules/category/models/Abstraction/ITheme.ts new file mode 100644 index 0000000..8b3e315 --- /dev/null +++ b/src/modules/category/models/Abstraction/ITheme.ts @@ -0,0 +1,7 @@ +import { Types } from "mongoose"; + +export interface ITheme { + _id: Types.ObjectId; + title: string; + inputType: string; +} diff --git a/src/modules/category/models/Abstraction/IThemeValue.ts b/src/modules/category/models/Abstraction/IThemeValue.ts new file mode 100644 index 0000000..88cd130 --- /dev/null +++ b/src/modules/category/models/Abstraction/IThemeValue.ts @@ -0,0 +1,8 @@ +import { Types } from "mongoose"; + +export interface IThemeValue { + _id: Types.ObjectId; + theme: Types.ObjectId; + name: string; + value: string; +} diff --git a/src/modules/category/models/category.model.ts b/src/modules/category/models/category.model.ts index 9f7387e..5d75689 100644 --- a/src/modules/category/models/category.model.ts +++ b/src/modules/category/models/category.model.ts @@ -1,7 +1,6 @@ import { CallbackError, Schema, model } from "mongoose"; import { ICategory } from "./Abstraction/ICategory"; -import { CategoryThemeEnum } from "../../../common/enums/category.enum"; const categorySchema = new Schema( { @@ -10,8 +9,8 @@ const categorySchema = new Schema( icon: { type: String, default: "/images/icons/default.png" }, imageUrl: { type: String, required: true }, description: { type: String, required: true }, - theme: { type: String, enum: CategoryThemeEnum }, - variants: [{ type: Number, refPath: "theme" }], + theme: { type: Schema.Types.ObjectId, ref: "Theme" }, + variants: [{ type: Schema.Types.ObjectId, ref: "ThemeValue" }], hierarchy: [{ type: Schema.Types.ObjectId, ref: "Category" }], leaf: { type: Boolean, default: true }, parent: { type: Schema.Types.ObjectId, ref: "Category", default: null }, diff --git a/src/modules/category/models/color.model.ts b/src/modules/category/models/color.model.ts deleted file mode 100644 index 2a03a8c..0000000 --- a/src/modules/category/models/color.model.ts +++ /dev/null @@ -1,21 +0,0 @@ -import mongoose, { Schema, model } from "mongoose"; - -import { IColor } from "./Abstraction/IColor"; -import { CategoryThemeEnum } from "../../../common/enums/category.enum"; - -// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports -const AutoIncrement = require("mongoose-sequence")(mongoose); - -const ColorSchema = new Schema( - { - _id: Number, - name: { type: String, unique: true, required: true }, - hexColor: { type: String, unique: true, required: true }, - }, - { timestamps: false, _id: false, toJSON: { versionKey: false } }, -); - -ColorSchema.plugin(AutoIncrement, { id: "color_id", inc_field: "_id" }); -const ColorModel = model(CategoryThemeEnum.Colored, ColorSchema); - -export { ColorModel }; diff --git a/src/modules/category/models/meterage.model.ts b/src/modules/category/models/meterage.model.ts deleted file mode 100644 index 1362f5d..0000000 --- a/src/modules/category/models/meterage.model.ts +++ /dev/null @@ -1,20 +0,0 @@ -import mongoose, { Schema, model } from "mongoose"; - -import { IMeterage } from "./Abstraction/IMeterage"; -import { CategoryThemeEnum } from "../../../common/enums/category.enum"; - -// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -const AutoIncrement = require("mongoose-sequence")(mongoose); - -const MeterageSchema = new Schema( - { - _id: Number, - value: { type: String, unique: true, required: true }, - }, - { timestamps: false, _id: false, toJSON: { versionKey: false } }, -); - -MeterageSchema.plugin(AutoIncrement, { id: "meterage_id", inc_field: "_id" }); -const MeterageModel = model(CategoryThemeEnum.Meterage, MeterageSchema); - -export { MeterageModel }; diff --git a/src/modules/category/models/size.model.ts b/src/modules/category/models/size.model.ts deleted file mode 100644 index d551c55..0000000 --- a/src/modules/category/models/size.model.ts +++ /dev/null @@ -1,20 +0,0 @@ -import mongoose, { Schema, model } from "mongoose"; - -import { ISize } from "./Abstraction/ISize"; -import { CategoryThemeEnum } from "../../../common/enums/category.enum"; - -// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports -const AutoIncrement = require("mongoose-sequence")(mongoose); - -const SizeSchema = new Schema( - { - _id: Number, - value: { type: String, unique: true, required: true }, - }, - { timestamps: false, _id: false, toJSON: { versionKey: false } }, -); - -SizeSchema.plugin(AutoIncrement, { id: "size_id", inc_field: "_id" }); -const SizeModel = model(CategoryThemeEnum.Sized, SizeSchema); - -export { SizeModel }; diff --git a/src/modules/category/models/theme.model.ts b/src/modules/category/models/theme.model.ts new file mode 100644 index 0000000..1e83d20 --- /dev/null +++ b/src/modules/category/models/theme.model.ts @@ -0,0 +1,18 @@ +import { Schema, model } from "mongoose"; + +import { ITheme } from "./Abstraction/ITheme"; +export enum InputTypeEnum { + Text = "text", + Number = "number", + Color = "color", +} +const themeSchema = new Schema( + { + title: { type: String, required: true }, + inputType: { type: String, enum: InputTypeEnum, required: true }, + }, + { timestamps: true, toJSON: { virtuals: true, versionKey: false }, id: false }, +); + +const ThemeModel = model("Theme", themeSchema); +export { ThemeModel }; diff --git a/src/modules/category/models/themeValue.model.ts b/src/modules/category/models/themeValue.model.ts new file mode 100644 index 0000000..296e37b --- /dev/null +++ b/src/modules/category/models/themeValue.model.ts @@ -0,0 +1,15 @@ +import { Schema, model } from "mongoose"; + +import { IThemeValue } from "./Abstraction/IThemeValue"; + +const themeValueSchema = new Schema( + { + theme: { type: Schema.Types.ObjectId, ref: "Theme", required: true }, + name: { type: String, required: true }, + value: { type: Schema.Types.Mixed, required: true }, + }, + { timestamps: true, toJSON: { virtuals: true, versionKey: false }, id: false }, +); + +const ThemeValueModel = model("ThemeValue", themeValueSchema); +export { ThemeValueModel }; diff --git a/src/modules/category/theme.service.ts b/src/modules/category/theme.service.ts new file mode 100644 index 0000000..ad193ab --- /dev/null +++ b/src/modules/category/theme.service.ts @@ -0,0 +1,151 @@ +import { inject, injectable } from "inversify"; +import { isValidObjectId } from "mongoose"; + +import { ThemeRepository, ThemeValueRepository } from "./category.repository"; +import { CreateThemeDTO } from "./DTO/CreateTheme.dto"; +import { CreateThemeValueDTO } from "./DTO/CreateThemeValue.dto"; +import { UpdateThemeDTO } from "./DTO/UpdateTheme.dto"; +import { UpdateThemeValueDTO } from "./DTO/UpdateThemeValue.dto"; +import { CommonMessage } from "../../common/enums/message.enum"; +import { BadRequestError } from "../../core/app/app.errors"; +import { IOCTYPES } from "../../IOC/ioc.types"; + +@injectable() +class ThemeService { + @inject(IOCTYPES.ThemeRepository) private themeRepository: ThemeRepository; + @inject(IOCTYPES.ThemeValueRepository) private themeValueRepository: ThemeValueRepository; + + async getAllThemes() { + return this.themeRepository.findAll(); + } + + async getThemeById(id: string) { + if (!isValidObjectId(id)) throw new BadRequestError(CommonMessage.NotValidId); + const theme = await this.themeRepository.findById(id); + if (!theme) throw new BadRequestError(CommonMessage.NotFoundById); + return theme; + } + + async createTheme(createDto: CreateThemeDTO) { + const existingTheme = await this.themeRepository.model.findOne({ title: createDto.title }); + if (existingTheme) throw new BadRequestError(CommonMessage.DuplicateName); + + const theme = await this.themeRepository.model.create(createDto); + return { + message: CommonMessage.Created, + theme, + }; + } + + async updateTheme(updateDto: UpdateThemeDTO) { + if (!isValidObjectId(updateDto.themeId)) throw new BadRequestError(CommonMessage.NotValidId); + + const existingTheme = await this.themeRepository.findById(updateDto.themeId); + if (!existingTheme) throw new BadRequestError(CommonMessage.NotFoundById); + + const updateData: Partial<{ title: string; inputType: string }> = {}; + + if (updateDto.title) { + const duplicateTheme = await this.themeRepository.model.findOne({ + title: updateDto.title, + _id: { $ne: updateDto.themeId }, + }); + if (duplicateTheme) throw new BadRequestError(CommonMessage.DuplicateName); + updateData.title = updateDto.title; + } + + if (updateDto.inputType) { + updateData.inputType = updateDto.inputType; + } + + const updatedTheme = await this.themeRepository.model.findByIdAndUpdate(updateDto.themeId, updateData, { new: true }); + + return { + message: CommonMessage.Updated, + theme: updatedTheme, + }; + } + + async deleteTheme(id: string) { + if (!isValidObjectId(id)) throw new BadRequestError(CommonMessage.NotValidId); + + const theme = await this.themeRepository.findById(id); + if (!theme) throw new BadRequestError(CommonMessage.NotFoundById); + + await this.themeRepository.delete(id); + return { + message: CommonMessage.Deleted, + }; + } + + //#########################################ThemeValue methods + async getAllThemeValues() { + return this.themeValueRepository.model.find().populate("theme"); + } + + async getThemeValueById(id: string) { + if (!isValidObjectId(id)) throw new BadRequestError(CommonMessage.NotValidId); + const themeValue = await this.themeValueRepository.model.findById(id).populate("theme"); + if (!themeValue) throw new BadRequestError(CommonMessage.NotFoundById); + return themeValue; + } + + async getThemeValuesByThemeId(themeId: string) { + if (!isValidObjectId(themeId)) throw new BadRequestError(CommonMessage.NotValidId); + const theme = await this.themeRepository.findById(themeId); + if (!theme) throw new BadRequestError(CommonMessage.NotFoundById); + return this.themeValueRepository.model.find({ theme: themeId }).populate("theme"); + } + + async createThemeValue(createDto: CreateThemeValueDTO) { + if (!isValidObjectId(createDto.theme)) throw new BadRequestError(CommonMessage.NotValidId); + const theme = await this.themeRepository.findById(createDto.theme); + if (!theme) throw new BadRequestError(CommonMessage.NotFoundById); + + const themeValue = await this.themeValueRepository.model.create(createDto); + return { + message: CommonMessage.Created, + themeValue, + }; + } + + async updateThemeValue(updateDto: UpdateThemeValueDTO) { + if (!isValidObjectId(updateDto.themeValueId)) throw new BadRequestError(CommonMessage.NotValidId); + + const existingThemeValue = await this.themeValueRepository.findById(updateDto.themeValueId); + if (!existingThemeValue) throw new BadRequestError(CommonMessage.NotFoundById); + + const updateData: Partial<{ name: string; value: string | number }> = {}; + + if (updateDto.name) { + updateData.name = updateDto.name; + } + + if (updateDto.value !== undefined) { + updateData.value = updateDto.value; + } + + const updatedThemeValue = await this.themeValueRepository.model + .findByIdAndUpdate(updateDto.themeValueId, updateData, { new: true }) + .populate("theme"); + + return { + message: CommonMessage.Updated, + themeValue: updatedThemeValue, + }; + } + + async deleteThemeValue(id: string) { + if (!isValidObjectId(id)) throw new BadRequestError(CommonMessage.NotValidId); + + const themeValue = await this.themeValueRepository.findById(id); + if (!themeValue) throw new BadRequestError(CommonMessage.NotFoundById); + + await this.themeValueRepository.delete(id); + return { + message: CommonMessage.Deleted, + }; + } +} + +export { ThemeService }; diff --git a/src/modules/product/DTO/addVariant.dto.ts b/src/modules/product/DTO/addVariant.dto.ts index 81f9d9d..58601c7 100644 --- a/src/modules/product/DTO/addVariant.dto.ts +++ b/src/modules/product/DTO/addVariant.dto.ts @@ -3,9 +3,7 @@ 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 { ThemeValueModel } from "../../category/models/themeValue.model"; import { WarrantyModel } from "../../warranty/models/warranty.model"; // import { ShipmentModel } from "../../../models/shipment.model"; @@ -19,23 +17,9 @@ export class AddVariantDTO { @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; + @IsValidId(ThemeValueModel) + @ApiProperty({ type: "string", description: "theme value id" }) + themeValueId?: string; @Expose() @IsNotEmpty() diff --git a/src/modules/product/models/Abstraction/IProductVariant.ts b/src/modules/product/models/Abstraction/IProductVariant.ts index 0a93d2d..e2d8129 100644 --- a/src/modules/product/models/Abstraction/IProductVariant.ts +++ b/src/modules/product/models/Abstraction/IProductVariant.ts @@ -1,14 +1,10 @@ import { Types } from "mongoose"; -import { CategoryThemeEnum } from "../../../../common/enums/category.enum"; import { ProductDiscountType, ProductMarketStatus } from "../../../../common/enums/product.enum"; // Dynamic type for category theme fields -type CategoryThemeFields = { - [K in Lowercase>]?: number; -}; -export interface IProductVariant extends CategoryThemeFields { +export interface IProductVariant { _id: Types.ObjectId; product: number; statistics: IStatistics; @@ -25,6 +21,7 @@ export interface IProductVariant extends CategoryThemeFields { saleFormat: ISaleFormat; dimensions: IDimensions; sellerSpecialCode: string; + themeValue: Types.ObjectId; } export interface IPrice { diff --git a/src/modules/product/models/productVariant.model.ts b/src/modules/product/models/productVariant.model.ts index bbb39f4..d3c9086 100644 --- a/src/modules/product/models/productVariant.model.ts +++ b/src/modules/product/models/productVariant.model.ts @@ -1,7 +1,6 @@ import { Schema, model } from "mongoose"; import { IDimensions, IPrice, IProductVariant, ISaleFormat, IStatistics } from "./Abstraction/IProductVariant"; -import { CategoryThemeEnum } from "../../../common/enums/category.enum"; import { ProductMarketStatus } from "../../../common/enums/product.enum"; const productStatisticsSchema = new Schema( @@ -45,24 +44,6 @@ const productPriceSchema = new Schema( { _id: false }, ); -// Utility function to generate dynamic category theme fields -const generateCategoryThemeFields = () => { - const fields: Record = {}; - - Object.values(CategoryThemeEnum).forEach((theme) => { - if (theme !== CategoryThemeEnum.No_color_No_sized) { - const fieldName = theme.toLowerCase(); - fields[fieldName] = { - type: Number, - ref: theme, - required: false, - }; - } - }); - - return fields; -}; - const productVariantSchema = new Schema( { product: { type: Number, ref: "Product", required: true }, @@ -78,7 +59,7 @@ const productVariantSchema = new Schema( saleFormat: { type: productSaleFormat }, dimensions: { type: productDimensionsSchema, required: true }, sellerSpecialCode: { type: String }, - ...generateCategoryThemeFields(), + themeValue: { type: Schema.Types.ObjectId, ref: "ThemeValue", required: true }, }, { timestamps: true, toObject: { virtuals: true, versionKey: false }, toJSON: { virtuals: true, versionKey: false }, id: false }, ); diff --git a/src/modules/product/providers/product.service.ts b/src/modules/product/providers/product.service.ts index 27677da..41b6433 100644 --- a/src/modules/product/providers/product.service.ts +++ b/src/modules/product/providers/product.service.ts @@ -1,9 +1,8 @@ import { inject, injectable } from "inversify"; -import { ClientSession, FilterQuery, isValidObjectId, startSession } from "mongoose"; +import { ClientSession, Types, isValidObjectId, startSession } from "mongoose"; import slugify from "slugify"; import { PaginationDTO } from "../../../common/dto/pagination.dto"; -import { CategoryThemeEnum } from "../../../common/enums/category.enum"; import { AdMessage, AttributeMessage, @@ -29,7 +28,7 @@ import { paginationUtils } from "../../../utils/pagination.utils"; import { TimeService } from "../../../utils/time.service"; import { AddIncredibleOffersParamDto } from "../../admin/DTO/product-param.dto"; import { BrandDTO } from "../../brand/DTO/brand.dto"; -import { AttributeValueRepo, CategoryAttributeRepo, CategoryRepository } from "../../category/category.repository"; +import { AttributeValueRepo, CategoryAttributeRepo, CategoryRepository, ThemeValueRepository } from "../../category/category.repository"; import { CategoryTreeDTO } from "../../category/DTO/category.dto"; import { NotificationService } from "../../notification/notification.service"; import { PricingRepository } from "../../pricing/pricing.repository"; @@ -96,6 +95,7 @@ class ProductService { @inject(IOCTYPES.ShopRepo) shopeRepo: ShopRepo; @inject(IOCTYPES.IncredibleOffersRepo) incredibleOffersRepo: IncredibleOffersRepo; @inject(IOCTYPES.NotificationService) notificationService: NotificationService; + @inject(IOCTYPES.ThemeValueRepository) themeValueRepo: ThemeValueRepository; //############################################################### async searchProducts(q: string) { @@ -557,41 +557,30 @@ class ProductService { if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound); this.ensureProductIsNotInDraft(product); - const category = await this.ensureCategoryTheme(product.category.toString(), addVariantDto); + await this.ensureCategoryTheme(product.category.toString(), addVariantDto); - await this.checkVariantCount(product, category.theme); + // await this.checkVariantCount(product, category.theme); - // Check if a variant with the same colorId, meterageId, or sizeId already exists - // Check if a variant with the same colorId, meterageId, or sizeId already exists - const query: FilterQuery = { product: product._id }; + // Check if a variant with the same themeValue already exists + // const query: FilterQuery = { product: product._id }; - if (addVariantDto.colorId) { - query.color = addVariantDto.colorId; - } - if (addVariantDto.meterageId) { - query.meterage = addVariantDto.meterageId; - } - if (addVariantDto.sizeId) { - query.size = addVariantDto.sizeId; - } + // if (addVariantDto.themeValueId) { + // const existingVariant = await this.productVariantRepo.model.findOne({ + // product: product._id, + // themeValue: addVariantDto.themeValueId, + // }); - if (category.toString() !== CategoryThemeEnum.No_color_No_sized) { - const existingVariant = await this.productVariantRepo.model.findOne(query); - - if (existingVariant) throw new BadRequestError(ProductMessage.VariantAlreadyExists); - } + // if (existingVariant) throw new BadRequestError(ProductMessage.VariantAlreadyExists); + // } const productVariant = await this.createProductVariant(addVariantDto, product._id, shop._id.toString(), session); await this.productRepo.model.findByIdAndUpdate(product._id, { $push: { variants: productVariant } }, { session }); // product.variants.push(productVariant[0]._id); // await product.save({ session }); - const { colorId, sizeId, meterageId } = addVariantDto; PriceChangeEvent.emitPriceChange({ - colorId, - sizeId, - meterageId, + themeValueId: addVariantDto.themeValueId, product: productId, variantId: productVariant[0]._id.toString(), shop: shop._id.toString(), @@ -654,9 +643,6 @@ class ProductService { ); PriceChangeEvent.emitPriceChange({ - colorId: updatedVariant?.color, - sizeId: updatedVariant?.size, - meterageId: updatedVariant?.meterage, product: productId, variantId: updatedVariant?._id.toString() as string, shop: shop._id.toString(), @@ -1447,33 +1433,10 @@ class ProductService { throw new BadRequestError(CategoryMessage.NotValidId); } - // Check if the category theme is color, size, or meterage - if (category.theme === CategoryThemeEnum.Colored) { - if (addVariantDto.sizeId || addVariantDto.meterageId) { - throw new BadRequestError(AttributeMessage.SizeNotAllowedForColoredTheme); - } - if (!addVariantDto.colorId) { - throw new BadRequestError(AttributeMessage.ColorRequiredForColoredTheme); - } - } else if (category.theme === CategoryThemeEnum.Sized) { - if (addVariantDto.colorId || addVariantDto.meterageId) { - throw new BadRequestError(AttributeMessage.ColorNotAllowedForSizedTheme); - } - if (!addVariantDto.sizeId) { - throw new BadRequestError(AttributeMessage.SizeRequiredForSizedTheme); - } - } else if (category.theme === CategoryThemeEnum.Meterage) { - if (addVariantDto.colorId || addVariantDto.sizeId) { - throw new BadRequestError(AttributeMessage.ColorOrSizeNotAllowedForMeterageTheme); - } - if (!addVariantDto.meterageId) { - throw new BadRequestError(AttributeMessage.MeterageRequiredForMeterageTheme); - } - } else { - // For other themes that are neither color, size, nor meterage - if (addVariantDto.sizeId || addVariantDto.colorId || addVariantDto.meterageId) { - throw new BadRequestError(AttributeMessage.ThemeIsNoColorNoSizeNoMeterage); - } + if (addVariantDto.themeValueId) { + const themeValue = await this.themeValueRepo.findById(addVariantDto.themeValueId); + if (!themeValue) throw new BadRequestError(CommonMessage.NotFoundById); + category.variants = [themeValue._id]; } return category; @@ -1549,9 +1512,7 @@ class ProductService { shop: shopId, warranty: addVariantDto.warrantyId, // shipmentMethod: addVariantDto.shipmentsId, - color: addVariantDto.colorId, - size: addVariantDto.sizeId, - meterage: addVariantDto.meterageId, + themeValue: new Types.ObjectId(addVariantDto.themeValueId), dimensions, market_status: ProductMarketStatus.Marketable, price: product_price, @@ -1611,16 +1572,6 @@ class ProductService { return totalCost; } - private async checkVariantCount(product: IProduct, categoryTheme: CategoryThemeEnum) { - if (categoryTheme !== CategoryThemeEnum.No_color_No_sized) return true; - - const variantCount = product.variants.length; - console.log("variantCount", variantCount); - if (!variantCount) return true; - console.log("pro", product, categoryTheme); - throw new BadRequestError(ProductMessage.ProductWithNoVariantCategoryTheme); - } - //############################################################### //###################### popular product ############################ @@ -1744,6 +1695,16 @@ class ProductService { // return searchCost; // } + + // private async checkVariantCount(product: IProduct, categoryTheme: Types.ObjectId | undefined) { + // if (categoryTheme) return true; + + // const variantCount = product.variants.length; + // console.log("variantCount", variantCount); + // if (!variantCount) return true; + // console.log("pro", product, categoryTheme); + // throw new BadRequestError(ProductMessage.ProductWithNoVariantCategoryTheme); + // } } export { ProductService }; diff --git a/src/utils/swagger.ts b/src/utils/swagger.ts index c7ff837..68d9abe 100644 --- a/src/utils/swagger.ts +++ b/src/utils/swagger.ts @@ -221,8 +221,22 @@ export function setupSwagger(app: Application, rootPath: string = "") { // Setup public Swagger docs app.use("/api-docs", basicAuth({ users: { admin: "admin" }, challenge: true })); - app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(generalSwaggerSpec)); + app.use( + "/api-docs", + swaggerUi.serve, + swaggerUi.setup(generalSwaggerSpec, { + swaggerOptions: { + persistAuthorization: true, + defaultModelsExpandDepth: -1, // Hide schemas section + }, + }), + ); // Setup admin Swagger docs - // app.use("/api-docs/admin", swaggerUi.serve, swaggerUi.setup(adminSwaggerSpec)); + // app.use("/api-docs/admin", swaggerUi.serve, swaggerUi.setup(adminSwaggerSpec, { + // swaggerOptions: { + // persistAuthorization: true, + // defaultModelsExpandDepth: -1, // Hide schemas section + // } + // })); }