[object Object]

← back to Designer Wallcoverings

Carl Robinson: Shopify push (draft) + activate/publish-all-channels scripts (env-only creds)

0eddd307db8dc87931f2fd29187f0f844fc996f8 · 2026-07-06 11:11:48 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit 0eddd307db8dc87931f2fd29187f0f844fc996f8
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Mon Jul 6 11:11:48 2026 -0700

    Carl Robinson: Shopify push (draft) + activate/publish-all-channels scripts (env-only creds)
---
 .../wallquest-refresh/load-db-carl-robinson.cjs    |  2 +-
 .../publish-channels-carl-robinson.cjs             | 58 ++++++++++++++++++
 .../push-shopify-carl-robinson.cjs                 | 70 ++++++++++++++++++++++
 3 files changed, 129 insertions(+), 1 deletion(-)

diff --git a/scripts/wallquest-refresh/load-db-carl-robinson.cjs b/scripts/wallquest-refresh/load-db-carl-robinson.cjs
index 92bb26da..2727be11 100644
--- a/scripts/wallquest-refresh/load-db-carl-robinson.cjs
+++ b/scripts/wallquest-refresh/load-db-carl-robinson.cjs
@@ -5,7 +5,7 @@
 const fs = require('fs');
 const { Client } = require('pg');
 
-const CONN = process.env.DATABASE_URL || 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/dw_unified';
+const CONN = process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified';
 const ENR = '/tmp/wq-carl-robinson-enriched.json';
 const NORM = '/tmp/wq-carl-robinson-normalized.json';
 const recs = JSON.parse(fs.readFileSync(fs.existsSync(ENR) ? ENR : NORM, 'utf8'));
