add api category brand and fix product

This commit is contained in:
2026-03-29 15:54:09 +03:00
parent 532f2ed8e8
commit 6b002edd34
10 changed files with 1450 additions and 32 deletions

View File

@@ -55,6 +55,7 @@ Query params:
- `attributes` as JSON string
- `tags` as JSON string array
- `featured`
- `sort`: `newest | oldest | price_asc | price_desc | popular | top_rated`
- `page`
- `limit`
@@ -65,12 +66,76 @@ Optional request header:
Example:
```http
GET /api/products?search=skf&featured=true&page=1&limit=20
GET /api/products?search=skf&featured=true&sort=price_asc&page=1&limit=20
x-product-type: Industrial
```
List response items are intentionally lightweight and include only fields needed for product cards / list pages. The `attributes` array is limited to the first 2 visible attributes, sorted by `displayOrder`.
### `GET /api/products/brands/:brandSlug`
Lists published products for one brand by brand slug.
Supports the same query params as `GET /api/products`:
- `search`
- `featured`
- `sort`
- `page`
- `limit`
Optional request header:
- `x-product-type`: `Industrial | Automotive`
### `GET /api/products/brands/:brandSlug/filters`
Returns available brand page filters relative to the selected brand and current query filters.
Response shape:
- `categories`: available categories with count
- `attributes`: available filterable attributes and their values with count
- `stock`: `inStock` and `outOfStock`
Optional request header:
- `x-product-type`: `Industrial | Automotive`
### `GET /api/products/categories/:categorySlug`
Lists published products for one category by category slug.
Supports the same query params as `GET /api/products`:
- `search`
- `featured`
- `sort`
- `page`
- `limit`
Optional request header:
- `x-product-type`: `Industrial | Automotive`
### `GET /api/products/categories/:categorySlug/filters`
Returns available category page filters relative to the selected category and current query filters.
Response shape:
- `brands`: available brands with count
- `attributes`: available filterable attributes and their values with count
- `stock`: `inStock` and `outOfStock`
Optional request header:
- `x-product-type`: `Industrial | Automotive`
### `GET /api/product/:slug`
Returns one published product by slug with category and up to 10 approved reviews.
### `GET /api/products/:id`
Returns one published product with category and up to 10 approved reviews.
@@ -117,6 +182,7 @@ Query params:
- `attributes` as JSON string
- `tags` as JSON string array
- `featured`
- `sort`: `newest | oldest | price_asc | price_desc | popular | top_rated`
- `page`
- `limit`

View File

@@ -8,6 +8,7 @@
"start": "nest start",
"start:dev": "nest start --watch",
"start:prod": "node dist/main",
"seed:industrial-products": "ts-node -r tsconfig-paths/register src/scripts/seed-industrial-products.ts",
"lint": "eslint \"{src,test}/**/*.ts\" --fix",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"typeorm": "typeorm-ts-node-commonjs -d src/config/typeorm.config.ts",

View File

@@ -7,6 +7,7 @@ import { BrandController } from './brand.controller';
import { BrandService } from './brand.service';
import { CategoryController } from './category.controller';
import { CategoryService } from './category.service';
import { ProductDetailsController } from './product-details.controller';
import { AttributeDefinition } from './entities/attribute-definition.entity';
import { Brand } from './entities/brand.entity';
import { Category } from './entities/category.entity';
@@ -34,6 +35,7 @@ import { ProductsService } from './products.service';
CategoryController,
BrandController,
ProductsController,
ProductDetailsController,
AdminProductsController,
AttributeDefinitionsController,
],

View File

