← back to Majilite Onboard

scripts/build.js

152 lines

#!/usr/bin/env node
/**
 * Majilite "Metallic Specialties I" (Nytek) — Full Monte staging build.
 * Reads data/source_swatches.json (160 original pattern/colorway swatches),
 * assigns DW-series SKUs (DWCC-######, auto next-free series), stamps the
 * ONE shared Nytek spec onto every SKU, buckets a color family + dot hex for
 * the sort/color grid, and writes data/products.json + data/collection.json.
 *
 * HARD rules honored:
 *  - Original pattern + colorway names stay CUSTOMER-FACING (title = "Pattern Colorway").
 *  - Majilite is name-keyed → mfr_sku = "Pattern Colorway" (the orderable identifier).
 *  - No cost on the card → quote-only (display_prices=false), $4.25 sample, cost=null.
 *  - Images always local: images/DWCC-######.png
 */
const fs = require('fs');
const path = require('path');

const ROOT = path.resolve(__dirname, '..');
const src = JSON.parse(fs.readFileSync(path.join(ROOT, 'data/source_swatches.json'), 'utf8'));

const VENDOR = 'Majilite';
const BRAND = 'Nytek';
const COLLECTION = 'Metallic Specialties I';
const SKU_PREFIX = 'DWCC';       // renamed DWMJ -> DWCC (Steve 2026-08-10); folds into DWCC house series
const SKU_BASE = 600000;         // DW number block for this series (Steve, 2026-08-10) -> DWCC-600001..
const VENDOR_CODE = 'MAJ';
const PRODUCT_TYPE = 'Wallcovering';
const MATERIAL = 'Faux Leather (Nylon Fiber Matrix / Nytek®)';
const SAMPLE_PRICE = 4.25;

// One spec block for the entire collection (page 10 of the digital card).
const SPEC = {
  material: 'Nytek® — specially engineered Nylon Fiber Matrix faux leather (PFAS-, PVC-, plasticizer-free)',
  width: '54 in / 137 cm',
  width_in: 54,
  weight: '8.8 oz/sq yd / 300 g/m²',
  thickness: '28 mils / 0.7 mm',
  cleanability_code: 'S/W',
  crocking: 'Wet 5 / Dry 5',
  colorfastness: 'Excellent (Gas, Fadeometer, Weather-O-Meter, Sulfide Staining)',
  durability: '300,000+ double rubs (Wyzenbeek wire screen, ASTM D-4157-82)',
  flammability_inherent: 'UFAC/NFPA 260 (Class 1) & CAL Technical Bulletin 117-2013 Section 1 — Pass',
  flammability_optional: 'Optional FR for ASTM-E84 (adhered), IMO 652, IMO 653, BS5852, BS476, EN13501-1',
  features: [
    'Breathable', 'Built-in stain resistance (no topcoat needed)',
    'Easy cleaning with common commercial cleaners', 'No off-gassing',
    'Hydrolysis resistance up to 10 weeks'
  ],
  custom_options: 'Custom colors (100-yard min), embossing (small min), quilting, printing, weaving, laser cutting; artisan collaborations',
  environmental: 'Free from PVCs, phthalates, formaldehyde, heavy metals, dioxins, ozone-depleting chemicals; made in Massachusetts via EPA-approved processes',
  care: 'Clean with mild soap and water or EPA-approved disinfectants',
  uses: 'Seating, wall coverings, flat surfaces, displays',
  industries: 'Retail, hospitality, healthcare, aerospace, marine, automotive, interior design',
  manufacturer: 'Majilite Corporation, 1530 Broadway Road, Dracut, MA 01826',
  contact: { phone: '+1.978.441.6800', email: 'Sales@majilite.com', web: 'www.majilite.com' },
  source: 'Majilite Metallic Specialties I Digital Card 2026 (p.10)'
};

// ---- Color-family bucketing (keyword -> family + representative dot hex) ----
// Ordered: first match wins. Tuned for this metallic/neutral faux-leather line.
const FAMILY_RULES = [
  [/rose gold|blossom|petal|pink|metallic rose/i, 'Rose Gold', '#d6a4a0'],
  [/gold|brass|mystic gold|antique gold|soft gold|gold leaf|gold coin|gold stardust|white gold|cream gold/i, 'Gold', '#c9a04e'],
  [/champagne|chardonnay|zinfandel|cameo|pashmina/i, 'Champagne', '#e4d3ad'],
  [/black|onyx|midnight|carbon|techno/i, 'Black', '#1c1c1e'],
  [/pewter|slate|charcoal|graphite|granite|distinction grey|nuance grey|infinity grey|shimmer slate/i, 'Pewter/Charcoal', '#6b6f74'],
  [/stainless|steel|mercury|nickel|silver|aluminum|chrome|platinum|paragon|status|iceberg|diamond|storm|dove|moonbeam|mist|igloo/i, 'Silver/Grey', '#b7bcc2'],
  [/white|snow|polar|ice|pure white|glacier|icicle|bright star|salt/i, 'White', '#f2f2f0'],
  [/wheat|buff|beige|natural|tan|sand|fawn|pebble|parchment|featherbed|gelato|eggshell|ivory|crème|creme|powder|taupe|penny|bronze|copper|espresso|coin/i, 'Neutral/Tan', '#cdbf9e'],
];
function familyFor(color) {
  for (const [re, fam, hex] of FAMILY_RULES) if (re.test(color)) return { color_family: fam, color_hex: hex };
  return { color_family: 'Metallic', color_hex: '#a9adb2' };
}

