← back to Designer Wallcoverings
WallQuest→PR grasscloth pusher + retitle mode; fix 34 collapsed live titles
c3082e421b90c95a885197936f907096d4cc4408 · 2026-07-06 13:42:09 -0700 · Steve
Files touched
A scripts/push_wallquest_pr.js
Diff
commit c3082e421b90c95a885197936f907096d4cc4408
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jul 6 13:42:09 2026 -0700
WallQuest→PR grasscloth pusher + retitle mode; fix 34 collapsed live titles
---
scripts/push_wallquest_pr.js | 175 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 175 insertions(+)
diff --git a/scripts/push_wallquest_pr.js b/scripts/push_wallquest_pr.js
new file mode 100644
index 00000000..84b9f1d1
--- /dev/null
+++ b/scripts/push_wallquest_pr.js
@@ -0,0 +1,175 @@
+#!/usr/bin/env node
+/**
+ * Push the 87 WallQuest specialty grasscloths/veneers → DW Shopify as PHILLIPE ROMANO.
+ * Source: dw_unified.phillipe_romano_catalog, created_at::date = 2026-07-06.
+ *
+ * Convention (locked with Steve 2026-07-06, matches existing PR grasscloth line):
+ * - vendor=Phillipe Romano, product_type=Wallcovering, status=DRAFT (activated separately)
+ * - Option "Size":
+ * "Sold Per Yard" sku=<dw_sku> price=(roll_cost/8)/0.65/0.85 policy=continue
+ * "Sample" sku=<dw_sku>-Sample price=4.25 policy=deny
+ * both mgmt=shopify, inventory=2026
+ * - handle = pr-<dw_sku lower>, image rehosted by Shopify from image_url
+ * - writes shopify_product_id/handle/on_shopify back into phillipe_romano_catalog
+ *
+ * Flags: --dry-run --limit N --only <dw_sku,dw_sku> --activate (flip drafts→active)
+ */
+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 DBURL = process.env.DATABASE_URL || 'postgresql://stevestudio2@/dw_unified?host=/tmp';
+if (!SHOP || !TOKEN) { console.error('Need SHOPIFY_STORE_DOMAIN + SHOPIFY_ADMIN_ACCESS_TOKEN'); process.exit(1); }
+
+const BASE = `https://${SHOP}/admin/api/${VER}`;
+const RATE_MS = 350; // ~3 req/s, under the 4 req/s bucket
+const argv = process.argv.slice(2);
+const DRY = argv.includes('--dry-run');
+const ACTIVATE = argv.includes('--activate');
+const RETITLE = argv.includes('--retitle');
+const LIMIT = (() => { const i = argv.indexOf('--limit'); return i > -1 ? parseInt(argv[i + 1], 10) : null; })();
+const ONLY = (() => { const i = argv.indexOf('--only'); return i > -1 ? new Set(argv[i + 1].split(',')) : null; })();
+
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+
+async function shopify(method, path, body) {
+ const res = await fetch(`${BASE}${path}`, {
+ method,
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+ body: body ? JSON.stringify(body) : undefined,
+ });
+ const text = await res.text();
+ let json; try { json = JSON.parse(text); } catch { json = { raw: text }; }
+ if (!res.ok) { const e = new Error(`${method} ${path} → ${res.status}: ${text.slice(0, 300)}`); e.status = res.status; throw e; }
+ return json;
+}
+
+const money = (n) => (Math.round(n * 100) / 100).toFixed(2);
+const perYard = (rollCost) => money((Number(rollCost) / 8) / 0.65 / 0.85);
+const titleOf = (colorName) => colorName.replace(/\s*-\s*/g, ' ').replace(/\s+/g, ' ').trim();
+
+function buildTags(row) {
+ const base = (row.tags || '').split(',').map(s => s.trim()).filter(Boolean);
+ const colorWord = (row.color_name || '').split(' ').pop();
+ const extra = ['Phillipe Romano', 'Wallcovering', 'Grasscloth & Naturals', 'Natural Texture',
+ row.material, `color:${colorWord}`, 'display_variant'];
+ return [...new Set([...base, ...extra].filter(Boolean))].join(', ');
+}
+
+function payload(row) {
+ const title = titleOf(row.color_name);
+ const price = perYard(row.price_retail);
+ return {
+ product: {
+ title,
+ body_html: row.ai_description ? `<p>${row.ai_description}</p>` : '',
+ vendor: 'Phillipe Romano',
+ product_type: 'Wallcovering',
+ status: 'draft',
+ handle: `pr-${row.dw_sku.toLowerCase()}`,
+ tags: buildTags(row),
+ options: [{ name: 'Size' }],
+ variants: [
+ { option1: 'Sold Per Yard', sku: row.dw_sku, price,
+ inventory_management: 'shopify', inventory_policy: 'continue', requires_shipping: true, taxable: true },
+ { option1: 'Sample', sku: `${row.dw_sku}-Sample`, price: '4.25',
+ inventory_management: 'shopify', inventory_policy: 'deny', requires_shipping: true, taxable: true },
+ ],
+ images: row.image_url ? [{ src: row.image_url, alt: title }] : [],
+ metafields: [
+ { namespace: 'dwc', key: 'source', type: 'single_line_text_field', value: 'wallquest-specialty-grasscloths-veneers' },
+ { namespace: 'dwc', key: 'mfr_sku', type: 'single_line_text_field', value: row.mfr_sku || '' },
+ { namespace: 'dwc', key: 'pricing_rule', type: 'single_line_text_field', value: '(roll_cost/8)/0.65/0.85 per yard' },
+ ],
+ },
+ };
+}
+
+async function setInv(variantId, locationId, qty) {
+ const v = await shopify('GET', `/variants/${variantId}.json`);
+ const iid = v.variant.inventory_item_id;
+ try { await shopify('POST', '/inventory_levels/connect.json', { location_id: locationId, inventory_item_id: iid }); } catch {}
+ await shopify('POST', '/inventory_levels/set.json', { location_id: locationId, inventory_item_id: iid, available: qty });
+}
+
+(async () => {
+ const db = new Client({ connectionString: DBURL });
+ await db.connect();
+
+ if (RETITLE) {
+ // Fix the 34 collapsed-title live products (Sisal x28 + Navy Grey & White x6)
+ // → title = "<pattern> - <color> | Phillipe Romano"
+ const { rows } = await db.query(
+ `SELECT 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 (color_name LIKE 'Sisal %' OR pattern_name='Navy Grey & White')
+ ORDER BY dw_sku`);
+ console.log(`${DRY ? '[DRY] ' : ''}Retitling ${rows.length} live products…`);
+ let ok = 0;
+ for (const r of rows) {
+ const title = `${r.pattern_name} - ${r.color_name} | Phillipe Romano`;
+ if (DRY) { console.log(` ${r.dw_sku} (#${r.shopify_product_id}) → "${title}"`); continue; }
+ try {
+ await shopify('PUT', `/products/${r.shopify_product_id}.json`,
+ { product: { id: Number(r.shopify_product_id), title } });
+ ok++; console.log(` ✓ ${r.dw_sku} → "${title}"`);
+ } catch (e) { console.log(` ✗ ${r.dw_sku}: ${e.message.slice(0, 160)}`); }
+ await sleep(RATE_MS);
+ }
+ console.log(`Retitled ${ok}/${rows.length}`);
+ await db.end(); return;
+ }
+
+ if (ACTIVATE) {
+ const { rows } = await db.query(
+ `SELECT dw_sku, shopify_product_id FROM phillipe_romano_catalog
+ WHERE created_at::date='2026-07-06' AND shopify_product_id IS NOT NULL ORDER BY dw_sku`);
+ console.log(`${DRY ? '[DRY] ' : ''}Activating ${rows.length} drafts → active…`);
+ let ok = 0;
+ for (const r of rows) {
+ if (DRY) { console.log(` would activate ${r.dw_sku} (#${r.shopify_product_id})`); continue; }
+ try {
+ await shopify('PUT', `/products/${r.shopify_product_id}.json`, { product: { id: Number(r.shopify_product_id), status: 'active' } });
+ ok++; console.log(` ✓ ${r.dw_sku} active`);
+ } catch (e) { console.log(` ✗ ${r.dw_sku}: ${e.message.slice(0, 160)}`); }
+ await sleep(RATE_MS);
+ }
+ console.log(`Activated ${ok}/${rows.length}`);
+ await db.end(); return;
+ }
+
+ let { rows } = await db.query(
+ `SELECT id, dw_sku, mfr_sku, color_name, material, tags, ai_description, image_url, price_retail
+ FROM phillipe_romano_catalog
+ WHERE created_at::date='2026-07-06' AND (shopify_product_id IS NULL)
+ ORDER BY dw_sku`);
+ if (ONLY) rows = rows.filter(r => ONLY.has(r.dw_sku));
+ if (LIMIT) rows = rows.slice(0, LIMIT);
+
+ console.log(`${DRY ? '[DRY] ' : ''}Pushing ${rows.length} as DRAFT → ${SHOP}`);
+ let locationId = null;
+ if (!DRY) locationId = (await shopify('GET', '/locations.json')).locations[0].id;
+
+ const created = [], failed = [];
+ for (let i = 0; i < rows.length; i++) {
+ const r = rows[i], p = payload(r), stamp = `[${String(i + 1).padStart(3, '0')}/${rows.length}]`;
+ if (DRY) {
+ console.log(`${stamp} ${r.dw_sku} "${p.product.title}" yard=$${p.product.variants[0].price} sample=$4.25 cost/roll=$${r.price_retail}`);
+ continue;
+ }
+ try {
+ const prod = (await shopify('POST', '/products.json', p)).product;
+ created.push({ id: r.id, sf: prod.id, handle: prod.handle });
+ console.log(`${stamp} ✓ ${r.dw_sku} "${prod.title}" → #${prod.id}`);
+ for (const v of prod.variants) { try { await setInv(v.id, locationId, 2026); await sleep(RATE_MS); } catch (e) { console.log(` inv ${v.id} fail: ${e.message.slice(0,120)}`); } }
+ await db.query(`UPDATE phillipe_romano_catalog SET shopify_product_id=$1, on_shopify=true, pl_updated_at=now() WHERE id=$2`, [prod.id, r.id]);
+ } catch (e) { failed.push({ dw_sku: r.dw_sku, err: e.message }); console.log(`${stamp} ✗ ${r.dw_sku}: ${e.message.slice(0, 200)}`); }
+ await sleep(RATE_MS);
+ }
+ console.log(`\nCreated ${created.length} Failed ${failed.length}`);
+ if (failed.length) console.log(JSON.stringify(failed, null, 2));
+ await db.end();
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
← 7c206ae0 wallquest login: check Remember-me → persistent session (one
·
back to Designer Wallcoverings
·
Add --reslug mode (34 clean handles) + read-only PR dedup au d6339904 →