@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
import { plainToInstance, Transform, Type } from 'class-transformer';
import {
ArrayMaxSize,
ArrayUnique,
@@ -123,14 +123,26 @@ export class CreateProductDto {
@ApiPropertyOptional({ type: ProductMetaDto })
@IsOptional()
@Transform(parseJsonValue)
@Transform(({ value }) => {
const parsedValue = parseJsonValue({ value } as never);
return parsedValue ? plainToInstance(ProductMetaDto, parsedValue) : undefined;
})
@ValidateNested()
@Type(() => ProductMetaDto)
meta?: ProductMetaDto;
@ApiPropertyOptional({ type: [ProductAttributeInputDto] })
@IsOptional()
@Transform(parseJsonValue)
@Transform(({ value }) => {
const parsedValue = parseJsonValue({ value } as never);
if (!Array.isArray(parsedValue)) {
return parsedValue;
}
return parsedValue.map((item) =>
plainToInstance(ProductAttributeInputDto, item),
);
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => ProductAttributeInputDto)

View File

@@ -1,3 +1,4 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import {
IsArray,
@@ -11,45 +12,55 @@ import {
Min,
} from 'class-validator';
import { parseJsonValue } from '../../../common/utils/json-transform.util';
import { ProductListSort } from '../enums/product-list-sort.enum';
import { ProductStatus } from '../enums/product-status.enum';
import { ProductType } from '../enums/product-type.enum';
export class FilterProductsDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
search?: string;
@ApiPropertyOptional({ enum: ProductType })
@IsOptional()
@IsEnum(ProductType)
type?: ProductType;
@ApiPropertyOptional({ enum: ProductStatus })
@IsOptional()
@IsEnum(ProductStatus)
status?: ProductStatus;
@ApiPropertyOptional()
@IsOptional()
@IsUUID()
categoryId?: string;
@ApiPropertyOptional()
@IsOptional()
@IsUUID()
brandId?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
brand?: string;
@ApiPropertyOptional({ type: Object })
@IsOptional()
@Transform(parseJsonValue)
@IsObject()
attributes?: Record<string, unknown>;
@ApiPropertyOptional({ type: [String] })
@IsOptional()
@Transform(parseJsonValue)
@IsArray()
@IsString({ each: true })
tags?: string[];
@ApiPropertyOptional()
@IsOptional()
@Transform(({ value }) =>
typeof value === 'boolean' ? value : String(value).toLowerCase() === 'true',
@@ -57,12 +68,23 @@ export class FilterProductsDto {
@IsBoolean()
featured?: boolean;
@ApiPropertyOptional({
enum: ProductListSort,
description:
'Sort list by newest, oldest, cheapest, most expensive, most popular, or top rated.',
})
@IsOptional()
@IsEnum(ProductListSort)
sort?: ProductListSort;
@ApiPropertyOptional({ default: 1 })
@IsOptional()
@Transform(({ value }) => Number(value))
@IsInt()
@Min(1)
page?: number = 1;
@ApiPropertyOptional({ default: 20 })
@IsOptional()
@Transform(({ value }) => Number(value))
@IsInt()

View File

@@ -0,0 +1,8 @@
export enum ProductListSort {
NEWEST = 'newest',
OLDEST = 'oldest',
PRICE_ASC = 'price_asc',
PRICE_DESC = 'price_desc',
POPULAR = 'popular',
TOP_RATED = 'top_rated',
}

View File

@@ -0,0 +1,24 @@
import { Controller, Get, Headers, Param } from '@nestjs/common';
import { ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger';
import { ProductsService } from './products.service';
@ApiTags('Products')
@ApiHeader({
name: 'x-product-type',
required: false,
description: 'Optional product type filter header. Falls back to query param `type` if omitted.',
enum: ['Industrial', 'Automotive'],
})
@Controller('product')
export class ProductDetailsController {
constructor(private readonly productsService: ProductsService) {}
@Get(':slug')
@ApiOperation({ summary: 'Get one published product by slug' })
findBySlug(
@Param('slug') slug: string,
@Headers('x-product-type') productType?: string,
) {
return this.productsService.findPublicOneBySlug(slug, productType);
}
}

View File

@@ -24,13 +24,60 @@ export class ProductsController {
return this.productsService.findPublic(filters, productType);
}
@Get('slug/:slug')
@ApiOperation({ summary: 'Get one published product by slug' })
findBySlug(
@Param('slug') slug: string,
@Get('brands/:brandSlug')
@ApiOperation({ summary: 'List published products by brand slug' })
findByBrandSlug(
@Param('brandSlug') brandSlug: string,
@Query() filters: FilterProductsDto,
@Headers('x-product-type') productType?: string,
) {
return this.productsService.findPublicOneBySlug(slug, productType);
return this.productsService.findPublicByBrandSlug(
brandSlug,
filters,
productType,
);
}
@Get('brands/:brandSlug/filters')
@ApiOperation({ summary: 'Get available brand page filters by brand slug' })
findBrandFilters(
@Param('brandSlug') brandSlug: string,
@Query() filters: FilterProductsDto,
@Headers('x-product-type') productType?: string,
) {
return this.productsService.findPublicBrandFilters(
brandSlug,
filters,
productType,
);
}
@Get('categories/:categorySlug')
@ApiOperation({ summary: 'List published products by category slug' })
findByCategorySlug(
@Param('categorySlug') categorySlug: string,
@Query() filters: FilterProductsDto,
@Headers('x-product-type') productType?: string,
) {
return this.productsService.findPublicByCategorySlug(
categorySlug,
filters,
productType,
);
}
@Get('categories/:categorySlug/filters')
@ApiOperation({ summary: 'Get available category page filters by category slug' })
findCategoryFilters(
@Param('categorySlug') categorySlug: string,
@Query() filters: FilterProductsDto,
@Headers('x-product-type') productType?: string,
) {
return this.productsService.findPublicCategoryFilters(
categorySlug,
filters,
productType,
);
}
@Get(':id/reviews')

View File

@@ -24,6 +24,7 @@ import { ProductMeta } from './entities/product-meta.entity';
import { Product } from './entities/product.entity';
import { ProductReview } from './entities/product-review.entity';
import { AttributeDataType } from './enums/attribute-data-type.enum';
import { ProductListSort } from './enums/product-list-sort.enum';
import { ProductStatus } from './enums/product-status.enum';
import { ProductType } from './enums/product-type.enum';
@@ -118,6 +119,68 @@ export class ProductsService {
return this.findAll(this.mergeProductTypeFilter(filters, headerProductType), false);
}
async findPublicByBrandSlug(
brandSlug: string,
filters: FilterProductsDto,
headerProductType?: string,
) {
const mergedFilters = this.mergeProductTypeFilter(filters, headerProductType);
const brand = await this.resolveBrandBySlug(brandSlug, mergedFilters.type);
return this.findAll(
{
...mergedFilters,
brandId: brand.id,
},
false,
);
}
async findPublicByCategorySlug(
categorySlug: string,
filters: FilterProductsDto,
headerProductType?: string,
) {
const mergedFilters = this.mergeProductTypeFilter(filters, headerProductType);
const category = await this.resolveCategoryBySlug(categorySlug, mergedFilters.type);
return this.findAll(
{
...mergedFilters,
categoryId: category.id,
},
false,
);
}
async findPublicBrandFilters(
brandSlug: string,
filters: FilterProductsDto,
headerProductType?: string,
) {
const mergedFilters = this.mergeProductTypeFilter(filters, headerProductType);
const brand = await this.resolveBrandBySlug(brandSlug, mergedFilters.type);
return this.buildProductFacets({
...mergedFilters,
brandId: brand.id,
});
}
async findPublicCategoryFilters(
categorySlug: string,
filters: FilterProductsDto,
headerProductType?: string,
) {
const mergedFilters = this.mergeProductTypeFilter(filters, headerProductType);
const category = await this.resolveCategoryBySlug(categorySlug, mergedFilters.type);
return this.buildProductFacets({
...mergedFilters,
categoryId: category.id,
});
}
async findAdmin(filters: FilterProductsDto, headerProductType?: string) {
return this.findAll(this.mergeProductTypeFilter(filters, headerProductType), true);
}
@@ -467,16 +530,36 @@ export class ProductsService {
const page = filters.page ?? 1;
const limit = filters.limit ?? 20;
const query = this.createFilteredProductsQuery(filters, includeUnpublished)
.skip((page - 1) * limit)
.take(limit);
const [items, total] = await query.getManyAndCount();
return {
items: items.map((item) =>
includeUnpublished
? this.serializeAdminProductListItem(item)
: this.serializePublicProductListItem(item),
),
meta: {
total,
page,
limit,
},
};
}
private createFilteredProductsQuery(
filters: FilterProductsDto,
includeUnpublished: boolean,
) {
const query = this.productsRepository
.createQueryBuilder('product')
.leftJoinAndSelect('product.primaryCategory', 'primaryCategory')
.leftJoinAndSelect('product.categories', 'categories')
.leftJoinAndSelect('product.brandEntity', 'brandEntity')
.leftJoinAndSelect('product.meta', 'meta')
.orderBy('product.featured', 'DESC')
.addOrderBy('product.createdAt', 'DESC')
.skip((page - 1) * limit)
.take(limit);
.leftJoinAndSelect('product.meta', 'meta');
if (!includeUnpublished) {
query.andWhere('product.status = :publishedStatus', {
@@ -525,17 +608,9 @@ export class ProductsService {
}
this.applyAttributeFilters(query, filters.attributes);
this.applyProductSort(query, filters.sort);
const [items, total] = await query.getManyAndCount();
return {
items: items.map((item) => this.serializeProductListItem(item, includeUnpublished)),
meta: {
total,
page,
limit,
},
};
return query;
}
private applyAttributeFilters(
@@ -572,6 +647,210 @@ export class ProductsService {
});
}
private applyProductSort(
query: SelectQueryBuilder<Product>,
sort?: ProductListSort,
) {
switch (sort) {
case ProductListSort.OLDEST:
query.orderBy('product.createdAt', 'ASC').addOrderBy('product.id', 'ASC');
return;
case ProductListSort.PRICE_ASC:
query
.orderBy('COALESCE(product.sale_price_usd, product.base_price_usd)', 'ASC')
.addOrderBy('product.createdAt', 'DESC');
return;
case ProductListSort.PRICE_DESC:
query
.orderBy('COALESCE(product.sale_price_usd, product.base_price_usd)', 'DESC')
.addOrderBy('product.createdAt', 'DESC');
return;
case ProductListSort.POPULAR:
query
.orderBy('product.reviewsCount', 'DESC')
.addOrderBy('product.averageRating', 'DESC')
.addOrderBy('product.createdAt', 'DESC');
return;
case ProductListSort.TOP_RATED:
query
.orderBy('product.averageRating', 'DESC')
.addOrderBy('product.reviewsCount', 'DESC')
.addOrderBy('product.createdAt', 'DESC');
return;
case ProductListSort.NEWEST:
default:
query
.orderBy('product.featured', 'DESC')
.addOrderBy('product.createdAt', 'DESC')
.addOrderBy('product.id', 'DESC');
}
}
private async buildProductFacets(filters: FilterProductsDto) {
const products = await this.createFilteredProductsQuery(filters, false)
.leftJoinAndSelect('product.attributeValues', 'attributeValues')
.leftJoinAndSelect('attributeValues.attribute', 'attributeDefinition')
.getMany();
const categoryMap = new Map<
string,
{ id: string; name: string; slug: string; imageUrl: string | null; count: number }
>();
const brandMap = new Map<
string,
{ id: string; name: string; slug: string; imageUrl: string | null; count: number }
>();
const attributeMap = new Map<
string,
{
slug: string;
name: string;
values: Map<string, { value: string; count: number }>;
}
>();
let inStock = 0;
let outOfStock = 0;
for (const product of products) {
if ((product.stock ?? 0) > 0) {
inStock += 1;
} else {
outOfStock += 1;
}
if (product.brandEntity) {
const currentBrand = brandMap.get(product.brandEntity.id);
if (currentBrand) {
currentBrand.count += 1;
} else {
brandMap.set(product.brandEntity.id, {
id: product.brandEntity.id,
name: product.brandEntity.name,
slug: product.brandEntity.slug,
imageUrl: product.brandEntity.imageUrl ?? null,
count: 1,
});
}
}
const uniqueCategories = new Map<string, Product['primaryCategory']>();
if (product.primaryCategory) {
uniqueCategories.set(product.primaryCategory.id, product.primaryCategory);
}
for (const category of product.categories ?? []) {
uniqueCategories.set(category.id, category);
}
for (const category of uniqueCategories.values()) {
if (!category) {
continue;
}
const currentCategory = categoryMap.get(category.id);
if (currentCategory) {
currentCategory.count += 1;
} else {
categoryMap.set(category.id, {
id: category.id,
name: category.name,
slug: category.slug,
imageUrl: category.imageUrl ?? null,
count: 1,
});
}
}
for (const attributeValue of this.sortAttributes(product.attributeValues)) {
if (!attributeValue.attribute?.isVisible || !attributeValue.attribute?.isFilterable) {
continue;
}
const normalizedValue = this.normalizeAttributeFacetValue(attributeValue);
if (!normalizedValue) {
continue;
}
const attributeSlug = attributeValue.attribute.slug;
let attributeEntry = attributeMap.get(attributeSlug);
if (!attributeEntry) {
attributeEntry = {
slug: attributeSlug,
name: attributeValue.attribute.name,
values: new Map<string, { value: string; count: number }>(),
};
attributeMap.set(attributeSlug, attributeEntry);
}
const currentValue = attributeEntry.values.get(normalizedValue);
if (currentValue) {
currentValue.count += 1;
} else {
attributeEntry.values.set(normalizedValue, {
value: normalizedValue,
count: 1,
});
}
}
}
return {
categories: [...categoryMap.values()].sort((left, right) =>
left.name.localeCompare(right.name),
),
brands: [...brandMap.values()].sort((left, right) =>
left.name.localeCompare(right.name),
),
attributes: [...attributeMap.values()]
.map((attribute) => ({
slug: attribute.slug,
name: attribute.name,
values: [...attribute.values.values()].sort((left, right) =>
left.value.localeCompare(right.value),
),
}))
.sort((left, right) => left.name.localeCompare(right.name)),
stock: {
inStock,
outOfStock,
},
};
}
private normalizeAttributeFacetValue(attributeValue: ProductAttributeValue) {
const valueText = attributeValue.valueText ?? attributeValue.attribute?.defaultValueText;
if (valueText) {
return valueText;
}
const valueNumber =
attributeValue.valueNumber ??
(attributeValue.attribute?.defaultValueNumber !== null &&
attributeValue.attribute?.defaultValueNumber !== undefined
? Number(attributeValue.attribute.defaultValueNumber)
: null);
if (valueNumber !== null && valueNumber !== undefined) {
return String(valueNumber);
}
const valueBoolean =
attributeValue.valueBoolean ?? attributeValue.attribute?.defaultValueBoolean;
if (valueBoolean !== null && valueBoolean !== undefined) {
return valueBoolean ? 'true' : 'false';
}
const valueJson = attributeValue.valueJson ?? attributeValue.attribute?.defaultValueJson;
if (Array.isArray(valueJson)) {
return valueJson.join(', ');
}
if (valueJson && typeof valueJson === 'object') {
return JSON.stringify(valueJson);
}
return null;
}
private async findOneById(id: string) {
const product = await this.productsRepository.findOne({
where: { id },
@@ -656,6 +935,36 @@ export class ProductsService {
return brand;
}
private async resolveBrandBySlug(slug: string, type?: Product['type']) {
const brand = await this.brandsRepository.findOne({
where: {
slug,
...(type ? { type } : {}),
},
});
if (!brand) {
throw new NotFoundException('Brand not found');
}
return brand;
}
private async resolveCategoryBySlug(slug: string, type?: Product['type']) {
const category = await this.categoriesRepository.findOne({
where: {
slug,
...(type ? { type } : {}),
},
});
if (!category) {
throw new NotFoundException('Category not found');
}
return category;
}
private async ensurePublishedProduct(productId: string) {
const product = await this.productsRepository.findOne({
where: { id: productId, status: ProductStatus.PUBLISHED },
@@ -882,7 +1191,55 @@ export class ProductsService {
};
}
private serializeProductListItem(product: Product, includeAdminFields: boolean) {
private serializePublicProductListItem(product: Product) {
const meta = product.meta ?? null;
const orderedAttributes = this.sortAttributes(product.attributeValues).filter(
(item) => item.attribute?.isVisible ?? true,
);
return {
id: product.id,
title: product.title,
slug: product.slug,
brand: product.brand,
brandslug: product.brandEntity?.slug ?? null,
basePriceUSD: product.basePriceUSD,
salePriceUSD: product.salePriceUSD ?? null,
stock: product.stock,
averageRating: product.averageRating,
reviewsCount: product.reviewsCount,
mainImageUrl: product.mainImageUrl ?? null,
primaryCategory: product.primaryCategory
? {
id: product.primaryCategory.id,
name: product.primaryCategory.name,
slug: product.primaryCategory.slug,
imageUrl: product.primaryCategory.imageUrl,
}
: null,
attributes: orderedAttributes.slice(0, 2).map((item) => ({
id: item.id,
displayOrder: item.displayOrder ?? 0,
attributeId: item.attribute?.id,
name: item.attribute?.name,
slug: item.attribute?.slug,
dataType: item.attribute?.dataType,
unit: item.overrideUnit ?? item.attribute?.unit ?? null,
valueText: item.valueText ?? item.attribute?.defaultValueText ?? null,
valueNumber:
item.valueNumber ??
(item.attribute?.defaultValueNumber !== null &&
item.attribute?.defaultValueNumber !== undefined
? Number(item.attribute.defaultValueNumber)
: null),
valueBoolean:
item.valueBoolean ?? item.attribute?.defaultValueBoolean ?? null,
valueJson: item.valueJson ?? item.attribute?.defaultValueJson ?? null,
})),
};
}
private serializeAdminProductListItem(product: Product) {
const meta = product.meta ?? null;
const orderedAttributes = this.sortAttributes(product.attributeValues).filter(
(item) => item.attribute?.isVisible ?? true,
@@ -941,13 +1298,9 @@ export class ProductsService {
item.valueBoolean ?? item.attribute?.defaultValueBoolean ?? null,
valueJson: item.valueJson ?? item.attribute?.defaultValueJson ?? null,
})),
...(includeAdminFields
? {
status: product.status,
createdAt: product.createdAt,
updatedAt: product.updatedAt,
}
: {}),
status: product.status,
createdAt: product.createdAt,
updatedAt: product.updatedAt,
};
}

View File

@@ -0,0 +1,883 @@
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AppModule } from '../app.module';
import { CreateProductDto } from '../modules/catalog/dto/create-product.dto';
import { AttributeDefinition } from '../modules/catalog/entities/attribute-definition.entity';
import { Brand } from '../modules/catalog/entities/brand.entity';
import { Category } from '../modules/catalog/entities/category.entity';
import { ProductAttributeValue } from '../modules/catalog/entities/product-attribute-value.entity';
import { ProductMeta } from '../modules/catalog/entities/product-meta.entity';
import { Product } from '../modules/catalog/entities/product.entity';
import { AttributeDataType } from '../modules/catalog/enums/attribute-data-type.enum';
import { ProductStatus } from '../modules/catalog/enums/product-status.enum';
import { ProductType } from '../modules/catalog/enums/product-type.enum';
type SeedAttribute = {
name: string;
slug: string;
valueText: string;
displayOrder: number;
};
type SeedProductInput = {
brandId: string;
brandName: string;
categoryId: string;
categoryName: string;
model: string;
title: string;
description: string;
basePriceUSD: number;
salePriceUSD: number;
stock: number;
featured?: boolean;
tags: string[];
attributes: SeedAttribute[];
};
const categoryIds = {
deepGroove: '23b15696-fac5-4237-a544-0c3caee8a9f7',
selfAligning: '4e617026-3128-440b-9ef9-fcfe8292f76b',
tapered: '1e5b26fa-4c85-4456-bd1f-934cad3c2e33',
spherical: '5e3203a5-003b-4232-9825-b4232682cf5f',
thrust: 'f0cf3ff5-fb2e-4475-90b9-236551e38b90',
angularContact: '7ee944d8-dbe2-47d2-80e7-b722d1a9c4ab',
} as const;
const categoryNames = {
[categoryIds.deepGroove]: 'بلبرینگ‌های شیار عمیق',
[categoryIds.selfAligning]: 'بلبرینگ‌های خودتنظیم',
[categoryIds.tapered]: 'رولبرینگ‌های مخروطی',
[categoryIds.spherical]: 'رولبرینگ‌های بشکه‌ای',
[categoryIds.thrust]: 'بلبرینگ‌های کف‌گرد',
[categoryIds.angularContact]: 'بلبرینگ‌های تماس زاویه‌ای',
} satisfies Record<string, string>;
const brandIds = {
skf: 'd3466c35-f9cd-4ed5-8dba-1b19217d3498',
fag: '44bd3e53-c1f5-447d-9e2a-ee5175996ef4',
nsk: '89952168-20ee-4209-aff0-cbf8a915fd54',
timken: 'af81bbef-8ce9-4001-a193-cc472c7ccf15',
} as const;
const brandNames = {
[brandIds.skf]: 'SKF',
[brandIds.fag]: 'FAG',
[brandIds.nsk]: 'NSK',
[brandIds.timken]: 'TIMKEN',
} satisfies Record<string, string>;
const seedProducts: SeedProductInput[] = [
{
brandId: brandIds.skf,
brandName: 'SKF',
categoryId: categoryIds.deepGroove,
categoryName: categoryNames[categoryIds.deepGroove],
model: '6204-2RS',
title: 'بلبرینگ SKF مدل 6204-2RS',
description:
'بلبرینگ شیار عمیق تک ردیفه، دارای آب‌بند پلاستیکی در دو طرف (2RS) برای جلوگیری از نفوذ گرد و غبار. مناسب برای الکتروموتورها و پمپ‌های کوچک.',
basePriceUSD: 15,
salePriceUSD: 13,
stock: 36,
featured: true,
tags: ['industrial', 'deep-groove', 'skf', '2rs'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'شیار عمیق تک ردیفه', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'آب‌بند پلاستیکی دوطرفه 2RS', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'الکتروموتورها و پمپ‌های کوچک', displayOrder: 2 },
],
},
{
brandId: brandIds.fag,
brandName: 'FAG',
categoryId: categoryIds.deepGroove,
categoryName: categoryNames[categoryIds.deepGroove],
model: '6305-ZZ',
title: 'بلبرینگ FAG مدل 6305-ZZ',
description:
'بلبرینگ با واشر فلزی دوطرفه (ZZ). مقاوم در برابر حرارت بالا و مناسب برای کاربردهایی با سرعت چرخش زیاد و محیط‌های خشک.',
basePriceUSD: 18,
salePriceUSD: 16,
stock: 28,
tags: ['industrial', 'deep-groove', 'fag', 'zz'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'شیار عمیق تک ردیفه', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'واشر فلزی دوطرفه ZZ', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'سرعت چرخش بالا در محیط‌های خشک', displayOrder: 2 },
],
},
{
brandId: brandIds.nsk,
brandName: 'NSK',
categoryId: categoryIds.deepGroove,
categoryName: categoryNames[categoryIds.deepGroove],
model: '6008-DDU',
title: 'بلبرینگ NSK مدل 6008-DDU',
description:
'طراحی بهینه‌سازی شده برای کاهش اصطکاک. دارای کاسه نمد لاستیکی با تماس کامل، ایده‌آل برای تجهیزات کشاورزی و محیط‌های آلوده.',
basePriceUSD: 21,
salePriceUSD: 18,
stock: 24,
tags: ['industrial', 'deep-groove', 'nsk', 'ddu'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'شیار عمیق با اصطکاک پایین', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'کاسه نمد لاستیکی DDU با تماس کامل', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'تجهیزات کشاورزی و محیط‌های آلوده', displayOrder: 2 },
],
},
{
brandId: brandIds.skf,
brandName: 'SKF',
categoryId: categoryIds.deepGroove,
categoryName: categoryNames[categoryIds.deepGroove],
model: '6210-C3',
title: 'بلبرینگ SKF مدل 6210-C3',
description:
'دارای لقی داخلی بیشتر از حد استاندارد (C3) برای جبران انبساط حرارتی. پرکاربرد در گیربکس‌ها و شفت‌های دور بالا.',
basePriceUSD: 24,
salePriceUSD: 22,
stock: 18,
tags: ['industrial', 'deep-groove', 'skf', 'c3'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'شیار عمیق تک ردیفه', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'لقی داخلی بیشتر C3', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'گیربکس‌ها و شفت‌های دور بالا', displayOrder: 2 },
],
},
{
brandId: brandIds.fag,
brandName: 'FAG',
categoryId: categoryIds.deepGroove,
categoryName: categoryNames[categoryIds.deepGroove],
model: '6004',
title: 'بلبرینگ FAG مدل 6004',
description:
'بلبرینگ بدون آب‌بند (باز)، مناسب برای کار در محیط‌های غوطه‌ور در روغن با قابلیت تحمل بارهای شعاعی متوسط.',
basePriceUSD: 11,
salePriceUSD: 9,
stock: 44,
tags: ['industrial', 'deep-groove', 'fag', 'open'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'بلبرینگ باز بدون آب‌بند', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'مناسب کارکرد در روغن', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'بارهای شعاعی متوسط در سیستم‌های روغن‌کاری‌شده', displayOrder: 2 },
],
},
{
brandId: brandIds.skf,
brandName: 'SKF',
categoryId: categoryIds.angularContact,
categoryName: categoryNames[categoryIds.angularContact],
model: '7205 BEP',
title: 'بلبرینگ SKF مدل 7205 BEP',
description:
'بلبرینگ تماس زاویه‌ای تک ردیفه با زاویه تماس ۴۰ درجه و قفسه پلی‌آمیدی. مناسب برای تحمل همزمان بارهای شعاعی و محوری یک‌طرفه.',
basePriceUSD: 29,
salePriceUSD: 26,
stock: 17,
featured: true,
tags: ['industrial', 'angular-contact', 'skf', 'bep'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'تماس زاویه‌ای تک ردیفه', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'زاویه تماس 40 درجه با قفسه پلی‌آمیدی', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'تحمل همزمان بار شعاعی و محوری یک‌طرفه', displayOrder: 2 },
],
},
{
brandId: brandIds.fag,
brandName: 'FAG',
categoryId: categoryIds.angularContact,
categoryName: categoryNames[categoryIds.angularContact],
model: '7308 B.TVP',
title: 'بلبرینگ FAG مدل 7308 B.TVP',
description:
'طراحی شده برای کارکردهای دقیق و سرعت بالا. دارای قفسه پلاستیکی تقویت شده با الیاف شیشه، پرکاربرد در ماشین‌ابزارها.',
basePriceUSD: 34,
salePriceUSD: 31,
stock: 13,
tags: ['industrial', 'angular-contact', 'fag', 'tvp'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'تماس زاویه‌ای دقیق برای سرعت بالا', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'قفسه پلاستیکی تقویت‌شده با الیاف شیشه', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'ماشین‌ابزارها و تجهیزات دقیق', displayOrder: 2 },
],
},
{
brandId: brandIds.nsk,
brandName: 'NSK',
categoryId: categoryIds.angularContact,
categoryName: categoryNames[categoryIds.angularContact],
model: '7210 BW',
title: 'بلبرینگ NSK مدل 7210 BW',
description:
'زاویه تماس استاندارد، مجهز به قفسه فولادی پرسکاری شده. دوام عالی در پمپ‌های گریز از مرکز.',
basePriceUSD: 32,
salePriceUSD: 29,
stock: 15,
tags: ['industrial', 'angular-contact', 'nsk', 'bw'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'تماس زاویه‌ای با زاویه استاندارد', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'قفسه فولادی پرسکاری شده', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'پمپ‌های گریز از مرکز', displayOrder: 2 },
],
},
{
brandId: brandIds.skf,
brandName: 'SKF',
categoryId: categoryIds.angularContact,
categoryName: categoryNames[categoryIds.angularContact],
model: '3206 A-2RS1',
title: 'بلبرینگ SKF مدل 3206 A-2RS1',
description:
'بلبرینگ تماس زاویه‌ای دو ردیفه مجهز به آب‌بند لاستیکی. عملکردی مشابه دو بلبرینگ تک ردیفه پشت به پشت با اشغال فضای کمتر.',
basePriceUSD: 38,
salePriceUSD: 35,
stock: 11,
tags: ['industrial', 'angular-contact', 'skf', '2rs1'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'تماس زاویه‌ای دو ردیفه', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'آب‌بند لاستیکی 2RS1 با فضای نصب کمتر', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'جایگزین فشرده برای آرایش پشت به پشت', displayOrder: 2 },
],
},
{
brandId: brandIds.fag,
brandName: 'FAG',
categoryId: categoryIds.angularContact,
categoryName: categoryNames[categoryIds.angularContact],
model: '7312 B.MP',
title: 'بلبرینگ FAG مدل 7312 B.MP',
description:
'دارای قفسه برنجی ماشین‌کاری شده (MP) برای مقاومت حداکثری در برابر بارهای شوک‌دار و ارتعاشات صنعتی.',
basePriceUSD: 42,
salePriceUSD: 39,
stock: 9,
tags: ['industrial', 'angular-contact', 'fag', 'mp'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'تماس زاویه‌ای تک ردیفه صنعتی', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'قفسه برنجی ماشین‌کاری‌شده MP', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'بارهای شوک‌دار و ارتعاشات صنعتی', displayOrder: 2 },
],
},
{
brandId: brandIds.skf,
brandName: 'SKF',
categoryId: categoryIds.selfAligning,
categoryName: categoryNames[categoryIds.selfAligning],
model: '1205 EKTN9',
title: 'بلبرینگ SKF مدل 1205 EKTN9',
description:
'سوراخ مخروطی (Tapered Bore) با قفسه پلی‌آمیدی. توانایی جبران انحرافات شفت تا ۲.۵ درجه، ایده‌آل برای شفت‌های بلند.',
basePriceUSD: 23,
salePriceUSD: 20,
stock: 21,
tags: ['industrial', 'self-aligning', 'skf', 'ektn9'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'خودتنظیم با سوراخ مخروطی', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'جبران انحراف شفت تا 2.5 درجه', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'شفت‌های بلند و ناهمراستا', displayOrder: 2 },
],
},
{
brandId: brandIds.fag,
brandName: 'FAG',
categoryId: categoryIds.selfAligning,
categoryName: categoryNames[categoryIds.selfAligning],
model: '1308 TVH',
title: 'بلبرینگ FAG مدل 1308 TVH',
description:
'بلبرینگ خودتنظیم با طراحی داخلی بهینه‌شده. مناسب برای تجهیزات نساجی و ماشین‌آلات کاغذسازی با انحرافات زاویه‌ای مکرر.',
basePriceUSD: 27,
salePriceUSD: 24,
stock: 16,
tags: ['industrial', 'self-aligning', 'fag', 'tvh'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'خودتنظیم با طراحی داخلی بهینه', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'عملکرد پایدار در انحرافات زاویه‌ای مکرر', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'تجهیزات نساجی و کاغذسازی', displayOrder: 2 },
],
},
{
brandId: brandIds.nsk,
brandName: 'NSK',
categoryId: categoryIds.selfAligning,
categoryName: categoryNames[categoryIds.selfAligning],
model: '2210 K',
title: 'بلبرینگ NSK مدل 2210 K',
description:
'دارای سوراخ مخروطی، نصب آسان با استفاده از غلاف کلاچ (Adapter Sleeve). اصطکاک بسیار پایین و کارکرد خنک.',
basePriceUSD: 31,
salePriceUSD: 28,
stock: 14,
tags: ['industrial', 'self-aligning', 'nsk', 'k'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'خودتنظیم با سوراخ مخروطی K', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'نصب آسان با Adapter Sleeve', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'سیستم‌های نیازمند اصطکاک پایین و کارکرد خنک', displayOrder: 2 },
],
},
{
brandId: brandIds.skf,
brandName: 'SKF',
categoryId: categoryIds.selfAligning,
categoryName: categoryNames[categoryIds.selfAligning],
model: '2306-2RS1TN9',
title: 'بلبرینگ SKF مدل 2306-2RS1TN9',
description:
'نسخه آب‌بندی شده بلبرینگ خودتنظیم. بدون نیاز به روانکاری مجدد (Maintenance-free)، مناسب برای محیط‌های کثیف.',
basePriceUSD: 35,
salePriceUSD: 32,
stock: 12,
tags: ['industrial', 'self-aligning', 'skf', '2rs1tn9'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'خودتنظیم آب‌بندی‌شده', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'بدون نیاز به روانکاری مجدد', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'محیط‌های کثیف و پرگردوغبار', displayOrder: 2 },
],
},
{
brandId: brandIds.fag,
brandName: 'FAG',
categoryId: categoryIds.selfAligning,
categoryName: categoryNames[categoryIds.selfAligning],
model: '1212',
title: 'بلبرینگ FAG مدل 1212',
description:
'مدل استاندارد سوراخ استوانه‌ای با قفسه فولادی. تحمل بارهای شعاعی ملایم و سرعت‌های دورانی بالا.',
basePriceUSD: 22,
salePriceUSD: 19,
stock: 22,
tags: ['industrial', 'self-aligning', 'fag', 'standard'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'خودتنظیم با سوراخ استوانه‌ای', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'قفسه فولادی استاندارد', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'بارهای شعاعی ملایم با سرعت دورانی بالا', displayOrder: 2 },
],
},
{
brandId: brandIds.timken,
brandName: 'TIMKEN',
categoryId: categoryIds.tapered,
categoryName: categoryNames[categoryIds.tapered],
model: '32210',
title: 'رولبرینگ TIMKEN مدل 32210',
description:
'طراحی اختصاصی تیمکن برای تحمل بارهای ترکیبی (شعاعی و محوری) سنگین. کاربرد وسیع در توپی چرخ خودروهای سنگین و گیربکس‌ها.',
basePriceUSD: 48,
salePriceUSD: 44,
stock: 19,
featured: true,
tags: ['industrial', 'tapered-roller', 'timken', 'heavy-duty'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ مخروطی برای بار ترکیبی', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'تحمل بارهای شعاعی و محوری سنگین', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'توپی چرخ خودروهای سنگین و گیربکس‌ها', displayOrder: 2 },
],
},
{
brandId: brandIds.skf,
brandName: 'SKF',
categoryId: categoryIds.tapered,
categoryName: categoryNames[categoryIds.tapered],
model: '30205 J2/Q',
title: 'رولبرینگ SKF مدل 30205 J2/Q',
description:
'کیفیت سطح هندسی فوق‌العاده بالا برای توزیع یکنواخت بار. دارای قفسه فولادی استاندارد، مناسب برای پینیون دیفرانسیل.',
basePriceUSD: 37,
salePriceUSD: 34,
stock: 20,
tags: ['industrial', 'tapered-roller', 'skf', 'j2-q'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ مخروطی متریک', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'توزیع یکنواخت بار با کیفیت سطح هندسی بالا', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'پینیون دیفرانسیل و انتقال قدرت', displayOrder: 2 },
],
},
{
brandId: brandIds.fag,
brandName: 'FAG',
categoryId: categoryIds.tapered,
categoryName: categoryNames[categoryIds.tapered],
model: '32309-A',
title: 'رولبرینگ FAG مدل 32309-A',
description:
'زاویه تماس بزرگتر برای تحمل بارهای محوری شدیدتر. استفاده گسترده در گیربکس‌های صنعتی و تجهیزات معدنی.',
basePriceUSD: 41,
salePriceUSD: 38,
stock: 17,
tags: ['industrial', 'tapered-roller', 'fag', 'axial-load'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ مخروطی با زاویه تماس بزرگ', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'تحمل بالاتر بارهای محوری', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'گیربکس‌های صنعتی و تجهیزات معدنی', displayOrder: 2 },
],
},
{
brandId: brandIds.timken,
brandName: 'TIMKEN',
categoryId: categoryIds.tapered,
categoryName: categoryNames[categoryIds.tapered],
model: '30306 M',
title: 'رولبرینگ TIMKEN مدل 30306 M',
description:
'طراحی متریک استاندارد با مقاومت سایش بالا. ایده‌آل برای محورهای دوار در ماشین‌آلات راه‌سازی.',
basePriceUSD: 39,
salePriceUSD: 36,
stock: 16,
tags: ['industrial', 'tapered-roller', 'timken', 'metric'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ مخروطی متریک استاندارد', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'مقاومت سایش بالا', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'محورهای دوار در ماشین‌آلات راه‌سازی', displayOrder: 2 },
],
},
{
brandId: brandIds.nsk,
brandName: 'NSK',
categoryId: categoryIds.tapered,
categoryName: categoryNames[categoryIds.tapered],
model: '32015 X',
title: 'رولبرینگ NSK مدل 32015 X',
description:
'طراحی فشرده با مقطع باریک. توانایی تحمل بارهای سنگین در فضاهای محدود سیستم‌های انتقال قدرت.',
basePriceUSD: 46,
salePriceUSD: 42,
stock: 12,
tags: ['industrial', 'tapered-roller', 'nsk', 'compact'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ مخروطی فشرده', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'تحمل بار سنگین در فضای محدود', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'سیستم‌های انتقال قدرت با فضای نصب محدود', displayOrder: 2 },
],
},
{
brandId: brandIds.skf,
brandName: 'SKF',
categoryId: categoryIds.spherical,
categoryName: categoryNames[categoryIds.spherical],
model: '22215 EK',
title: 'رولبرینگ SKF مدل 22215 EK',
description:
'سوراخ مخروطی، تحمل بارهای فوق‌سنگین شعاعی و جبران ناهمراستایی شفت. قلب تپنده سنگ‌شکن‌ها و آسیاب‌های صنعتی.',
basePriceUSD: 67,
salePriceUSD: 61,
stock: 10,
featured: true,
tags: ['industrial', 'spherical-roller', 'skf', 'heavy-duty'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ بشکه‌ای با سوراخ مخروطی', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'تحمل بار فوق‌سنگین و جبران ناهمراستایی', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'سنگ‌شکن‌ها و آسیاب‌های صنعتی', displayOrder: 2 },
],
},
{
brandId: brandIds.fag,
brandName: 'FAG',
categoryId: categoryIds.spherical,
categoryName: categoryNames[categoryIds.spherical],
model: '22318 E1.CC',
title: 'رولبرینگ FAG مدل 22318 E1.CC',
description:
'طراحی سری E1 با ظرفیت تحمل بار ارتقا یافته. دارای قفسه فولادی دوتکه، مناسب برای سرندهای ارتعاشی و نوار نقاله‌ها.',
basePriceUSD: 72,
salePriceUSD: 66,
stock: 8,
tags: ['industrial', 'spherical-roller', 'fag', 'e1-cc'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ بشکه‌ای سری E1', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'قفسه فولادی دوتکه با ظرفیت بار ارتقایافته', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'سرندهای ارتعاشی و نوار نقاله‌ها', displayOrder: 2 },
],
},
{
brandId: brandIds.timken,
brandName: 'TIMKEN',
categoryId: categoryIds.spherical,
categoryName: categoryNames[categoryIds.spherical],
model: '22220 EJ W33',
title: 'رولبرینگ TIMKEN مدل 22220 EJ W33',
description:
'مجهز به شیار و سه سوراخ روانکاری (W33) روی حلقه خارجی. طول عمر بالا در شرایط کاری خشن صنایع فولاد.',
basePriceUSD: 78,
salePriceUSD: 71,
stock: 7,
tags: ['industrial', 'spherical-roller', 'timken', 'w33'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ بشکه‌ای صنعتی سنگین', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'شیار و سه سوراخ روانکاری W33', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'شرایط کاری خشن صنایع فولاد', displayOrder: 2 },
],
},
{
brandId: brandIds.skf,
brandName: 'SKF',
categoryId: categoryIds.spherical,
categoryName: categoryNames[categoryIds.spherical],
model: '22312 E',
title: 'رولبرینگ SKF مدل 22312 E',
description:
'سوراخ استوانه‌ای با طراحی داخلی بهینه‌شده برای کاهش دمای کاری. قابل اطمینان در توربین‌های بادی و صنایع دریایی.',
basePriceUSD: 63,
salePriceUSD: 58,
stock: 9,
tags: ['industrial', 'spherical-roller', 'skf', 'low-temperature'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ بشکه‌ای با سوراخ استوانه‌ای', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'طراحی داخلی بهینه برای کاهش دمای کاری', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'توربین‌های بادی و صنایع دریایی', displayOrder: 2 },
],
},
{
brandId: brandIds.nsk,
brandName: 'NSK',
categoryId: categoryIds.spherical,
categoryName: categoryNames[categoryIds.spherical],
model: '22211 EA',
title: 'رولبرینگ NSK مدل 22211 EA',
description:
'قفسه فولادی پرسکاری شده مقاوم. عملکرد عالی در سرعت‌های بالا با حداقل لرزش و صدای تولیدی.',
basePriceUSD: 59,
salePriceUSD: 54,
stock: 11,
tags: ['industrial', 'spherical-roller', 'nsk', 'ea'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ بشکه‌ای برای سرعت بالا', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'قفسه فولادی پرسکاری‌شده مقاوم', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'سیستم‌های سرعت بالا با لرزش و صدای کم', displayOrder: 2 },
],
},
{
brandId: brandIds.skf,
brandName: 'SKF',
categoryId: categoryIds.thrust,
categoryName: categoryNames[categoryIds.thrust],
model: '51105',
title: 'بلبرینگ SKF مدل 51105',
description:
'بلبرینگ کف‌گرد تک‌جهته، تحمل منحصراً بارهای محوری. طراحی شده با واشر شفت و واشر محفظه با دقت بالا.',
basePriceUSD: 17,
salePriceUSD: 15,
stock: 26,
tags: ['industrial', 'thrust-bearing', 'skf', 'single-direction'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'کف‌گرد تک‌جهته', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'تحمل انحصاری بار محوری', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'موقعیت‌های نیازمند دقت بالای واشر شفت و محفظه', displayOrder: 2 },
],
},
{
brandId: brandIds.fag,
brandName: 'FAG',
categoryId: categoryIds.thrust,
categoryName: categoryNames[categoryIds.thrust],
model: '51208',
title: 'بلبرینگ FAG مدل 51208',
description:
'مناسب برای بارهای محوری متوسط تا سنگین. قطعات کاملاً قابل تفکیک برای نصب و نگهداری آسان در جک‌های صنعتی.',
basePriceUSD: 22,
salePriceUSD: 20,
stock: 18,
tags: ['industrial', 'thrust-bearing', 'fag', 'serviceable'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'کف‌گرد تک‌جهته قابل تفکیک', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'نصب و نگهداری آسان', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'جک‌های صنعتی و بارهای محوری متوسط تا سنگین', displayOrder: 2 },
],
},
{
brandId: brandIds.nsk,
brandName: 'NSK',
categoryId: categoryIds.thrust,
categoryName: categoryNames[categoryIds.thrust],
model: '51310',
title: 'بلبرینگ NSK مدل 51310',
description:
'مقطع ضخیم‌تر برای کاربردهای سنگین‌تر مانند دستگاه‌های پرس و اکسترودرها. دارای قفسه فولادی ماشین‌کاری شده.',
basePriceUSD: 28,
salePriceUSD: 25,
stock: 14,
tags: ['industrial', 'thrust-bearing', 'nsk', 'heavy-section'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'کف‌گرد با مقطع ضخیم‌تر', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'قفسه فولادی ماشین‌کاری‌شده', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'دستگاه‌های پرس و اکسترودرها', displayOrder: 2 },
],
},
{
brandId: brandIds.skf,
brandName: 'SKF',
categoryId: categoryIds.thrust,
categoryName: categoryNames[categoryIds.thrust],
model: '51112',
title: 'بلبرینگ SKF مدل 51112',
description:
'ایده‌آل برای موقعیت‌دهی محوری شفت‌ها. کیفیت ساخت بالا برای جلوگیری از لغزش ساچمه‌ها در سرعت‌های دورانی متغیر.',
basePriceUSD: 24,
salePriceUSD: 22,
stock: 16,
tags: ['industrial', 'thrust-bearing', 'skf', 'shaft-positioning'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'کف‌گرد تک‌جهته برای موقعیت‌دهی محوری', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'جلوگیری از لغزش ساچمه‌ها در سرعت متغیر', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'موقعیت‌دهی محوری شفت‌ها', displayOrder: 2 },
],
},
{
brandId: brandIds.fag,
brandName: 'FAG',
categoryId: categoryIds.thrust,
categoryName: categoryNames[categoryIds.thrust],
model: '52205',
title: 'بلبرینگ FAG مدل 52205',
description:
'بلبرینگ کف‌گرد دوجهته. قابلیت تحمل بارهای محوری در هر دو جهت و تثبیت کامل موقعیت شفت، مناسب برای ماشین‌های فرز و تراش.',
basePriceUSD: 31,
salePriceUSD: 28,
stock: 12,
tags: ['industrial', 'thrust-bearing', 'fag', 'double-direction'],
attributes: [
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'کف‌گرد دوجهته', displayOrder: 0 },
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'تحمل بار محوری در هر دو جهت', displayOrder: 1 },
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'ماشین‌های فرز و تراش', displayOrder: 2 },
],
},
];
function slugify(value: string) {
return value
.toLowerCase()
.replace(/[./]/g, '-')
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
function buildPayload(input: SeedProductInput): CreateProductDto {
const slug = slugify(`${input.brandName}-${input.model}`);
const summary = input.description.length > 160
? `${input.description.slice(0, 157)}...`
: input.description;
return {
sku: `${input.brandName}-${input.model}`.replace(/\s+/g, '-'),
title: input.title,
slug,
technicalCode: input.model,
brandId: input.brandId,
basePriceUSD: input.basePriceUSD,
salePriceUSD: input.salePriceUSD,
stock: input.stock,
type: ProductType.INDUSTRIAL,
status: ProductStatus.PUBLISHED,
featured: input.featured ?? false,
primaryCategoryId: input.categoryId,
categoryIds: [input.categoryId],
tags: input.tags,
meta: {
shortDescription: summary,
description: [
input.description,
`برند: ${input.brandName}`,
`دسته‌بندی: ${input.categoryName}`,
`کد فنی: ${input.model}`,
].join('\n'),
metaTitle: input.title,
metaDescription: summary,
shareTitle: input.title,
shareDescription: summary,
},
attributes: input.attributes.map((attribute) => ({
name: attribute.name,
slug: attribute.slug,
dataType: AttributeDataType.TEXT,
valueText: attribute.valueText,
displayOrder: attribute.displayOrder,
isVisible: true,
isFilterable: attribute.displayOrder < 2,
})),
};
}
async function main() {
const app = await NestFactory.createApplicationContext(AppModule, {
logger: ['error', 'warn', 'log'],
});
try {
const productRepository = app.get<Repository<Product>>(getRepositoryToken(Product));
const brandRepository = app.get<Repository<Brand>>(getRepositoryToken(Brand));
const categoryRepository = app.get<Repository<Category>>(getRepositoryToken(Category));
const productMetaRepository = app.get<Repository<ProductMeta>>(getRepositoryToken(ProductMeta));
const productAttributeValueRepository = app.get<Repository<ProductAttributeValue>>(
getRepositoryToken(ProductAttributeValue),
);
const attributeDefinitionRepository = app.get<Repository<AttributeDefinition>>(
getRepositoryToken(AttributeDefinition),
);
const brands = await brandRepository.findBy(
Object.values(brandIds).map((id) => ({ id })),
);
const categories = await categoryRepository.findBy(
Object.values(categoryIds).map((id) => ({ id })),
);
const brandById = new Map(brands.map((item) => [item.id, item]));
const categoryById = new Map(categories.map((item) => [item.id, item]));
const requiredAttributeDefinitions = [
{
slug: 'design-type',
name: 'نوع طراحی',
isFilterable: true,
},
{
slug: 'key-feature',
name: 'ویژگی شاخص',
isFilterable: true,
},
{
slug: 'recommended-application',
name: 'کاربرد پیشنهادی',
isFilterable: false,
},
] as const;
const attributeDefinitionBySlug = new Map<string, AttributeDefinition>();
for (const definition of requiredAttributeDefinitions) {
let attributeDefinition = await attributeDefinitionRepository.findOne({
where: { slug: definition.slug },
});
if (!attributeDefinition) {
attributeDefinition = await attributeDefinitionRepository.save(
attributeDefinitionRepository.create({
name: definition.name,
slug: definition.slug,
dataType: AttributeDataType.TEXT,
unit: null,
options: [],
isFilterable: definition.isFilterable,
isVisible: true,
}),
);
}
attributeDefinitionBySlug.set(definition.slug, attributeDefinition);
}
console.log(`Seeding ${seedProducts.length} industrial products...`);
for (const item of seedProducts) {
const payload = buildPayload(item);
const brand = brandById.get(item.brandId);
const category = categoryById.get(item.categoryId);
if (!brand) {
throw new Error(`Brand not found for seed input: ${item.brandName} (${item.brandId})`);
}
if (!category) {
throw new Error(
`Category not found for seed input: ${item.categoryName} (${item.categoryId})`,
);
}
const existing = await productRepository
.createQueryBuilder('product')
.select('product.id', 'id')
.where('product.slug = :slug', { slug: payload.slug })
.getRawOne<{ id: string }>();
const product = await productRepository.save(
productRepository.create({
id: existing?.id,
sku: payload.sku,
title: payload.title,
slug: payload.slug,
technicalCode: payload.technicalCode,
brand: item.brandName,
brandEntity: brand,
basePriceUSD: payload.basePriceUSD,
salePriceUSD: payload.salePriceUSD ?? null,
stock: payload.stock,
featured: payload.featured ?? false,
type: ProductType.INDUSTRIAL,
status: ProductStatus.PUBLISHED,
primaryCategory: category,
categories: [category],
tags: payload.tags ?? [],
mainImageUrl: null,
threeDModelUrl: null,
imageGalleryUrls: [],
averageRating: existing?.id ? undefined : 0,
reviewsCount: existing?.id ? undefined : 0,
}),
);
const existingMeta = await productMetaRepository
.createQueryBuilder('meta')
.select('meta.id', 'id')
.where('meta.product_id = :productId', { productId: product.id })
.getRawOne<{ id: string }>();
await productMetaRepository.save(
productMetaRepository.create({
id: existingMeta?.id,
product,
shortDescription: payload.meta?.shortDescription ?? null,
description: payload.meta?.description ?? null,
metaTitle: payload.meta?.metaTitle ?? null,
metaDescription: payload.meta?.metaDescription ?? null,
shareTitle: payload.meta?.shareTitle ?? null,
shareDescription: payload.meta?.shareDescription ?? null,
shareImageUrl: payload.meta?.shareImageUrl ?? null,
}),
);
await productAttributeValueRepository
.createQueryBuilder()
.delete()
.from(ProductAttributeValue)
.where('productId = :productId', { productId: product.id })
.execute();
const attributeValues = (payload.attributes ?? []).map((attribute) => {
const definition = attributeDefinitionBySlug.get(attribute.slug ?? '');
if (!definition) {
throw new Error(`Attribute definition not found for slug: ${attribute.slug}`);
}
return productAttributeValueRepository.create({
product,
attribute: definition,
displayOrder: attribute.displayOrder ?? 0,
valueText: attribute.valueText ?? null,
valueNumber: attribute.valueNumber ?? null,
valueBoolean: attribute.valueBoolean ?? null,
valueJson: attribute.valueJson ?? null,
overrideUnit: attribute.overrideUnit ?? null,
});
});
if (attributeValues.length > 0) {
await productAttributeValueRepository.save(attributeValues);
}
console.log(`${existing?.id ? 'Updated' : 'Created'}: ${payload.title}`);
}
console.log('Industrial products seed completed successfully.');
} finally {
await app.close();
}
}
void main().catch((error: unknown) => {
console.error('Industrial products seed failed.');
console.error(error);
process.exit(1);
});