feat: add product detail enrichment for Magnit products

- Add isDetailsFetched field to Product model
- Add fetchProductDetails() and fetchProductObjectInfo() methods to MagnitApiScraper
- Add ProductParser methods for detail parsing
- Add ProductService methods: getProductsNeedingDetails(), updateProductDetails(), markAsDetailsFetched()
- Add enrich-product-details.ts script with statistics tracking
- Update package.json with "enrich" script command
- Add E2E_GUIDE.md documentation
- Exclude debug scripts from tsconfig type-check (temporary)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-22 01:52:50 +05:00
parent 5a763a4e13
commit 3299cca574
11 changed files with 795 additions and 88 deletions

View File

@@ -1,4 +1,4 @@
import { ProductItem } from '../../scrapers/api/magnit/types.js';
import { ProductItem, ProductDetailsResponse, ObjectInfo } from '../../scrapers/api/magnit/types.js';
import { CreateProductData } from '../product/ProductService.js';
export class ProductParser {
@@ -80,5 +80,113 @@ export class ProductParser {
return this.parseProductItem(item, storeId, categoryId);
});
}
/**
* Парсинг деталей товара из ProductDetailsResponse
* Извлекает бренд, описание, вес, единицу измерения из массива details
*/
static parseProductDetails(detailsResponse: ProductDetailsResponse): {
brand?: string;
description?: string;
weight?: string;
unit?: string;
categoryId?: number;
} {
const result: {
brand?: string;
description?: string;
weight?: string;
unit?: string;
categoryId?: number;
} = {};
// Получаем первую категорию из массива
if (detailsResponse.categories && detailsResponse.categories.length > 0) {
result.categoryId = detailsResponse.categories[0];
}
// Парсим массив деталей для извлечения полей
for (const detail of detailsResponse.details) {
const name = detail.name;
const nameLower = detail.name.toLowerCase();
// Бренд - проверяем и русское и английское название
if (name === 'Бренд' || nameLower === 'brand') {
// Игнорируем "Различные бренды"
if (detail.value && !detail.value.includes('Различные бренды')) {
result.brand = detail.value;
}
}
// Описание товара
else if (name === 'Описание товара' || nameLower.includes('описание')) {
if (detail.value) {
result.description = detail.value;
}
}
// Вес - может быть на русском
else if (nameLower.includes('вес') || nameLower === 'weight') {
if (detail.value) {
result.weight = detail.value;
}
}
// Единица измерения - может быть на русском
else if (name === 'Единица измерения' || nameLower.includes('единица') || nameLower === 'unit') {
if (detail.value) {
result.unit = detail.value;
}
}
}
return result;
}
/**
* Парсинг ObjectInfo для получения рейтингов и описания
*/
static parseObjectInfo(objectInfo: ObjectInfo): {
description?: string;
rating?: number;
scoresCount?: number;
commentsCount?: number;
imageUrl?: string;
} {
const result: {
description?: string;
rating?: number;
scoresCount?: number;
commentsCount?: number;
imageUrl?: string;
} = {};
// Описание из object_info (если есть)
if (objectInfo.description) {
result.description = objectInfo.description;
}
// Рейтинг
if (objectInfo.average_rating !== undefined) {
result.rating = objectInfo.average_rating;
}
// Количество оценок
if (objectInfo.count_ratings !== undefined) {
result.scoresCount = objectInfo.count_ratings;
}
// Количество отзывов
if (objectInfo.count_reviews !== undefined) {
result.commentsCount = objectInfo.count_reviews;
}
// URL изображения (если есть)
if (objectInfo.url) {
result.imageUrl = objectInfo.url;
}
return result;
}
}