[object Object]

← back to Trending Dw

trending: scripts/push-to-shopify.js — creates DW products from DWPV queue as DRAFT (safe: refuses w/o creds, never auto-publishes); ready for token

bd1056c3eec0d954a7e1b4c9f9d62d86dad891a3 · 2026-07-03 09:34:08 -0700 · Steve

Files touched

Diff

commit bd1056c3eec0d954a7e1b4c9f9d62d86dad891a3
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Jul 3 09:34:08 2026 -0700

    trending: scripts/push-to-shopify.js — creates DW products from DWPV queue as DRAFT (safe: refuses w/o creds, never auto-publishes); ready for token
---
 scripts/push-to-shopify.js | 81 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 81 insertions(+)

diff --git a/scripts/push-to-shopify.js b/scripts/push-to-shopify.js
new file mode 100644
index 0000000..ed1be68
--- /dev/null
+++ b/scripts/push-to-shopify.js
@@ -0,0 +1,81 @@
+#!/usr/bin/env node
+// push-to-shopify.js — create real DW Shopify products from the staged DWPV queue.
+//
+// SAFE BY DEFAULT: refuses to run unless SHOPIFY_STORE + SHOPIFY_ADMIN_TOKEN are set,
+// and creates every product as status:'draft' (NOT published) so nothing goes
+// customer-facing without an explicit publish. Live publish stays a Steve-gated step.
+//
+//   env SHOPIFY_STORE=xxxx.myshopify.com SHOPIFY_ADMIN_TOKEN=shpat_… \
+//     node scripts/push-to-shopify.js            # push all 'staged'
+//   ... --dry                                     # print payloads, no API calls
+//
+// Idempotent: only pushes queue items with status==='staged'; on success flips them
+// to 'pushed' + records shopifyId + handle, so re-runs never double-create.
+
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+
+const QUEUE = path.join(__dirname, '..', 'data', 'shopify-queue.json');
+const STORE = process.env.SHOPIFY_STORE;                 // xxxx.myshopify.com
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;           // shpat_…
+const API_VER = process.env.SHOPIFY_API_VERSION || '2024-01';
+const DRY = process.argv.includes('--dry');
+
+if (!DRY && (!STORE || !TOKEN)){
+  console.error('REFUSING TO RUN: set SHOPIFY_STORE + SHOPIFY_ADMIN_TOKEN (route via /secrets). Use --dry to preview payloads.');
+  process.exit(2);
+}
+
+function priceFromTier(tier){                            // "Commercial — $495" -> "495.00"
+  const m = String(tier || '').match(/\$\s*([\d,]+)/);
+  return m ? m[1].replace(/,/g, '') + '.00' : '149.00';
+}
+
+function buildProduct(p){
+  return { product: {
+    title: p.title,
+    body_html: `<p>${p.style} · ${p.color}. Original Designer Wallcoverings pattern (Pattern Vault). Trend signal: ${p.trendSignal || ''} (rank ${p.signalRank || ''}).</p>`,
+    vendor: p.vendor || 'Designer Wallcoverings',
+    product_type: 'Wallcovering',
+    status: 'draft',                                     // never auto-publish
+    tags: [p.series, 'Pattern Vault', 'AI-Original', p.style, p.color].filter(Boolean).join(', '),
+    variants: [{ sku: p.sku, price: priceFromTier(p.priceTier), inventory_management: null }],
+    images: p.image ? [{ src: p.image }] : []
+  }};
+}
+
+function apiPost(bodyObj){
+  return new Promise((resolve, reject) => {
+    const body = JSON.stringify(bodyObj);
+    const req = https.request({
+      hostname: STORE, path: `/admin/api/${API_VER}/products.json`, method: 'POST',
+      headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN, 'Content-Length': Buffer.byteLength(body) }
+    }, res => {
+      let s = ''; res.on('data', d => s += d); res.on('end', () => {
+        if (res.statusCode >= 200 && res.statusCode < 300) resolve(JSON.parse(s));
+        else reject(new Error(`HTTP ${res.statusCode}: ${s.slice(0, 300)}`));
+      });
+    });
+    req.on('error', reject); req.write(body); req.end();
+  });
+}
+
+async function main(){
+  const q = JSON.parse(fs.readFileSync(QUEUE, 'utf8'));
+  const todo = q.filter(p => p.status === 'staged');
+  console.log(`${todo.length} staged product(s) to push${DRY ? ' (DRY RUN)' : ` to ${STORE} as DRAFT`}`);
+  let ok = 0;
+  for (const p of todo){
+    const payload = buildProduct(p);
+    if (DRY){ console.log(`  ${p.sku}  ${p.title}  $${payload.product.variants[0].price}`); continue; }
+    try {
+      const r = await apiPost(payload);
+      p.status = 'pushed'; p.shopifyId = r.product.id; p.handle = r.product.handle; p.pushedAt = new Date(Date.now()).toISOString();
+      ok++; console.log(`  ✓ ${p.sku} -> product ${r.product.id} (draft)`);
+    } catch (e){ console.error(`  ✗ ${p.sku}: ${e.message}`); }
+  }
+  if (!DRY){ fs.writeFileSync(QUEUE, JSON.stringify(q, null, 2)); console.log(`pushed ${ok}/${todo.length} as DRAFT — review + publish in Shopify admin.`); }
+}
+
+main().catch(e => { console.error(e); process.exit(1); });

← 732d40d trending: 'Put on Shopify' button — stages our-own designs a  ·  back to Trending Dw  ·  gap-lane generator: 3 seam-PASS settlement-OK tiles (TR-013/ bf548c9 →