add api category brand and fix product
This commit is contained in:
@@ -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`
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
8
src/modules/catalog/enums/product-list-sort.enum.ts
Normal file
8
src/modules/catalog/enums/product-list-sort.enum.ts
Normal 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',
|
||||
}
|
||||
24
src/modules/catalog/product-details.controller.ts
Normal file
24
src/modules/catalog/product-details.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
883
src/scripts/seed-industrial-products.ts
Normal file
883
src/scripts/seed-industrial-products.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user