← back to Dw Yolo Loop

scripts/cole-son-apply-pattern-map.js

54 lines

#!/usr/bin/env node
/* Price the remaining Cole & Son products (no SKU/pattern metafields) by parsing the PATTERN
   from the title and looking up its uniform MAP (248/249 Cole & Son patterns have one MAP).
   Reads /tmp/cs_recon.json (noMatch list) + /tmp/cs_pattern_map.tsv. DRY RUN unless --apply. */
const fs = require('fs');
const STORE = 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = process.env.T;
const APPLY = process.argv.includes('--apply');
const sleep = ms => new Promise(r => setTimeout(r, ms));

const PMAP = {};
fs.readFileSync('/tmp/cs_pattern_map.tsv', 'utf8').trim().split('\n').forEach(l => { const [p, m] = l.split('\t'); if (p) PMAP[p.trim().toUpperCase()] = parseFloat(m); });

// "Acacia - Blue & Green Multi | Cole & Son" → pattern "ACACIA"
function patternOf(title) {
  let t = title.split('|')[0].trim();                 // drop "| Cole & Son ..."
  t = t.split(/\s+-\s+/)[0].trim();                   // pattern is before " - colorway"
  return t.toUpperCase().replace(/\s+/g, ' ').trim();
}

async function api(p, opts = {}, tries = 5) {
  for (let i = 0; i < tries; i++) {
    const r = await fetch(`https://${STORE}/admin/api/2024-10${p}`, { ...opts, headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', ...(opts.headers || {}) } });
    if (r.status === 429 || r.status >= 500) { await sleep(1500 * (i + 1)); continue; }
    await sleep(95); return r;
  }
  throw new Error('fail ' + p);
}

(async () => {
  const recon = JSON.parse(fs.readFileSync('/tmp/cs_recon.json', 'utf8'));
  const cand = recon.noMatch;
  const matched = [], unmatched = [];
  for (const x of cand) {
    const pat = patternOf(x.title);
    const map = PMAP[pat];
    if (map && Math.abs(x.cur - map) > 0.5) matched.push({ ...x, pat, map });
    else if (map) { /* already at MAP */ }
    else unmatched.push({ ...x, pat });
  }
  console.log(`no-match pool: ${cand.length} | pattern-matched & need reprice: ${matched.length} | still unmatched: ${unmatched.length} | mode: ${APPLY ? 'APPLY' : 'DRY RUN'}\n`);
  matched.slice(0, 10).forEach(x => console.log(`  [${x.status}] $${x.cur} → $${x.map}  (${x.pat})  ${x.title.slice(0,40)}`));
  console.log('\nstill-unmatched patterns (sample):', [...new Set(unmatched.map(u => u.pat))].slice(0, 15).join(' | '));
  if (!APPLY) { console.log(`\nDRY RUN — re-run with --apply to reprice ${matched.length}.`); return; }
  let ok = 0, fail = 0;
  for (const x of matched) {
    try {
      const r = await api(`/variants/${x.vid}.json`, { method: 'PUT', body: JSON.stringify({ variant: { id: x.vid, price: String(x.map) } }) });
      if (r.ok) { ok++; if (ok % 50 === 0) console.log(`  …${ok}/${matched.length}`); } else { fail++; console.log(`  FAIL ${x.id} ${r.status}`); }
    } catch (e) { fail++; }
  }
  console.log(`\nDONE — ${ok} repriced, ${fail} failed.`);
})().catch(e => { console.error(e); process.exit(1); });