diff --git a/scripts/wallquest-refresh/publish-channels-carl-robinson.cjs b/scripts/wallquest-refresh/publish-channels-carl-robinson.cjs
new file mode 100644
index 00000000..31d5441a
--- /dev/null
+++ b/scripts/wallquest-refresh/publish-channels-carl-robinson.cjs
@@ -0,0 +1,58 @@
+// Activate the 67 Carl Robinson → Phillipe Romano products and publish to ALL sales channels.
+// Reads SHOPIFY_ADMIN_TOKEN from env (no probing). Idempotent. Rate-limited.
+// Pre-req already verified: all 67 pass the DW activation gate (width + description + images).
+const https = require('https');
+const { Client } = require('pg');
+
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const CONN = process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function gql(query, variables) {
+  return new Promise((resolve, reject) => {
+    const body = JSON.stringify({ query, variables });
+    const req = https.request({ hostname: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST',
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: 40000 },
+      res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(new Error('bad json: ' + d.slice(0, 120))); } }); });
+    req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
+    req.write(body); req.end();
+  });
+}
+
+(async () => {
+  // 1) enumerate all sales channels (publications)
+  const pubRes = await gql('{ publications(first:25){edges{node{id name}}} }');
+  if (pubRes.errors) { console.error('publications error:', JSON.stringify(pubRes.errors).slice(0, 200)); process.exit(1); }
+  const pubs = pubRes.data.publications.edges.map(e => e.node);
+  console.log('sales channels:', pubs.map(p => p.name).join(', '));
+  const pubInput = pubs.map(p => ({ publicationId: p.id }));
+
+  // 2) load the 67 product ids
+  const db = new Client({ connectionString: CONN }); await db.connect();
+  const { rows } = await db.query("SELECT dw_sku, shopify_product_id FROM dw_sku_registry WHERE dw_sku LIKE 'DWCR-%' AND shopify_product_id IS NOT NULL ORDER BY dw_sku");
+  console.log(`activating + publishing ${rows.length} products to ${pubs.length} channels…`);
+
+  let act = 0, pub = 0, err = 0;
+  for (const r of rows) {
+    const gid = `gid://shopify/Product/${r.shopify_product_id}`;
+    try {
+      // activate
+      const a = await gql('mutation($id:ID!){ productUpdate(input:{id:$id, status:ACTIVE}){ product{id status} userErrors{message} } }', { id: gid });
+      const aErr = a.errors || a.data?.productUpdate?.userErrors?.length;
+      if (!aErr) act++;
+      // publish to all channels
+      const p = await gql('mutation($id:ID!,$in:[PublicationInput!]!){ publishablePublish(id:$id, input:$in){ userErrors{message} } }', { id: gid, in: pubInput });
+      const pErr = p.errors || p.data?.publishablePublish?.userErrors?.length;
+      if (!pErr) { pub++; await db.query("UPDATE dw_sku_registry SET status='active', updated_at=now() WHERE dw_sku=$1", [r.dw_sku]); }
+      else { err++; console.log(`✗ ${r.dw_sku} publish: ${JSON.stringify(p.errors || p.data?.publishablePublish?.userErrors).slice(0,140)}`); }
+      if (!aErr && !pErr) console.log(`✓ ${r.dw_sku} active + on ${pubs.length} channels`);
+    } catch (e) { err++; console.log(`✗ ${r.dw_sku} ${String(e).slice(0, 100)}`); }
+    await sleep(600);
+  }
+  await db.query("UPDATE carl_robinson_catalog SET on_shopify=true WHERE shopify_product_id IS NOT NULL");
+  console.log(`\nDONE: activated=${act} published=${pub} errors=${err} across channels: ${pubs.map(p=>p.name).join(', ')}`);
+  await db.end();
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
diff --git a/scripts/wallquest-refresh/push-shopify-carl-robinson.cjs b/scripts/wallquest-refresh/push-shopify-carl-robinson.cjs
new file mode 100644
index 00000000..f4a8239d
--- /dev/null
+++ b/scripts/wallquest-refresh/push-shopify-carl-robinson.cjs
@@ -0,0 +1,70 @@
+// Phase 8 — push Carl Robinson → Phillipe Romano drafts to Shopify as status:DRAFT.
+// DRAFT = invisible on storefront, reversible. Registers each SKU in dw_sku_registry,
+// records shopify_product_id back into carl_robinson_catalog. Rate-limited.
+// Env: LIMIT=1 to validate one first; DRAFTS=/path override.
+const fs = require('fs');
+const https = require('https');
+const { Client } = require('pg');
+
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const CONN = process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified';
+const drafts = JSON.parse(fs.readFileSync(process.env.DRAFTS || '/tmp/carl-robinson-PR-drafts.json', 'utf8'));
+const LIMIT = process.env.LIMIT ? parseInt(process.env.LIMIT, 10) : drafts.length;
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function shopify(method, path, body) {
+  return new Promise((resolve, reject) => {
+    const data = body ? JSON.stringify(body) : null;
+    const req = https.request({ hostname: STORE, path: `/admin/api/${API}${path}`, method,
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}) }, timeout: 40000 },
+      res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { const j = d ? JSON.parse(d) : {}; resolve({ status: res.statusCode, j }); } catch (e) { resolve({ status: res.statusCode, raw: d }); } }); });
+    req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
+    if (data) req.write(data); req.end();
+  });
+}
+
+function buildPayload(d) {
+  return { product: {
+    title: d.title, body_html: d.body_html, vendor: d.vendor, product_type: d.product_type,
+    status: 'draft',                                   // <-- reversible, not customer-visible
+    tags: d.tags.join(', '),
+    options: [{ name: 'Size' }],
+    variants: [
+      { option1: 'Sample', sku: `${d.dw_sku}-Sample`, price: '4.25', inventory_management: null, inventory_policy: 'deny', taxable: true },
+      { option1: 'Standard', sku: d.dw_sku, price: d.variants[1].price || '0.00', inventory_management: 'shopify', inventory_policy: 'continue', inventory_quantity: 2026, taxable: true },
+    ],
+    images: (d.images || []).slice(0, 20).map(src => ({ src })),
+    metafields: d.metafields.filter(m => m.value !== '' && m.value != null),
+  }};
+}
+
+(async () => {
+  const db = new Client({ connectionString: CONN }); await db.connect();
+  let ok = 0, fail = 0, skip = 0;
+  const work = drafts.slice(0, LIMIT);
+  for (const d of work) {
+    // dedup: skip if already registered/pushed
+    const reg = await db.query('SELECT shopify_product_id FROM dw_sku_registry WHERE dw_sku=$1', [d.dw_sku]);
+    if (reg.rows[0]?.shopify_product_id) { console.log(`skip ${d.dw_sku} (already on Shopify ${reg.rows[0].shopify_product_id})`); skip++; continue; }
+    try {
+      const { status, j, raw } = await shopify('POST', '/products.json', buildPayload(d));
+      if (status >= 200 && status < 300 && j.product) {
+        const pid = j.product.id, handle = j.product.handle;
+        await db.query(`INSERT INTO dw_sku_registry (dw_sku,vendor_prefix,vendor_name,mfr_sku,shopify_product_id,shopify_handle,status)
+          VALUES ($1,'DWCR-','Phillipe Romano',$2,$3,$4,'draft')
+          ON CONFLICT (dw_sku) DO UPDATE SET shopify_product_id=EXCLUDED.shopify_product_id, shopify_handle=EXCLUDED.shopify_handle, updated_at=now()`,
+          [d.dw_sku, d._mfr, String(pid), handle]);
+        await db.query('UPDATE carl_robinson_catalog SET shopify_product_id=$1, on_shopify=true, updated_at=now() WHERE mfr_sku=$2', [String(pid), d._mfr]);
+        ok++; console.log(`✓ ${d.dw_sku} → ${pid} (draft) "${d.title}" [${ok}/${work.length}]`);
+      } else {
+        fail++; console.log(`✗ ${d.dw_sku} HTTP ${status}: ${JSON.stringify(j?.errors || raw).slice(0, 160)}`);
+      }
+    } catch (e) { fail++; console.log(`✗ ${d.dw_sku} ${String(e).slice(0, 100)}`); }
+    await sleep(650); // standard plan 2 req/s
+  }
+  console.log(`\nPUSH DONE: created=${ok} failed=${fail} skipped=${skip} (status=draft on ${STORE})`);
+  await db.end();
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });

← 60fb5850 Carl Robinson E20 → Phillipe Romano onboard pipeline (scrape  ·  back to Designer Wallcoverings  ·  Onboard Daisy Bennett Natural Wallcoverings (WallQuest→Phill 532c47be →