← back to Sister Parish Onboarding

scripts/normalize.js

145 lines

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');

const DW_MARKUP_DIVISOR = 0.85;
const ROOT = path.join(__dirname, '..');

const pages = ['raw_page1.json','raw_page2.json'].map(f => JSON.parse(fs.readFileSync(path.join(ROOT,'data',f),'utf8')).products);
const all = pages.flat();

function classifyVariant(v) {
  const opt3 = (v.option3 || '').toLowerCase();
  const title = (v.title || '').toLowerCase();
  if (opt3.includes('sample') || title.includes('sample')) return 'sample';
  if (/yard|roll|bolt/i.test(opt3) || /yard|roll|bolt/i.test(title)) return 'bolt';
  return 'other';
}

function extractYards(v) {
  const s = `${v.option3 || ''} ${v.title || ''}`;
  const yd = s.match(/(\d+(?:\.\d+)?)\s*yard/i);
  return yd ? parseFloat(yd[1]) : null;
}

const PRINT_TYPES = new Set(['Wallpaper']);

const normalized = [];
let skipped = 0;
for (const p of all) {
  if (!PRINT_TYPES.has(p.product_type)) { skipped++; continue; }

  const variants = p.variants.map(v => {
    const kind = classifyVariant(v);
    const yards = extractYards(v);
    const vendorPrice = parseFloat(v.price);
    const dwPrice = Math.round((vendorPrice / DW_MARKUP_DIVISOR) * 100) / 100;
    return {
      sp_variant_id: v.id,
      sku: v.sku,
      title: v.title,
      option1: v.option1,
      option2: v.option2,
      option3: v.option3,
      kind,
      yards,
      vendor_retail: vendorPrice,
      dw_retail: dwPrice,
      available: v.available,
      featured_image: v.featured_image?.src || null,
      grams: v.grams
    };
  });

  const images = p.images.map(i => ({ src: i.src, position: i.position, width: i.width, height: i.height, alt: i.alt }));

  normalized.push({
    sp_product_id: p.id,
    handle: p.handle,
    title: p.title,
    vendor: p.vendor,
    product_type: p.product_type,
    tags: p.tags,
    body_html: p.body_html,
    url: `https://sisterparishdesign.com/products/${p.handle}`,
    images,
    primary_image: images[0]?.src || null,
    variants,
    has_sample: variants.some(v => v.kind === 'sample'),
    has_bolt: variants.some(v => v.kind === 'bolt'),
    has_image: images.length > 0
  });
}

fs.writeFileSync(path.join(ROOT,'output','normalized.json'), JSON.stringify(normalized, null, 2));

// Stats
console.log(`Total products in collection: ${all.length}`);
console.log(`Skipped (non-print): ${skipped}`);
console.log(`Print products kept: ${normalized.length}`);
console.log(`Total variants: ${normalized.reduce((s,p)=>s+p.variants.length,0)}`);
console.log(`Products with image: ${normalized.filter(p=>p.has_image).length}/${normalized.length}`);
console.log(`Products with sample variant: ${normalized.filter(p=>p.has_sample).length}`);
console.log(`Products with bolt variant: ${normalized.filter(p=>p.has_bolt).length}`);

// Price spot-check
const samples = normalized.slice(0,5);
console.log('\nPrice spot-check (vendor → DW = vendor/0.85):');
for (const p of samples) {
  const bolt = p.variants.find(v=>v.kind==='bolt');
  if (bolt) console.log(`  ${p.title.padEnd(40)}  $${bolt.vendor_retail.toFixed(2).padStart(8)} → $${bolt.dw_retail.toFixed(2)}`);
}

// Sample CSV for Shopify draft import (Shopify format)
const csvLines = ['Handle,Title,Body (HTML),Vendor,Type,Tags,Published,Option1 Name,Option1 Value,Option2 Name,Option2 Value,Option3 Name,Option3 Value,Variant SKU,Variant Inventory Qty,Variant Inventory Policy,Variant Inventory Tracker,Variant Price,Variant Compare At Price,Variant Requires Shipping,Variant Taxable,Image Src,Image Position,Image Alt Text,SEO Title,SEO Description,Status'];

function csvEscape(s) {
  if (s == null) return '';
  s = String(s);
  if (/[",\n]/.test(s)) return `"${s.replace(/"/g,'""')}"`;
  return s;
}

for (const p of normalized) {
  const handle = `sp-${p.handle}`;  // prefix to avoid collisions in DW shop
  const tags = [...new Set([...(p.tags||[]), 'Sister Parish', 'Wallcovering', 'Wallpaper', 'Print'])].join(', ');
  const seoTitle = `${p.title} | Sister Parish at Designer Wallcoverings`;
  const seoDesc = `Free samples and trade service on ${p.title} by Sister Parish. Designer Wallcoverings ships nationwide.`;
  p.variants.forEach((v, i) => {
    const isFirst = i === 0;
    csvLines.push([
      handle,
      isFirst ? p.title : '',
      isFirst ? (p.body_html||'').replace(/\n/g,' ') : '',
      isFirst ? 'Sister Parish' : '',
      isFirst ? 'Wallpaper' : '',
      isFirst ? tags : '',
      isFirst ? 'TRUE' : '',
      isFirst ? 'Pattern' : '',
      v.option1 || '',
      isFirst ? 'Material' : '',
      v.option2 || '',
      isFirst ? 'Size' : '',
      v.option3 || '',
      `DWSP-${v.sku}`,
      2026,
      'continue',
      'shopify',
      v.dw_retail.toFixed(2),
      v.vendor_retail.toFixed(2),  // compare-at = vendor retail (the "list" price)
      'TRUE',
      'TRUE',
      isFirst ? (p.primary_image || '') : '',
      isFirst ? 1 : '',
      isFirst ? p.title : '',
      isFirst ? seoTitle : '',
      isFirst ? seoDesc : '',
      'draft'   // DRAFT per standing rule until reviewed
    ].map(csvEscape).join(','));
  });
}
fs.writeFileSync(path.join(ROOT,'output','shopify_import.csv'), csvLines.join('\n'));
console.log(`\nWrote ${normalized.length} products / ${normalized.reduce((s,p)=>s+p.variants.length,0)} variants to output/`);
console.log(`  - normalized.json  (full data)`);
console.log(`  - shopify_import.csv  (Shopify CSV, status=draft)`);