← back to Designer Wallcoverings
WallQuest title material-append + renumber planners
91e639edcea384e5b8ecd883362dfd588edf55a0 · 2026-07-06 15:01:35 -0700 · Steve
Files touched
A scripts/renumber_material.jsA scripts/renumber_today.jsA scripts/wq_title_material.js
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 →