← back to Designer Wallcoverings

onboarding/sangetsu-lilycolor/lily-merge.cjs

85 lines

#!/usr/bin/env node
/**
 * Merge the Lilycolor trade PRICE-BOOK specs (lily-pricebook-parser output) with the
 * shop.lilycolor.co.jp feed IMAGES into one unified per-SKU staging row (OFFLINE).
 *
 * Pricebook = full trade line + price + specs (no images).
 * Shop feed = clean per-SKU images (subset of the line).
 * Join on a normalized SKU key (uppercase, alphanumerics only, strip sample/nori suffix).
 *
 * HARD: writes ONLY to local staging. No Shopify, no dw_unified, no publish.
 *   node lily-merge.cjs
 */
const fs = require('fs');
const path = require('path');
const DIR = path.join(__dirname, 'staging');
const PB = path.join(DIR, 'lilycolor-pricebook-staging.jsonl');
const FEED = path.join(DIR, 'lilycolor-staging.jsonl');
const MANIFEST = path.join(DIR, 'lilycolor-image-manifest.json');
const OUT = path.join(DIR, 'lilycolor-unified-staging.jsonl');

const rd = (f) => fs.readFileSync(f, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
const norm = (s) => String(s || '').toUpperCase().replace(/[^A-Z0-9]/g, '').replace(/(?:SAMPLE|NORI)$/, '');

// index shop-feed images by normalized SKU
const feedImg = new Map();
for (const p of rd(FEED)) {
  const k = norm(p.mfr_sku);
  if (!k || !(p.images && p.images.length)) continue;
  if (!feedImg.has(k)) feedImg.set(k, { images: p.images, title_ja: p.title_ja, handle: p.handle });
}

// official per-SKU image manifest (filenames inside the catalog zips; bytes deferred)
const manifest = fs.existsSync(MANIFEST) ? JSON.parse(fs.readFileSync(MANIFEST, 'utf8')) : {};

// PATTERN-FIRST ordering. The swatch (pattern/texture) must lead; the room/model
// scene goes last. Shop-feed room shots are named `<SKU>_model.jpg`; swatches are
// `<SKU>_<uuid>.jpg`. Also prefer images that match THIS exact SKU (feed products
// bundle sibling colorways' images together).
const isRoom = (name) => /_model\.|_room|_scene|_R(?:_\d+)?\.|_RS\./i.test(name);
function orderPreviews(images, sku) {
  const skuKey = norm(sku);
  const rank = (u) => {
    const base = u.split('/').pop().split('?')[0];
    const mine = norm((base.match(/^([A-Za-z]+-?\d+)/) || [])[1]) === skuKey;
    const room = isRoom(base);
    return (room ? 2 : 0) + (mine ? 0 : 1);   // mine-swatch < other-swatch < mine-room < other-room
  };
  return [...images].sort((a, b) => rank(a) - rank(b));
}
// manifest kind priority: C swatch, P pattern-tile, SP special — all BEFORE R room
const KIND_RANK = { C: 0, P: 1, SP: 2, R: 3, '?': 4 };
const orderManifest = (ms) => [...ms].sort((a, b) => (KIND_RANK[a.kind] ?? 9) - (KIND_RANK[b.kind] ?? 9));

const out = [];
let withAnyImg = 0, withPreview = 0;
for (const r of rd(PB)) {
  const k = norm(r.mfr_sku);
  const f = feedImg.get(k);
  const mImgs = orderManifest(manifest[r.mfr_sku] || manifest[k] || []);   // pattern-first
  const previews = orderPreviews((f && f.images) || [], r.mfr_sku);        // pattern-first, this-SKU-first
  if (previews.length) withPreview++;
  if (previews.length || mImgs.length) withAnyImg++;
  out.push({
    ...r,
    title_ja: (f && f.title_ja) || null,
    title_en: null,                       // TODO JA->EN at gated enrichment
    preview_images: previews,             // displayable remote URLs (shop feed, $0 disk)
    image_manifest: mImgs,                // ALL official swatch/room/pattern shots (bytes gated)
    image_count: mImgs.length || previews.length,
    image_kinds: [...new Set(mImgs.map((i) => i.kind))],
    image_bytes_fetched: false,           // gated: pull actual bytes later / on import box
    shop_handle: (f && f.handle) || null,
    has_image: !!(previews.length || mImgs.length),
  });
}
fs.writeFileSync(OUT, out.map((r) => JSON.stringify(r)).join('\n') + '\n');
console.log(JSON.stringify({
  pricebookSkus: out.length,
  withAnyImage: withAnyImg,
  withDisplayablePreview: withPreview,
  imageCoveragePct: Math.round((withAnyImg / out.length) * 100),
  totalManifestImages: out.reduce((a, r) => a + r.image_manifest.length, 0),
  out: OUT,
}, null, 2));