Add initial Prisma migration and catalog-based product seed

This commit is contained in:
Ruslan Bakiev
2026-03-31 12:29:21 +07:00
parent 9e379985ad
commit 705d76c597
3 changed files with 385 additions and 18 deletions

View File

@@ -2,9 +2,98 @@ import 'dotenv/config';
import { prisma } from '../src/prisma-client.js';
const CATALOG_PAGES = ['https://fregat.org/catalog/', 'https://fregat.org/catalog/page/2/'];
const managerEmail = 'manager@fregat.local';
const clientEmail = 'client@fregat.local';
function decodeHtmlEntities(value) {
return value
.replaceAll(' ', ' ')
.replaceAll('&', '&')
.replaceAll('"', '"')
.replaceAll(''', "'")
.replaceAll('«', '«')
.replaceAll('»', '»')
.replaceAll('—', '—')
.replaceAll('–', '-');
}
function normalizeText(value) {
return decodeHtmlEntities(value.replaceAll(/<[^>]*>/g, ' ').replaceAll(/\s+/g, ' ')).trim();
}
function skuFromHref(href, index) {
const pathname = href.replace(/^https?:\/\/[^/]+/i, '').split('?')[0] || '';
const segments = pathname.split('/').filter(Boolean);
const slug = segments[segments.length - 1] ?? '';
if (!slug) {
return `FREGAT-CATALOG-${String(index + 1).padStart(3, '0')}`;
}
const normalized = slug.toUpperCase().replaceAll(/[^A-Z0-9]+/g, '-').replace(/^-+|-+$/g, '');
return `FREGAT-${normalized || String(index + 1).padStart(3, '0')}`;
}
function parseCatalogCards(html) {
const cards = [];
const cardRegex =
/<a[^>]*class="[^"]*product-card[^"]*"[^>]*href="([^"]+)"[^>]*>[\s\S]*?<div class="product-card__title">([\s\S]*?)<\/div>\s*<div class="product-card__type">([\s\S]*?)<\/div>/g;
let match;
while ((match = cardRegex.exec(html)) !== null) {
const href = match[1];
const title = normalizeText(match[2]);
const type = normalizeText(match[3]);
if (!title) {
continue;
}
cards.push({ href, title, type });
}
return cards;
}
async function loadCatalogProducts() {
const parsed = [];
for (const pageUrl of CATALOG_PAGES) {
const response = await fetch(pageUrl, { redirect: 'follow' });
if (!response.ok) {
throw new Error(`Catalog request failed: ${pageUrl} (${response.status})`);
}
const html = await response.text();
const cards = parseCatalogCards(html);
if (!cards.length) {
throw new Error(`No product cards found at ${pageUrl}`);
}
parsed.push(...cards);
}
const deduped = new Map();
for (const [index, card] of parsed.entries()) {
const sku = skuFromHref(card.href, index);
if (deduped.has(sku)) {
continue;
}
const descriptionParts = [];
if (card.type) {
descriptionParts.push(card.type);
}
descriptionParts.push(`Источник: ${card.href}`);
deduped.set(sku, {
sku,
name: card.title,
description: descriptionParts.join('. '),
isCustomizable: /логотип|печать/i.test(`${card.title} ${card.type}`),
qtyMain: 300 + index * 17,
qtyReserve: 120 + index * 9,
});
}
return [...deduped.values()];
}
const company = await prisma.company.upsert({
where: { inn: '7701000000' },
update: {},
@@ -47,24 +136,8 @@ const warehouseReserve = await prisma.warehouse.upsert({
create: { code: 'SPB-01', name: 'Reserve warehouse' },
});
const products = [
{
sku: 'FILM-100',
name: 'Пленка прозрачная 100мкм',
description: 'Готовая продукция для стандартной упаковки',
isCustomizable: false,
qtyMain: 1250,
qtyReserve: 420,
},
{
sku: 'FILM-200-COLOR',
name: 'Пленка цветная 200мкм',
description: 'Продукция с дополнительной окраской',
isCustomizable: true,
qtyMain: 860,
qtyReserve: 230,
},
];
const products = await loadCatalogProducts();
console.log(`Loaded ${products.length} products from fregat.org catalog.`);
for (const product of products) {
const dbProduct = await prisma.product.upsert({