[object Object]

← back to Designer Wallcoverings

Add schu-activate: gated DRAFT→ACTIVE for Schumacher (price+image+title gate, --limit test batch)

a49bb8e496ea675871695129a798efb5831396ba · 2026-06-11 13:10:41 -0700 · SteveStudio2

Files touched

Diff

commit a49bb8e496ea675871695129a798efb5831396ba
Author: SteveStudio2 <stevestudio2@SteveStudio2s-Mac-Studio.local>
Date:   Thu Jun 11 13:10:41 2026 -0700

    Add schu-activate: gated DRAFT→ACTIVE for Schumacher (price+image+title gate, --limit test batch)
---
 shopify/scripts/schu-activate.js | 58 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 58 insertions(+)

diff --git a/shopify/scripts/schu-activate.js b/shopify/scripts/schu-activate.js
new file mode 100644
index 00000000..769c297f
--- /dev/null
+++ b/shopify/scripts/schu-activate.js
@@ -0,0 +1,58 @@
+#!/usr/bin/env node
+/**
+ * Activate Schumacher DRAFT products (DRAFT → ACTIVE) — gated on quality.
+ *
+ * Only flips a product to ACTIVE if it passes the gate: status currently DRAFT, has a
+ * cost recorded, a sane retail price (variant price > sample), a featured image, and a
+ * non-empty title. Skips anything missing those. Reversible (flip back to DRAFT).
+ *
+ *   node schu-activate.js            # dry: report how many pass the gate
+ *   node schu-activate.js --commit
+ *   node schu-activate.js --limit 50 --commit   # activate a test batch first
+ */
+const https = require('https'); const fs = require('fs'); const os = require('os');
+const { execFileSync } = require('child_process');
+const args = process.argv.slice(2);
+const COMMIT = args.includes('--commit');
+const lIdx = args.indexOf('--limit'); const LIMIT = lIdx >= 0 ? parseInt(args[lIdx + 1], 10) : 100000;
+const TOKEN = (fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/g, '').trim();
+const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+function psql(s) { return execFileSync('psql', ['-At', '-F', '\t', '-d', 'dw_unified', '-c', s], { encoding: 'utf8', maxBuffer: 1 << 28 }).trim(); }
+
+function gql(query, variables) {
+  return new Promise(res => { const data = JSON.stringify({ query, variables });
+    const req = https.request({ host: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
+      r => { let d = ''; r.on('data', c => d += c); r.on('end', () => { try { res(JSON.parse(d)); } catch { res({}); } }); });
+    req.on('error', () => res({})); req.write(data); req.end(); });
+}
+async function gqlRetry(q, v) { for (let a = 0; a < 5; a++) { const r = await gql(q, v); if (r.errors && JSON.stringify(r.errors).includes('THROTTLED')) { await sleep(2000 * (a + 1)); continue; } await sleep(300); return r; } return {}; }
+
+(async () => {
+  if (!TOKEN) { console.error('no token'); process.exit(1); }
+  const rows = psql(`SELECT shopify_product_id FROM schumacher_catalog
+    WHERE shopify_product_id IS NOT NULL AND shopify_product_id<>'' AND cost>0
+    AND NOT coalesce(excluded,false) ORDER BY shopify_product_id LIMIT ${LIMIT};`).split('\n').filter(Boolean);
+  console.log(`candidate Schumacher products (costed, not excluded): ${rows.length} | mode: ${COMMIT ? 'COMMIT' : 'DRY'}`);
+
+  const Q = `query($id:ID!){product(id:$id){id status title featuredImage{url} variants(first:2){edges{node{price}}}}}`;
+  const mUpd = `mutation($input:ProductInput!){productUpdate(input:$input){product{id status} userErrors{field message}}}`;
+  let pass = 0, skip = 0, activated = 0, fail = 0, already = 0, n = 0;
+  for (const pid of rows) {
+    const gid = `gid://shopify/Product/${pid}`;
+    const r = await gqlRetry(Q, { id: gid });
+    const p = r?.data?.product; if (!p) { skip++; continue; }
+    if (p.status === 'ACTIVE') { already++; continue; }
+    const prices = (p.variants?.edges || []).map(e => parseFloat(e.node.price)).filter(x => x > 0);
+    const maxPrice = Math.max(0, ...prices);
+    const okGate = p.status === 'DRAFT' && p.title && p.title.trim() && p.featuredImage?.url && maxPrice > 5; // > sample $4.25
+    if (!okGate) { skip++; if (n < 8) { console.log(`  skip ${pid}  status=${p.status} img=${!!p.featuredImage} price=${maxPrice} title="${(p.title||'').slice(0,30)}"`); n++; } continue; }
+    pass++;
+    if (!COMMIT) { if (pass <= 6) console.log(`  would activate ${pid}  $${maxPrice}  "${p.title.slice(0,42)}"`); continue; }
+    const u = await gqlRetry(mUpd, { input: { id: gid, status: 'ACTIVE' } });
+    const ue = u?.data?.productUpdate?.userErrors;
+    if (ue && ue.length) { fail++; console.log(`  ✗ ${pid} ${JSON.stringify(ue).slice(0, 90)}`); }
+    else { activated++; if (activated % 25 === 0) process.stdout.write(`\r  activated ${activated}…`); }
+  }
+  console.log(`\n${COMMIT ? `DONE: activated ${activated}` : `DRY: ${pass} would activate`} | gate-skipped ${skip} | already-active ${already}${COMMIT ? ` | failed ${fail}` : ''}`);
+})();

← ac43bae8 Fix accented-title casing (Unicode \p{L}) + re-title 46 Schu  ·  back to Designer Wallcoverings  ·  schu-activate: normalize mixed pid format (bare-id vs full-g e5cc2b10 →