[object Object]

← back to Designer Wallcoverings

WallQuest title material-append + renumber planners

91e639edcea384e5b8ecd883362dfd588edf55a0 · 2026-07-06 15:01:35 -0700 · Steve

Files touched

Diff

commit 91e639edcea384e5b8ecd883362dfd588edf55a0
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jul 6 15:01:35 2026 -0700

    WallQuest title material-append + renumber planners
---
 scripts/renumber_material.js | 75 ++++++++++++++++++++++++++++++++++++++++++++
 scripts/renumber_today.js    | 67 +++++++++++++++++++++++++++++++++++++++
 scripts/wq_title_material.js | 59 ++++++++++++++++++++++++++++++++++
 3 files changed, 201 insertions(+)

diff --git a/scripts/renumber_material.js b/scripts/renumber_material.js
new file mode 100644
index 00000000..0800676c
--- /dev/null
+++ b/scripts/renumber_material.js
@@ -0,0 +1,75 @@
+#!/usr/bin/env node
+/**
+ * Plan/execute renumber of PR material-lines' SKUs into a 300000 series.
+ * Material prefixes (configurable): GRS MIC CORK PWV RAF SIS GRA GRT PRG PRC.
+ * DRY-RUN by default: reports per-prefix counts + current number ranges + proposed 300000 mapping.
+ * NO writes without --apply. Numbering: single shared 300000 counter, ordered by (prefix, current#),
+ * each material product → <PREFIX>-3000NN. Sample variant SKU follows as <newsku>-Sample.
+ */
+require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
+const SHOP = process.env.SHOPIFY_STORE_DOMAIN;
+const TOKEN = process.env.SHOPIFY_ADMIN_ACCESS_TOKEN || process.env.SHOPIFY_ADMIN_TOKEN;
+const VER = process.env.SHOPIFY_ADMIN_API_VERSION || '2024-01';
+const REST = `https://${SHOP}/admin/api/${VER}`;
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+const MATERIAL = new Set((process.env.MAT_PREFIXES || 'GRS,MIC,CORK,PWV,RAF,SIS,GRA,GRT,PRG,PRC').split(','));
+const START = 300001;
+
+async function getPage(url) {
+  for (let attempt = 0; attempt < 6; attempt++) {
+    const res = await fetch(url, { headers: { 'X-Shopify-Access-Token': TOKEN } });
+    if (res.status === 429) { await sleep(2000); continue; }
+    if (!res.ok) throw new Error(`${res.status}: ${(await res.text()).slice(0, 150)}`);
+    const link = res.headers.get('Link') || ''; let next = null;
+    if (link.includes('rel="next"')) for (const p of link.split(',')) if (p.includes('rel="next"')) next = p.split(';')[0].trim().replace(/[<>]/g, '');
+    return { products: (await res.json()).products, next };
+  }
+  throw new Error('429 retries exhausted');
+}
+const prefixOf = (sku) => (sku.match(/^[A-Za-z]+/) || ['?'])[0].toUpperCase();
+
+(async () => {
+  let url = `${REST}/products.json?vendor=Phillipe%20Romano&status=active&limit=250&fields=id,title,variants`;
+  const mats = [];
+  while (url) {
+    const { products, next } = await getPage(url);
+    for (const p of products) {
+      const main = (p.variants || []).find(v => v.sku && !/-sample$/i.test(v.sku));
+      if (!main) continue;
+      const pfx = prefixOf(main.sku);
+      if (!MATERIAL.has(pfx)) continue;
+      const sample = (p.variants || []).find(v => /-sample$/i.test(v.sku || ''));
+      mats.push({ id: p.id, title: p.title, sku: main.sku, mainVariantId: main.id, sampleVariantId: sample?.id, sampleSku: sample?.sku, pfx });
+    }
+    url = next; await sleep(400);
+  }
+  // per-prefix scope
+  const byP = {};
+  for (const m of mats) { (byP[m.pfx] ||= { n: 0, nums: [] }); byP[m.pfx].n++; const num = parseInt((m.sku.match(/(\d+)/) || [])[1] || '0', 10); byP[m.pfx].nums.push(num); }
+  console.log(`SCOPE — active PR material-line products: ${mats.length}`);
+  console.log('PREFIX  COUNT  CURRENT#RANGE');
+  for (const [k, v] of Object.entries(byP).sort((a, b) => b[1].n - a[1].n))
+    console.log(`  ${k.padEnd(6)} ${String(v.n).padStart(5)}  ${Math.min(...v.nums)}–${Math.max(...v.nums)}`);
+
+  // proposed mapping: shared 300000 counter, stable order (prefix, current#)
+  mats.sort((a, b) => a.pfx === b.pfx ? (parseInt((a.sku.match(/(\d+)/)||[])[1]||0) - parseInt((b.sku.match(/(\d+)/)||[])[1]||0)) : a.pfx.localeCompare(b.pfx));
+  let seq = START;
+  for (const m of mats) { m.newSku = `${m.pfx}-${seq}`; m.newSample = `${m.pfx}-${seq}-Sample`; seq++; }
+  console.log(`\nProposed 300000-series range: ${START}–${seq - 1}  (${mats.length} products)`);
+  console.log('Sample mapping (first 12):');
+  mats.slice(0, 12).forEach(m => console.log(`  ${m.sku.padEnd(16)} → ${m.newSku.padEnd(14)}  ${m.title.slice(0, 40)}`));
+
+  const APPLY = process.argv.includes('--apply');
+  if (!APPLY) { console.log('\nNO WRITES. Re-run --apply to renumber (Shopify variant SKUs only; dw_unified sync is a separate step).'); return; }
+  console.log(`\nApplying ${mats.length} renumbers (main + sample variant SKUs)…`);
+  let ok = 0;
+  for (const m of mats) {
+    try {
+      await fetch(`${REST}/variants/${m.mainVariantId}.json`, { method: 'PUT', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ variant: { id: m.mainVariantId, sku: m.newSku } }) });
+      if (m.sampleVariantId) { await sleep(300); await fetch(`${REST}/variants/${m.sampleVariantId}.json`, { method: 'PUT', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ variant: { id: m.sampleVariantId, sku: m.newSample } }) }); }
+      ok++;
+    } catch (e) { console.log(`  ✗ ${m.sku}: ${e.message.slice(0, 100)}`); }
+    await sleep(300);
+  }
+  console.log(`Renumbered ${ok}/${mats.length}. (Remember: update dw_unified dw_sku mapping to match.)`);
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
diff --git a/scripts/renumber_today.js b/scripts/renumber_today.js
new file mode 100644
index 00000000..961ac947
--- /dev/null
+++ b/scripts/renumber_today.js
@@ -0,0 +1,67 @@
+#!/usr/bin/env node
+/**
+ * Renumber TODAY'S material SKUs (phillipe_romano_catalog created 2026-07-06 = the 87 WallQuest
+ * grasscloth/veneers) into a 300000 series, per-prefix (GRS-300001+, CORK-300001+, MIC-300001+).
+ * Updates BOTH Shopify variant SKUs (main + -Sample) AND dw_unified phillipe_romano_catalog.dw_sku,
+ * in lockstep. DRY-RUN by default (prints full old→new map, NO writes). --apply to execute.
+ */
+require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
+const { Client } = require('pg');
+const SHOP = process.env.SHOPIFY_STORE_DOMAIN;
+const TOKEN = process.env.SHOPIFY_ADMIN_ACCESS_TOKEN || process.env.SHOPIFY_ADMIN_TOKEN;
+const VER = process.env.SHOPIFY_ADMIN_API_VERSION || '2024-01';
+const REST = `https://${SHOP}/admin/api/${VER}`;
+const DBURL = process.env.DATABASE_URL || 'postgresql://stevestudio2@/dw_unified?host=/tmp';
+const DAY = process.env.RENUM_DAY || '2026-07-06';
+const APPLY = process.argv.includes('--apply');
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+const prefixOf = (sku) => (sku.match(/^[A-Za-z]+/) || ['?'])[0].toUpperCase();
+
+async function rest(method, path, body) {
+  for (let a = 0; a < 6; a++) {
+    const res = await fetch(`${REST}${path}`, { method, headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined });
+    if (res.status === 429) { await sleep(2000); continue; }
+    if (!res.ok) throw new Error(`${res.status}: ${(await res.text()).slice(0, 150)}`);
+    return res.json();
+  }
+  throw new Error('429 retries exhausted');
+}
+
+(async () => {
+  const db = new Client({ connectionString: DBURL }); await db.connect();
+  const { rows } = await db.query(
+    `SELECT id, dw_sku, shopify_product_id, pattern_name, color_name
+     FROM phillipe_romano_catalog
+     WHERE created_at::date=$1 AND shopify_product_id IS NOT NULL
+     ORDER BY dw_sku`, [DAY]);
+  // per-prefix 300000 counter
+  const ctr = {};
+  for (const r of rows) { const p = prefixOf(r.dw_sku); ctr[p] = (ctr[p] || 300000) + 1; r.pfx = p; r.newSku = `${p}-${ctr[p]}`; }
+
+  console.log(`${APPLY ? '' : '[DRY-RUN] '}Renumber TODAY'S (${DAY}) material SKUs → 300000 series`);
+  console.log(`Target: ${rows.length} products\n`);
+  const byP = {}; rows.forEach(r => (byP[r.pfx] = (byP[r.pfx] || 0) + 1));
+  console.log('per-prefix:', Object.entries(byP).map(([k, v]) => `${k}:${v}`).join('  '));
+  console.log('\nOLD SKU        → NEW SKU        (pattern - color)');
+  for (const r of rows) console.log(`  ${r.dw_sku.padEnd(13)} → ${r.newSku.padEnd(13)}  ${(r.pattern_name || '')} - ${(r.color_name || '')}`.slice(0, 90));
+
+  if (!APPLY) { console.log(`\nNO WRITES. Re-run --apply to renumber (Shopify variants main+Sample AND dw_unified dw_sku).`); await db.end(); return; }
+
+  console.log(`\nApplying ${rows.length}: Shopify variant SKUs + dw_unified dw_sku…`);
+  let ok = 0;
+  for (const r of rows) {
+    try {
+      const prod = (await rest('GET', `/products/${r.shopify_product_id}.json?fields=id,variants`)).product;
+      for (const v of prod.variants) {
+        const isSample = /-sample$/i.test(v.sku || '');
+        const newSku = isSample ? `${r.newSku}-Sample` : r.newSku;
+        await rest('PUT', `/variants/${v.id}.json`, { variant: { id: v.id, sku: newSku } });
+        await sleep(350);
+      }
+      await db.query(`UPDATE phillipe_romano_catalog SET dw_sku=$1, pl_updated_at=now() WHERE id=$2`, [r.newSku, r.id]);
+      ok++; console.log(`  ✓ ${r.dw_sku} → ${r.newSku}`);
+    } catch (e) { console.log(`  ✗ ${r.dw_sku}: ${e.message.slice(0, 120)}`); }
+  }
+  console.log(`Renumbered ${ok}/${rows.length} (Shopify + dw_unified in sync).`);
+  await db.end();
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
diff --git a/scripts/wq_title_material.js b/scripts/wq_title_material.js
new file mode 100644
index 00000000..1f4d373a
--- /dev/null
+++ b/scripts/wq_title_material.js
@@ -0,0 +1,59 @@
+#!/usr/bin/env node
+/**
+ * Add material/type to the tile title for the new WallQuest non-vinyl products.
+ * Per Steve's example: "Pure Elements - Sisal Onyx | Phillipe Romano" → "Pure Elements Sisal Onyx Wallcoverings".
+ * Transform: strip "| Phillipe Romano..." suffix, replace " - " with " ", append " Wallcoverings".
+ * Scope: phillipe_romano_catalog created 2026-07-06 with WallQuest patterns (the 87 non-vinyl batch).
+ * DRY-RUN by default (NO writes). --apply to write Shopify product titles.
+ */
+require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
+const { Client } = require('pg');
+const SHOP = process.env.SHOPIFY_STORE_DOMAIN;
+const TOKEN = process.env.SHOPIFY_ADMIN_ACCESS_TOKEN || process.env.SHOPIFY_ADMIN_TOKEN;
+const VER = process.env.SHOPIFY_ADMIN_API_VERSION || '2024-01';
+const REST = `https://${SHOP}/admin/api/${VER}`;
+const DBURL = process.env.DATABASE_URL || 'postgresql://stevestudio2@/dw_unified?host=/tmp';
+const APPLY = process.argv.includes('--apply');
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+const WQ_PATTERNS = ['Navy Grey & White', 'Natural Silhouettes', 'Pure Elements', 'Specialty Grasscloths & Veneers'];
+
+const newTitle = (pattern, color) => {
+  const core = `${pattern} - ${color}`.replace(/\s*\|\s*Phillipe Romano.*$/i, '').replace(/\s*-\s*/g, ' ').replace(/\s+/g, ' ').trim();
+  return `${core} Wallcoverings`;
+};
+
+async function rest(method, path, body) {
+  for (let a = 0; a < 6; a++) {
+    const res = await fetch(`${REST}${path}`, { method, headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined });
+    if (res.status === 429) { await sleep(2000); continue; }
+    if (!res.ok) throw new Error(`${res.status}: ${(await res.text()).slice(0, 150)}`);
+    return res.json();
+  }
+  throw new Error('429 retries exhausted');
+}
+
+(async () => {
+  const db = new Client({ connectionString: DBURL }); await db.connect();
+  const { rows } = await db.query(
+    `SELECT id, dw_sku, shopify_product_id, pattern_name, color_name
+     FROM phillipe_romano_catalog
+     WHERE created_at::date='2026-07-06' AND shopify_product_id IS NOT NULL AND pattern_name = ANY($1)
+     ORDER BY dw_sku`, [WQ_PATTERNS]);
+
+  console.log(`${APPLY ? '' : '[DRY-RUN] '}WallQuest non-vinyl → add material/type to title`);
+  console.log(`Scope: ${rows.length} products\n`);
+  for (const r of rows) r.title = newTitle(r.pattern_name, r.color_name);
+  console.log('DW_SKU        NEW TITLE');
+  rows.slice(0, 20).forEach(r => console.log(`  ${r.dw_sku.padEnd(13)} ${r.title}`));
+  if (rows.length > 20) console.log(`  … (+${rows.length - 20} more)`);
+
+  if (!APPLY) { console.log('\nNO WRITES. Re-run --apply to set these Shopify product titles.'); await db.end(); return; }
+  console.log(`\nApplying ${rows.length} titles…`); let ok = 0;
+  for (const r of rows) {
+    try { await rest('PUT', `/products/${r.shopify_product_id}.json`, { product: { id: Number(r.shopify_product_id), title: r.title } }); ok++; }
+    catch (e) { console.log(`  ✗ ${r.dw_sku}: ${e.message.slice(0, 110)}`); }
+    await sleep(350);
+  }
+  console.log(`Retitled ${ok}/${rows.length}`);
+  await db.end();
+})().catch(e => { console.error('FATAL', e); process.exit(1); });

← a0933e5b wallquest→PR: Lillian August onboard + per-yard/8yd model +  ·  back to Designer Wallcoverings  ·  Daisy contrarian fixes: strip coordinate-image contamination 87ba5cdc →