diff --git a/prisma/migrations/0004_add_product_spec_fields/migration.sql b/prisma/migrations/0004_add_product_spec_fields/migration.sql new file mode 100644 index 0000000..238c281 --- /dev/null +++ b/prisma/migrations/0004_add_product_spec_fields/migration.sql @@ -0,0 +1,8 @@ +-- AlterTable +ALTER TABLE "Product" ADD COLUMN "lengthM" INTEGER, +ADD COLUMN "productType" TEXT, +ADD COLUMN "quantityPerBox" TEXT, +ADD COLUMN "sleeveBrand" TEXT, +ADD COLUMN "thicknessMicron" INTEGER, +ADD COLUMN "widthMm" INTEGER; + diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 09e9f30..a012542 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -165,6 +165,12 @@ model Product { id String @id @default(cuid()) sku String @unique name String + productType String? + widthMm Int? + lengthM Int? + thicknessMicron Int? + sleeveBrand String? + quantityPerBox String? description String? isCustomizable Boolean @default(false) isActive Boolean @default(true) diff --git a/scripts/seed.js b/scripts/seed.js index bd23a86..0ca56f6 100644 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -2,100 +2,62 @@ 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('–', '-'); +const PRODUCT_TYPES = { + STANDARD: 'стандартная лента', + PAINTING: 'малярный скотч', +}; + +const PRODUCT_ROWS = [ + { sku: '480200', productType: PRODUCT_TYPES.STANDARD, widthMm: 48, lengthM: 40, thicknessMicron: 38, sleeveBrand: 'фрегат', quantityPerBox: '72' }, + { sku: '481200', productType: PRODUCT_TYPES.STANDARD, widthMm: 48, lengthM: 55, thicknessMicron: 38, sleeveBrand: 'фрегат', quantityPerBox: '72/36' }, + { sku: '482200', productType: PRODUCT_TYPES.STANDARD, widthMm: 48, lengthM: 66, thicknessMicron: 38, sleeveBrand: 'фрегат', quantityPerBox: '36' }, + { sku: '483200', productType: PRODUCT_TYPES.STANDARD, widthMm: 48, lengthM: 120, thicknessMicron: 38, sleeveBrand: 'фрегат', quantityPerBox: '36' }, + { sku: '751200', productType: PRODUCT_TYPES.STANDARD, widthMm: 75, lengthM: 55, thicknessMicron: 38, sleeveBrand: 'фрегат', quantityPerBox: '48' }, + { sku: '752200', productType: PRODUCT_TYPES.STANDARD, widthMm: 75, lengthM: 66, thicknessMicron: 38, sleeveBrand: 'фрегат', quantityPerBox: '24' }, + { sku: '753200', productType: PRODUCT_TYPES.STANDARD, widthMm: 75, lengthM: 120, thicknessMicron: 38, sleeveBrand: 'фрегат', quantityPerBox: '24' }, + { sku: '481400', productType: PRODUCT_TYPES.STANDARD, widthMm: 48, lengthM: 55, thicknessMicron: 43, sleeveBrand: 'фрегат', quantityPerBox: '72/36' }, + { sku: '482400', productType: PRODUCT_TYPES.STANDARD, widthMm: 48, lengthM: 66, thicknessMicron: 43, sleeveBrand: 'фрегат', quantityPerBox: '36' }, + { sku: '483400', productType: PRODUCT_TYPES.STANDARD, widthMm: 48, lengthM: 120, thicknessMicron: 43, sleeveBrand: 'фрегат', quantityPerBox: '36' }, + { sku: '751400', productType: PRODUCT_TYPES.STANDARD, widthMm: 75, lengthM: 55, thicknessMicron: 43, sleeveBrand: 'фрегат', quantityPerBox: '48' }, + { sku: '752400', productType: PRODUCT_TYPES.STANDARD, widthMm: 75, lengthM: 66, thicknessMicron: 43, sleeveBrand: 'фрегат', quantityPerBox: '24' }, + { sku: '753400', productType: PRODUCT_TYPES.STANDARD, widthMm: 75, lengthM: 120, thicknessMicron: 43, sleeveBrand: 'фрегат', quantityPerBox: '24' }, + { sku: '481500', productType: PRODUCT_TYPES.STANDARD, widthMm: 48, lengthM: 55, thicknessMicron: 45, sleeveBrand: 'фрегат', quantityPerBox: '72/36' }, + { sku: '482500', productType: PRODUCT_TYPES.STANDARD, widthMm: 48, lengthM: 66, thicknessMicron: 45, sleeveBrand: 'фрегат', quantityPerBox: '36' }, + { sku: '483500', productType: PRODUCT_TYPES.STANDARD, widthMm: 48, lengthM: 120, thicknessMicron: 45, sleeveBrand: 'фрегат', quantityPerBox: '36' }, + { sku: '487500', productType: PRODUCT_TYPES.STANDARD, widthMm: 48, lengthM: 150, thicknessMicron: 45, sleeveBrand: 'фрегат', quantityPerBox: '36' }, + { sku: '751500', productType: PRODUCT_TYPES.STANDARD, widthMm: 75, lengthM: 55, thicknessMicron: 45, sleeveBrand: 'фрегат', quantityPerBox: '48' }, + { sku: '752500', productType: PRODUCT_TYPES.STANDARD, widthMm: 75, lengthM: 66, thicknessMicron: 45, sleeveBrand: 'фрегат', quantityPerBox: '24' }, + { sku: '743500', productType: PRODUCT_TYPES.STANDARD, widthMm: 75, lengthM: 120, thicknessMicron: 45, sleeveBrand: 'фрегат', quantityPerBox: '24' }, + { sku: '482600', productType: PRODUCT_TYPES.STANDARD, widthMm: 48, lengthM: 66, thicknessMicron: 47, sleeveBrand: 'фрегат', quantityPerBox: '36' }, + { sku: '483600', productType: PRODUCT_TYPES.STANDARD, widthMm: 48, lengthM: 120, thicknessMicron: 47, sleeveBrand: 'фрегат', quantityPerBox: '36' }, + { sku: '752600', productType: PRODUCT_TYPES.STANDARD, widthMm: 75, lengthM: 66, thicknessMicron: 47, sleeveBrand: 'фрегат', quantityPerBox: '24' }, + { sku: '753600', productType: PRODUCT_TYPES.STANDARD, widthMm: 75, lengthM: 120, thicknessMicron: 47, sleeveBrand: 'фрегат', quantityPerBox: '24' }, +]; + +function sentenceCase(value) { + if (!value) { + return value; + } + return value[0].toUpperCase() + value.slice(1); } -function normalizeText(value) { - return decodeHtmlEntities(value.replaceAll(/<[^>]*>/g, ' ').replaceAll(/\s+/g, ' ')).trim(); +function buildName(product) { + return [ + sentenceCase(product.productType), + `${product.widthMm}x${product.lengthM} м`, + `${product.thicknessMicron} мкм`, + `втулка ${sentenceCase(product.sleeveBrand)}`, + `короб ${product.quantityPerBox}`, + ].join(', '); } -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 = /]*)class="[^"]*product-card[^"]*"([^>]*)>([\s\S]*?)<\/a>/g; - - let match; - while ((match = cardRegex.exec(html)) !== null) { - const attrs = `${match[1]} ${match[2]}`; - const body = match[3]; - const hrefMatch = attrs.match(/href="([^"]+)"/); - const titleMatch = body.match(/
([\s\S]*?)<\/div>/); - const typeMatch = body.match(/
([\s\S]*?)<\/div>/); - const href = hrefMatch?.[1] ?? ''; - const title = normalizeText(titleMatch?.[1] ?? ''); - const type = normalizeText(typeMatch?.[1] ?? ''); - 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()]; +function baseQuantity(quantityPerBox) { + const firstValue = quantityPerBox.split('/')[0]; + const parsed = Number.parseInt(firstValue, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : 24; } const company = await prisma.company.upsert({ @@ -140,23 +102,42 @@ const warehouseReserve = await prisma.warehouse.upsert({ create: { code: 'SPB-01', name: 'Reserve warehouse' }, }); -const products = await loadCatalogProducts(); -console.log(`Loaded ${products.length} products from fregat.org catalog.`); +const activeSkus = PRODUCT_ROWS.map((item) => item.sku); +await prisma.product.updateMany({ + where: { sku: { notIn: activeSkus } }, + data: { isActive: false }, +}); + +for (const product of PRODUCT_ROWS) { + const qtyBase = baseQuantity(product.quantityPerBox); + const qtyMain = qtyBase * 10; + const qtyReserve = qtyBase * 5; -for (const product of products) { const dbProduct = await prisma.product.upsert({ where: { sku: product.sku }, update: { - name: product.name, - description: product.description, - isCustomizable: product.isCustomizable, + name: buildName(product), + description: `Артикул: ${product.sku}`, + productType: product.productType, + widthMm: product.widthMm, + lengthM: product.lengthM, + thicknessMicron: product.thicknessMicron, + sleeveBrand: product.sleeveBrand, + quantityPerBox: product.quantityPerBox, + isCustomizable: false, isActive: true, }, create: { sku: product.sku, - name: product.name, - description: product.description, - isCustomizable: product.isCustomizable, + name: buildName(product), + description: `Артикул: ${product.sku}`, + productType: product.productType, + widthMm: product.widthMm, + lengthM: product.lengthM, + thicknessMicron: product.thicknessMicron, + sleeveBrand: product.sleeveBrand, + quantityPerBox: product.quantityPerBox, + isCustomizable: false, isActive: true, }, }); @@ -168,11 +149,11 @@ for (const product of products) { warehouseId: warehouseMain.id, }, }, - update: { availableQty: product.qtyMain }, + update: { availableQty: qtyMain }, create: { productId: dbProduct.id, warehouseId: warehouseMain.id, - availableQty: product.qtyMain, + availableQty: qtyMain, }, }); @@ -183,14 +164,14 @@ for (const product of products) { warehouseId: warehouseReserve.id, }, }, - update: { availableQty: product.qtyReserve }, + update: { availableQty: qtyReserve }, create: { productId: dbProduct.id, warehouseId: warehouseReserve.id, - availableQty: product.qtyReserve, + availableQty: qtyReserve, }, }); } -console.log('Seed complete. Use manager header x-user-id:', manager.id); +console.log(`Seed complete with ${PRODUCT_ROWS.length} products. Use manager header x-user-id: ${manager.id}`); await prisma.$disconnect();