// Style / motif tags from the pattern texture family
function styleTagsFor(pattern) {
  const p = pattern.toLowerCase();
  const t = [];
  if (/raindrop|starlite|hive|hammered|pebble|stature/.test(p)) t.push('Textured');
  if (/legacy|nuance|charm|linea|cross-hatch|status|techno/.test(p)) t.push('Woven Look');
  if (/reflection|shimmer|radiance|finesse|eclipse|brushed|burnished|metallic paragon|attache/.test(p)) t.push('Metallic Sheen');
  if (!t.length) t.push('Faux Leather');
  return t;
}

const nowISO = new Date().toISOString();
const products = src.swatches.map((sw, i) => {
  const seq = i + 1;
  const num = SKU_BASE + seq;                         // 600001 .. 600160
  const dw_sku = `${SKU_PREFIX}-${num}`;
  const title = `${sw.pattern} ${sw.color}`;          // original names, customer-facing
  const mfr_sku = `${sw.pattern} ${sw.color}`;        // Majilite is name-keyed
  const fam = familyFor(sw.color);
  const tags = Array.from(new Set([
    'Faux Leather', 'Metallic', VENDOR, BRAND, COLLECTION, 'Contract',
    sw.pattern, sw.color, fam.color_family, ...styleTagsFor(sw.pattern)
  ]));
  return {
    seq,
    dw_sku,
    variant_sku: `${dw_sku}-Sample`,
    mfr_sku,
    title,
    pattern: sw.pattern,
    color: sw.color,
    vendor: VENDOR,
    brand: BRAND,
    collection: COLLECTION,
    product_type: PRODUCT_TYPE,
    material: MATERIAL,
    color_family: fam.color_family,
    color_hex: fam.color_hex,
    image: `images/${dw_sku}.png`,
    tags,
    pricing: {
      unit: 'yard', model: 'sample', sellable_as: 'sample',
      price: SAMPLE_PRICE, sample_price: SAMPLE_PRICE, cost: null, retail: null,
      display_prices: true,             // onboarded as a sellable $4.25 SAMPLE (Steve, 2026-08-10)
      pricing_notes: 'Onboarded as a $4.25 sample. By-the-yard retail pending Majilite cost list.'
    },
    spec: SPEC,                         // <-- spec assigned to EVERY sku
    source_page: sw.page,
    created_at: nowISO,
    onboard_status: 'sample'            // sellable $4.25 sample (yard retail deferred)
  };
});

const collection = {
  vendor: VENDOR, vendor_code: VENDOR_CODE, brand: BRAND, collection: COLLECTION,
  sku_prefix: SKU_PREFIX + '-', sku_range_start: SKU_BASE + 1, is_private_label: false, private_label_name: null,
  pricing_unit: 'yard', pricing_model: 'sample', sample_price: SAMPLE_PRICE,
  display_prices: true, onboard_as: 'sample', product_type: PRODUCT_TYPE, material: MATERIAL,
  count: products.length, spec: SPEC,
  source_pdf: src.source_pdf, built_at: nowISO,
  gated_for_golive: [
    'Shopify publish of the 160 $4.25 SAMPLE products (customer-facing) — Steve-gated',
    'vendor_registry INSERT to dw_unified (Mac2-canonical)',
    'Later: by-the-yard retail once Majilite sends the cost list (Kathryn Gabriel / Two Gabriels)'
  ]
};

fs.mkdirSync(path.join(ROOT, 'data'), { recursive: true });
fs.writeFileSync(path.join(ROOT, 'data/products.json'), JSON.stringify(products, null, 2));
fs.writeFileSync(path.join(ROOT, 'data/collection.json'), JSON.stringify(collection, null, 2));

// quick family tally for the console
const tally = {};
for (const p of products) tally[p.color_family] = (tally[p.color_family] || 0) + 1;
console.log(`Built ${products.length} SKUs  ${products[0].dw_sku} .. ${products[products.length-1].dw_sku}`);
console.log('Color families:', tally);