import fs from "node:fs"; import path from "node:path"; import crypto from "node:crypto"; import { fileURLToPath } from "node:url"; import sharp from "sharp"; import type { Db } from "../db.js"; import type { AppConfig } from "../config.js"; import { createProduct, getProductBySlug } from "../domain/product.js"; import { setFact } from "../domain/facts.js"; import { upsertFabric, upsertShade, ensureVariantMatrix } from "../domain/fabric.js"; import { upsertFurnitureLeg } from "../domain/furnitureLegs.js"; import { ingestUpload } from "../media/ingest.js"; import type { FactInput } from "../domain/facts.js"; const CONF_REF = "confirmations/2026-09-14-oskar (HANDOFF §2K)"; const CARD_REF = "TILIA_karta_katalogowa_2026-05-14.pdf (davis.pl)"; /** Fakty OSKAR-a wg arkusza potwierdzenia producenta z 2026-09-14. */ const OSKAR_FACTS: FactInput[] = [ { key: "dimensions.width_cm", value: 257, unit: "cm", status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF }, { key: "dimensions.depth_cm", value: 151, unit: "cm", status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF }, { key: "dimensions.height_with_cushions_cm", value: 88, unit: "cm", status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF, note: "wysokość z poduszkami oparciowymi" }, { key: "dimensions.height_without_cushions_cm", value: 77, unit: "cm", status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF }, { key: "dimensions.body_depth_cm", value: 104, unit: "cm", status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF, note: "głębokość korpusu bez szezlonga — arkusz producenta; korekta potwierdzona przez właściciela 2026-09-16" }, { key: "dimensions.seat_height_cm", value: 46, unit: "cm", status: "confirmed", sourceKind: "owner", sourceRef: "decyzja właściciela 2026-09-15", note: "wysokość siedziska" }, { key: "dimensions.seat_depth_cm", value: 58, unit: "cm", status: "confirmed", sourceKind: "owner", sourceRef: "decyzja właściciela 2026-09-15", note: "głębokość siedziska" }, { key: "dimensions.armrest_width_cm", value: 30, unit: "cm", status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF, note: "SZEROKOŚĆ podłokietnika, nie wysokość" }, { key: "sleeping.width_cm", value: 135, unit: "cm", status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF, note: "powierzchnia spania — aktualne, zastępuje historyczne 192" }, { key: "sleeping.length_cm", value: 190, unit: "cm", status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF }, { key: "packages.items", value: [ { dimensions_cm: [97, 88, 62], weight_kg: 39 }, { dimensions_cm: [132, 153, 74], weight_kg: 71 }, ], status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF }, { key: "packages.total_weight_kg", value: 110, unit: "kg", status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF }, { key: "cushions.loose_back_count", value: 5, status: "confirmed", sourceKind: "owner", sourceRef: "weryfikacja fizyczna na zdjęciach hali, 2026-09-15", note: "luźne poduszki oparciowe; korekta 4→5 po weryfikacji egzemplarza; poduszka dekoracyjna ze wzorca NIE jest w zestawie" }, { key: "legs.included", value: true, status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF }, { key: "legs.height_cm", value: 4.5, unit: "cm", status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF, note: "montuje klient; na zdjęciach hali niezamontowane" }, { key: "legs.furniture_leg_id", value: "leg_oskar_standard", status: "confirmed", sourceKind: "owner", sourceRef: "seed — nóżki bazowe OSKAR (katalog furniture_legs)", note: "wiersz furniture_legs używany do podmiany nóg w generowanych ujęciach" }, { key: "assembly.by_customer", value: "okucia / nóżki — montaż samodzielny", status: "confirmed", sourceKind: "owner", sourceRef: CONF_REF }, { key: "sides.available", value: ["L", "P"], status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF, note: "lustro konstrukcji zaakceptowane" }, { key: "sides.photographed", value: "P", status: "confirmed", sourceKind: "derived", sourceRef: "SKU VIL-OSK-P-TIL86 + zatwierdzona galeria (szezlong po prawej)", note: "strona egzemplarza referencyjnego — steruje rysunkiem wymiarowym" }, { key: "mechanism.sleep", value: "seat_pullout_backfold", status: "confirmed", sourceKind: "owner", sourceRef: "brief Etap C 2026-09-15 + zdjęcie src_c040b1f0 (oskar-rozlozony-oparcie-zlozone)", note: "siedzisko wysuwa się do przodu, jego oparcie opada płasko; oparcie szezlonga zostaje pionowo" }, { key: "warranty.months", value: 24, unit: "months", status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF }, { key: "production.country", value: "Polska", status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF, note: "produkcja Vilmax — wolno publikować" }, { key: "lead_time.days", value: 14, unit: "days", status: "confirmed", sourceKind: "manufacturer_sheet", sourceRef: CONF_REF, note: "dane wewnętrzne — NIE do opisu oferty" }, { key: "features.bedding_container", value: true, status: "confirmed", sourceKind: "owner", sourceRef: "zdjęcie hali IMG_6413 (2026-09-15)", note: "pojemnik na pościel pod siedziskiem — widoczny otwarty na zdjęciu hali" }, // Dane handlowe — decyzje właściciela 2026-09-15. { key: "fabric.photographed_shade", value: "86", status: "confirmed", sourceKind: "owner", sourceRef: "decyzja właściciela 2026-09-15 + zdjęcie hali IMG_6413", note: "TILIA 86 — potwierdzone; wcześniejsze dopasowanie RGB było orientacyjne" }, { key: "commerce.price_pln", value: 4399, unit: "PLN", status: "confirmed", sourceKind: "owner", sourceRef: "decyzja właściciela 2026-09-15", note: "cena sprzedaży w e-commerce właściciela (zwolnienie podmiotowe z VAT → cena = brutto); kanał Vilmax/Base: wycena osobno" }, { key: "commerce.vat_rate", value: "zwolnienie podmiotowe", status: "confirmed", sourceKind: "owner", sourceRef: "decyzja właściciela 2026-09-15", note: "kanał startowy = e-commerce właściciela; sprzedaż Vilmax przez Base.com: 23% — parametr kanału do skonfigurowania" }, { key: "commerce.sku", value: "VIL-OSK-P-TIL86", status: "confirmed", sourceKind: "owner", sourceRef: "decyzja właściciela 2026-09-15", note: "SKU wariantu referencyjnego (strona P, TILIA 86)" }, { key: "commerce.sku_pattern", value: "VIL-OSK-{L|P}-TIL{kod}", status: "confirmed", sourceKind: "owner", sourceRef: "decyzja właściciela 2026-09-15", note: "schemat SKU wariantów: strona L/P + odcień TILII" }, // Świadomie puste — decyzja właściciela, nie brak pracy. { key: "commerce.ean", value: null, status: "unknown", sourceKind: "owner", sourceRef: "decyzja właściciela 2026-09-15", note: "świadomie puste — pole opcjonalne w BaseLinker" }, ]; const TILIA_SHADES = ["01", "03", "08", "11", "17", "37", "39", "52", "56", "62", "75", "77", "83", "85", "86", "90", "100"]; const TILIA_PARAMS = { composition: "100% poliester", grammage_g_m2: { value: 420, tolerance: "±5%" }, martindale_cycles: { min: 90000, max: 100000, category: "A" }, usable_width_cm: ">=140", flammability_passed: ["EN 1021-1", "BS 5852 źr.0", "CAL TB117"], reach: true, easy_clean: false, pet_friendly: false, water_repellent: false, notes: [ "pilling 4 = kat. B — nie publikować", "zaczepianie 5X bez kategorii wyniku — nie publikować", "trudnopalność = usługa dodatkowa — nie publikować jako cecha standardu", "OEKO-TEX niepotwierdzone", ], }; /** Wzorce estetyki — obrazy wygenerowane przez pracownika w ChatGPT. */ const STYLE_BENCHMARKS: Array<{ file: string; shot: string; label: string }> = [ { file: "Messenger_creation_932D68BD-AC1D-4320-A58C-B19A14A21A21.jpeg", shot: "hero", label: "aranżacja salonu" }, { file: "Messenger_creation_531250AC-ED1C-47CA-80E5-4402DF80AAB8.jpeg", shot: "packshot_front", label: "packshot front" }, { file: "Messenger_creation_E93BAFFF-C1CB-47B7-B42D-286D4478C88F.jpeg", shot: "packshot_34", label: "packshot 3/4" }, { file: "Messenger_creation_5FDCF7E4-8489-4511-9139-AF53C26B21DD.jpeg", shot: "side", label: "boczna perspektywa z poduszkami" }, { file: "Messenger_creation_6B681C20-69AC-4570-B9C1-7ECC5477B50E.jpeg", shot: "back", label: "tył" }, { file: "Messenger_creation_4935D083-D11F-4732-A3A2-CA81503080D7.jpeg", shot: "sleep", label: "spanie — podwyższona perspektywa boczna" }, { file: "Messenger_creation_A62EF653-6364-421A-A7B2-618C5EAF69C9.jpeg", shot: "sleep", label: "spanie — widok rozłożonej powierzchni" }, { file: "Messenger_creation_28E19333-D11D-4AEA-913A-4097FA0103A3.jpeg", shot: "storage", label: "pojemnik na pościel" }, { file: "Messenger_creation_1915053D-0F8D-43A6-9A2A-56CA958AEDB2.jpeg", shot: "detail", label: "rozkładanie — perspektywa z góry" }, ]; const BENCHMARK_SOURCE_DIR = path.resolve( path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..", "vilmax-d2c-demo", "public", "vilmax-generated" ); const COCKPIT_ROOT = path.resolve( path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..", "vilmax-cockpit" ); /** Dokumenty źródłowe wgrywane realnym ingestem — dowody faktów w bazie. */ const SOURCE_DOCS: Array<{ rel: string; note: string; cardForTilia?: boolean }> = [ { rel: "data/fabrics/tilia/TILIA_karta_katalogowa_2026-05-14.pdf", note: `Karta katalogowa TILIA (Davis) — ${CARD_REF} — źródło parametrów tkaniny.`, cardForTilia: true, }, { rel: "data/confirmations/2026-09-14-oskar/Messenger_creation_201E28D8-B8F0-4C1D-B0B6-0716EA84E4FC.jpeg", note: "Materiał potwierdzenia danych OSKAR od producenta (2026-09-14) — plik 1/2.", }, { rel: "data/confirmations/2026-09-14-oskar/Messenger_creation_C0F561CA-47E2-4079-AF8C-387289A303C9.jpeg", note: "Materiał potwierdzenia danych OSKAR od producenta (2026-09-14) — plik 2/2.", }, ]; async function importSourceDocs(db: Db, cfg: AppConfig, productId: string): Promise<{ docs: number; cardPath: string | null }> { let docs = 0; let cardPath: string | null = null; for (const d of SOURCE_DOCS) { const abs = path.join(COCKPIT_ROOT, d.rel); if (!fs.existsSync(abs)) continue; const out = await ingestUpload(db, cfg, { buffer: fs.readFileSync(abs), originalName: path.basename(abs), productId, role: "source_document", note: d.note, uploadedBy: "system:seed", }); if (!out.duplicate) docs++; if (d.cardForTilia) cardPath = out.path; } return { docs, cardPath }; } async function importBenchmarks(db: Db, cfg: AppConfig, productId: string): Promise { let imported = 0; for (const b of STYLE_BENCHMARKS) { const src = path.join(BENCHMARK_SOURCE_DIR, b.file); if (!fs.existsSync(src)) continue; const buf = fs.readFileSync(src); const sha = crypto.createHash("sha256").update(buf).digest("hex"); const exists = db.prepare("SELECT id FROM source_assets WHERE sha256 = ?").get(sha); if (exists) continue; const rel = path.join(sha.slice(0, 2), `${sha}.jpeg`); const abs = path.join(cfg.originalsDir, rel); fs.mkdirSync(path.dirname(abs), { recursive: true }); fs.copyFileSync(src, abs); const meta = await sharp(buf).metadata(); const thumbName = `${sha}-w480.webp`; const thumbAbs = path.join(cfg.derivedDir, sha.slice(0, 2), thumbName); fs.mkdirSync(path.dirname(thumbAbs), { recursive: true }); await sharp(buf).resize({ width: 480, withoutEnlargement: true }).webp({ quality: 82 }).toFile(thumbAbs); const thumbRel = path.relative(cfg.derivedDir, thumbAbs); db.prepare( `INSERT INTO source_assets (id, product_id, role, sha256, mime, bytes, width, height, original_name, path, thumb_path, note, uploaded_by, created_at) VALUES (?, ?, 'style_reference', ?, 'image/jpeg', ?, ?, ?, ?, ?, ?, ?, 'system:seed', ?)` ).run( `src_${crypto.randomUUID()}`, productId, sha, buf.length, meta.width ?? null, meta.height ?? null, b.file, rel, thumbRel, `Wzorzec estetyki: ${b.label} [${b.shot}]. Wygenerowany przez pracownika w ChatGPT — NIE wynik pipeline Vilmal, NIE dowód geometrii produktu. Kopia z vilmax-d2c-demo/public — do weryfikacji z oryginałem.`, new Date().toISOString() ); imported++; } return imported; } export async function seedIfEmpty(db: Db, cfg: AppConfig): Promise<{ seeded: boolean; detail?: Record }> { const count = (db.prepare("SELECT COUNT(*) AS c FROM products").get() as { c: number }).c; if (count > 0) return { seeded: false }; const product = createProduct(db, { slug: "oskar", name: "Narożnik rozkładany OSKAR", kind: "corner-sofa" }); for (const f of OSKAR_FACTS) setFact(db, product.id, { ...f, changedBy: "system:seed" }); const tilia = upsertFabric(db, { code: "tilia", name: "TILIA", producer: "Davis", params: TILIA_PARAMS, aiStructure: "corduroy", aiPrompt: "ribbed corduroy with parallel wales, thick distinctive wale structure", verification: "confirmed", }); upsertFurnitureLeg(db, { id: "leg_oskar_standard", name: "OSKAR — nóżki standardowe", aiPrompt: "short matte black cylindrical wooden furniture legs, 4.5cm height", finishType: "czarny mat", }); for (const code of TILIA_SHADES) { upsertShade(db, tilia.id, { code, verification: "confirmed" }); } const matrix = ensureVariantMatrix(db, { productId: product.id, fabricId: tilia.id, sides: ["L", "P"] }); const benchmarks = await importBenchmarks(db, cfg, product.id); const { docs, cardPath } = await importSourceDocs(db, cfg, product.id); if (cardPath) { db.prepare("UPDATE fabrics SET card_path = ?, updated_at = ? WHERE id = ?") .run(cardPath, new Date().toISOString(), tilia.id); } return { seeded: true, detail: { product: product.slug, facts: OSKAR_FACTS.length, shades: TILIA_SHADES.length, variants: matrix.total, benchmarks, docs }, }; }