← 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)`);