[object Object]

← 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

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 →