[object Object]

← back to Designer Wallcoverings

Set Versace line (111 SKUs: 11 VER- gaps + 100 DWVS-) to double-rolls-only on live Shopify + mirror

3839101fdb663b0fec7343b4c17eb4351788e5c6 · 2026-07-14 09:54:55 -0700 · Steve

Files touched

Diff

commit 3839101fdb663b0fec7343b4c17eb4351788e5c6
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jul 14 09:54:55 2026 -0700

    Set Versace line (111 SKUs: 11 VER- gaps + 100 DWVS-) to double-rolls-only on live Shopify + mirror
---
 DW-Programming/set-versace-doubleroll.js | 100 +++++++++++++++++++++++++++++++
 1 file changed, 100 insertions(+)

diff --git a/DW-Programming/set-versace-doubleroll.js b/DW-Programming/set-versace-doubleroll.js
new file mode 100644
index 00000000..c4136e0c
--- /dev/null
+++ b/DW-Programming/set-versace-doubleroll.js
@@ -0,0 +1,100 @@
+#!/usr/bin/env node
+/**
+ * Set Versace line to double-rolls-only.
+ * Targets: all ACTIVE DWVS-* (legacy) + the 11 enriched VER-* gaps missing the rule.
+ * Excludes: samples, DWVS-NaN-Bolt (broken SKUs), borders/panels (correctly single).
+ * Writes 3 global metafields via GraphQL metafieldsSet (upsert), then patches the
+ * local dw_unified mirror so viewers reflect it immediately.
+ *
+ * Usage:  node set-versace-doubleroll.js            (DRY RUN — prints plan, no writes)
+ *         node set-versace-doubleroll.js --live      (writes to live Shopify + mirror)
+ */
+require('dotenv').config({ path: require('os').homedir() + '/Projects/secrets-manager/.env' });
+const { Client } = require('pg');
+
+const LIVE = process.argv.includes('--live');
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-01';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const DB = process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified';
+const DELAY = 550;
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+const FIELDS = [
+  { key: 'packaged',                     value: 'Packaged in Double Rolls' },
+  { key: 'v_prods_quantity_order_min',   value: '2' },
+  { key: 'v_prods_quantity_order_units', value: '2' },
+];
+
+const TARGET_SQL = `
+  SELECT id, sku, shopify_id, title
+  FROM shopify_products
+  WHERE status='ACTIVE' AND sku NOT ILIKE '%-Sample' AND sku NOT ILIKE '%NaN%'
+    AND shopify_id LIKE 'gid://%'
+    AND (
+      (sku ILIKE 'DWVS-%')
+      OR (sku ILIKE 'VER-%' AND title NOT ILIKE '%border%' AND title NOT ILIKE '%panel%'
+          AND coalesce(metafields->'global'->'v_prods_quantity_order_min'->>'value','') <> '2')
+    )
+  ORDER BY sku`;
+
+async function metafieldsSet(ownerId) {
+  const mutation = `mutation($mf:[MetafieldsSetInput!]!){
+    metafieldsSet(metafields:$mf){ metafields{ id } userErrors{ field message } } }`;
+  const variables = { mf: FIELDS.map(f => ({
+    ownerId, namespace: 'global', key: f.key, type: 'single_line_text_field', value: f.value,
+  })) };
+  const res = await fetch(`https://${STORE}/admin/api/${API}/graphql.json`, {
+    method: 'POST',
+    headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+    body: JSON.stringify({ query: mutation, variables }),
+  });
+  const j = await res.json();
+  if (!res.ok) throw new Error(`HTTP ${res.status}: ${JSON.stringify(j).slice(0,200)}`);
+  const ue = j.data?.metafieldsSet?.userErrors;
+  if (ue && ue.length) throw new Error('userErrors: ' + JSON.stringify(ue));
+  return j.data.metafieldsSet.metafields.length;
+}
+
+async function patchMirror(client, id) {
+  // NOTE: deep-MERGE, not jsonb_set — jsonb_set won't create a missing intermediate
+  // {global} object (bare DWVS- rows have none), so it silently no-ops on those.
+  await client.query(`
+    UPDATE shopify_products SET metafields =
+      coalesce(metafields,'{}'::jsonb) || jsonb_build_object('global',
+        coalesce(metafields->'global','{}'::jsonb) || jsonb_build_object(
+          'packaged', jsonb_build_object('type','string','value','Packaged in Double Rolls'),
+          'v_prods_quantity_order_min', jsonb_build_object('type','string','value','2'),
+          'v_prods_quantity_order_units', jsonb_build_object('type','string','value','2')))
+    WHERE id = $1`, [ id ]);
+}
+
+async function main() {
+  if (LIVE && !TOKEN) { console.error('MISSING SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+  const client = new Client(DB); await client.connect();
+  const { rows } = await client.query(TARGET_SQL);
+  const dwvs = rows.filter(r => /^DWVS-/i.test(r.sku)).length;
+  const ver  = rows.length - dwvs;
+  console.log(`Mode: ${LIVE ? '🔴 LIVE' : '🟡 DRY RUN'} | targets: ${rows.length} (DWVS- ${dwvs}, VER- ${ver})`);
+  console.log(`Each gets: packaged="Packaged in Double Rolls", order_min=2, order_units=2`);
+  if (!LIVE) {
+    rows.slice(0,5).forEach(r => console.log(`   would set ${r.sku} (${r.shopify_id})`));
+    console.log(`   … and ${rows.length-5} more. Re-run with --live to write.`);
+    await client.end(); return;
+  }
+  let ok=0, err=0;
+  for (let i=0;i<rows.length;i++){
+    const r = rows[i];
+    try {
+      await metafieldsSet(r.shopify_id);
+      await patchMirror(client, r.id);
+      ok++;
+    } catch(e){ err++; console.log(`  ERROR ${r.sku}: ${String(e.message).slice(0,140)}`); }
+    if ((i+1)%20===0) console.log(`  ${i+1}/${rows.length} | ok ${ok} err ${err}`);
+    await sleep(DELAY);
+  }
+  await client.end();
+  console.log(`\n=== DONE === updated ${ok} products (${ok*3} metafields), ${err} errors`);
+  console.log(`Cost: $0 (Shopify Admin API + local PG)`);
+}
+main().catch(e => { console.error(e); process.exit(1); });

← deb759eb auto-save: 2026-07-14T09:30:17 (4 files) — DW-Programming/Im  ·  back to Designer Wallcoverings  ·  auto-save: 2026-07-14T10:00:24 (5 files) — DW-Programming/Im f124efc4 →