[object Object]

← back to Designer Wallcoverings

DWAE: price 166 from Schumacher net-cost API + activate + tag Trending Wallpaper 2026

0d278a72ae27a33de0659970b2dca66c4aca8bc9 · 2026-06-12 07:43:48 -0700 · SteveStudio2

Files touched

Diff

commit 0d278a72ae27a33de0659970b2dca66c4aca8bc9
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Fri Jun 12 07:43:48 2026 -0700

    DWAE: price 166 from Schumacher net-cost API + activate + tag Trending Wallpaper 2026
---
 shopify/scripts/launch-dwae.js | 54 ++++++++++++++++++++++++
 shopify/scripts/price-dwae.js  | 95 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 149 insertions(+)

diff --git a/shopify/scripts/launch-dwae.js b/shopify/scripts/launch-dwae.js
new file mode 100644
index 00000000..7d489dcb
--- /dev/null
+++ b/shopify/scripts/launch-dwae.js
@@ -0,0 +1,54 @@
+#!/usr/bin/env node
+/**
+ * Activate + tag the DWAE-* products that now pass the quality gate (2 variants, roll>sample,
+ * image, title). Tag "Trending Wallpaper 2026". Idempotent.
+ *   node launch-dwae.js            # AUDIT
+ *   node launch-dwae.js --commit
+ */
+const https = require('https'); const fs = require('fs'); const os = require('os');
+const COMMIT = process.argv.includes('--commit');
+const TAG = 'Trending Wallpaper 2026'; const SAMPLE = 4.25;
+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 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 < 8; a++) { const r = await gql(q, v); if (r.errors && JSON.stringify(r.errors).includes('THROTTLED')) { await sleep(2500 * (a + 1)); continue; } await sleep(300); return r; } return {}; }
+function audit(n) {
+  const vs = (n.variants?.edges || []).map(e => e.node); const issues = [];
+  if (vs.length !== 2) issues.push(`variants=${vs.length}`);
+  if (vs.some(v => !v.sku || !v.sku.trim())) issues.push('empty-sku');
+  if (!vs.some(v => Math.abs(parseFloat(v.price) - SAMPLE) < 0.01 || /-sample$/i.test(v.sku || ''))) issues.push('no-sample');
+  if (!vs.some(v => parseFloat(v.price) > SAMPLE + 0.5)) issues.push('no-roll-price');
+  if (!n.featuredImage?.url) issues.push('no-image');
+  if (!n.title || !n.title.trim()) issues.push('no-title');
+  return issues;
+}
+(async () => {
+  if (!TOKEN) { console.error('no token'); process.exit(1); }
+  const Q = `query($after:String){products(first:50,query:"sku:DWAE-*",after:$after){pageInfo{hasNextPage endCursor} edges{node{id title status featuredImage{url} tags variants(first:5){edges{node{sku price}}}}}}}`;
+  const prods = []; let after = null;
+  while (true) { const r = await gqlRetry(Q, { after }); const p = r?.data?.products; if (!p) { console.error('fetch fail'); break; } for (const e of p.edges) prods.push(e.node); if (!p.pageInfo.hasNextPage) break; after = p.pageInfo.endCursor; }
+  const pass = [], failed = [];
+  for (const n of prods) { const iss = audit(n); (iss.length ? failed : pass).push({ n, iss }); }
+  const active = prods.filter(n => n.status === 'ACTIVE').length;
+  console.log(`DWAE ${prods.length} | PASS ${pass.length} | FAIL ${failed.length} | currently ACTIVE ${active}/DRAFT ${prods.length-active}`);
+  const byIssue = {}; failed.forEach(f => f.iss.forEach(i => byIssue[i]=(byIssue[i]||0)+1)); if (failed.length) console.log('  fail reasons:', JSON.stringify(byIssue));
+  failed.slice(0,8).forEach(f => console.log(`   ✗ [${f.iss.join(',')}] ${f.n.title.slice(0,44)}`));
+  if (!COMMIT) { console.log(`\nAUDIT — --commit would activate + tag the ${pass.length} passing.`); return; }
+  const mUpd = `mutation($input:ProductInput!){productUpdate(input:$input){product{id status} userErrors{field message}}}`;
+  const mTag = `mutation($id:ID!,$tags:[String!]!){tagsAdd(id:$id,tags:$tags){userErrors{field message}}}`;
+  let act = 0, tagged = 0, fail = 0;
+  for (const { n } of pass) {
+    let err = null;
+    if (n.status !== 'ACTIVE') { const r = await gqlRetry(mUpd, { input: { id: n.id, status: 'ACTIVE' } }); const e = r?.data?.productUpdate?.userErrors; if (e && e.length) err = e; else act++; }
+    if (!(n.tags || []).includes(TAG)) { const r = await gqlRetry(mTag, { id: n.id, tags: [TAG] }); const e = r?.data?.tagsAdd?.userErrors; if (e && e.length) err = err || e; else tagged++; }
+    if (err) { fail++; console.log(`  ✗ ${n.id} ${JSON.stringify(err).slice(0,90)}`); }
+    if ((act + tagged) % 30 === 0) process.stdout.write(`\r  activated ${act} / tagged ${tagged}…`);
+  }
+  console.log(`\nDONE: activated ${act}, tagged ${tagged} (passing ${pass.length}), failed ${fail}.`);
+})();
diff --git a/shopify/scripts/price-dwae.js b/shopify/scripts/price-dwae.js
new file mode 100644
index 00000000..31ae77e9
--- /dev/null
+++ b/shopify/scripts/price-dwae.js
@@ -0,0 +1,95 @@
+#!/usr/bin/env node
+/**
+ * Price the unpriced DWAE-* products by sourcing net cost from the authenticated Schumacher
+ * API (api.schumacher.com/products/{manufacturer_sku}). priceUsd IS our net cost (Designer Net,
+ * discount 0.00) → roll price = round(priceUsd/0.65/0.85, 2). Sample variant stays $4.25.
+ *
+ * Resolution per product: read custom.manufacturer_sku metafield → look up cost in local
+ * schumacher_catalog.mfr_sku first (no API call) → fall back to live API with .schu-token.
+ *
+ *   node price-dwae.js            # DRY — report what would be priced
+ *   node price-dwae.js --commit   # set roll variant prices
+ */
+const https = require('https'); const fs = require('fs'); const os = require('os');
+const { execFileSync } = require('child_process');
+const COMMIT = process.argv.includes('--commit');
+const SAMPLE = 4.25;
+const DIR = __dirname;
+const TOKEN = (fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/g, '').trim();
+const JWT = fs.readFileSync(DIR + '/.schu-token', 'utf8').replace(/[\n\r ]/g, '');
+const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+function psql(s) { try { return execFileSync('psql', ['-At', '-d', 'dw_unified', '-c', s], { encoding: 'utf8', maxBuffer: 1 << 28 }).trim(); } catch { return ''; } }
+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 < 8; a++) { const r = await gql(q, v); if (r.errors && JSON.stringify(r.errors).includes('THROTTLED')) { await sleep(2500 * (a + 1)); continue; } await sleep(300); return r; } return {}; }
+function schuApi(item) {
+  return new Promise(res => {
+    const req = https.request({ host: 'api.schumacher.com', path: `/products/${encodeURIComponent(item)}?skipAnalytics=true`, method: 'GET', headers: { 'Authorization': `Bearer ${JWT}`, 'accept': 'application/json' } },
+      r => { let d = ''; r.on('data', c => d += c); r.on('end', () => { try { const j = JSON.parse(d); res(typeof j.priceUsd === 'number' ? j.priceUsd : null); } catch { res(null); } }); });
+    req.on('error', () => res(null)); req.end();
+  });
+}
+
+(async () => {
+  if (!TOKEN) { console.error('no shopify token'); process.exit(1); }
+  if (!JWT || JWT.length < 50) { console.error('no schu token'); process.exit(1); }
+
+  // 1. Pull all DWAE-* products via variant SKU search, capturing manufacturer_sku metafield.
+  const Q = `query($after:String){products(first:50,query:"sku:DWAE-*",after:$after){pageInfo{hasNextPage endCursor} edges{node{id title status
+      variants(first:5){edges{node{id sku price}}}
+      metafields(first:30){edges{node{namespace key value}}}}}}}`;
+  const prods = []; let after = null;
+  while (true) { const r = await gqlRetry(Q, { after }); const p = r?.data?.products; if (!p) { console.error('fetch fail', JSON.stringify(r).slice(0,200)); break; }
+    for (const e of p.edges) prods.push(e.node); if (!p.pageInfo.hasNextPage) break; after = p.pageInfo.endCursor; }
+  console.log(`DWAE products found: ${prods.length}`);
+
+  // 2. Keep those with an unpriced roll variant.
+  const targets = [];
+  for (const n of prods) {
+    const vs = (n.variants?.edges || []).map(e => e.node);
+    const roll = vs.find(v => !/-sample$/i.test(v.sku || '') && parseFloat(v.price) <= SAMPLE + 0.5);
+    if (!roll) continue;
+    const mf = {}; (n.metafields?.edges || []).forEach(e => mf[`${e.node.namespace}.${e.node.key}`] = e.node.value);
+    const item = mf['custom.manufacturer_sku'] || mf['specs.sku'] || ((mf['import.source_url']||'').match(/schumacher\.com\/(\d+)/)||[])[1] || null;
+    const vendor = mf['custom.real_vendor'] || mf['private_label.real_vendor_name'] || mf['specs.brand'] || '?';
+    targets.push({ n, roll, item, vendor });
+  }
+  const schu = targets.filter(t => /schumacher/i.test(t.vendor) && t.item && /^\d+$/.test(t.item));
+  const other = targets.filter(t => !(/schumacher/i.test(t.vendor) && t.item && /^\d+$/.test(t.item)));
+  console.log(`unpriced-roll DWAE: ${targets.length} | Schumacher-with-item ${schu.length} | other/no-item ${other.length}`);
+  if (other.length) { const byv = {}; other.forEach(t => byv[t.vendor]=(byv[t.vendor]||0)+1); console.log('  non-schu/no-item:', JSON.stringify(byv)); }
+
+  // 3. Resolve cost: local schumacher_catalog.mfr_sku first, then live API.
+  let priced = [], nocost = [], localHit = 0, apiHit = 0;
+  for (const t of schu) {
+    let cost = null;
+    const loc = psql(`SELECT cost FROM schumacher_catalog WHERE mfr_sku='${t.item}' AND cost>0 LIMIT 1;`);
+    const lv = parseFloat(loc); if (!isNaN(lv) && lv > 0) { cost = lv; localHit++; }
+    if (cost == null) { const a = await schuApi(t.item); if (a && a > 0) { cost = a; apiHit++; } await sleep(150); }
+    if (cost == null) { nocost.push(t); continue; }
+    const retail = Math.round((cost / 0.65 / 0.85) * 100) / 100;
+    if (retail <= SAMPLE) { nocost.push(t); continue; }
+    priced.push({ ...t, cost, retail });
+  }
+  console.log(`cost resolved: ${priced.length} (local ${localHit}, api ${apiHit}) | no-cost ${nocost.length}`);
+  priced.slice(0, 8).forEach(t => console.log(`   ${t.roll.sku}  item ${t.item}  cost $${t.cost} → roll $${t.retail}  ${t.n.title.slice(0,38)}`));
+  nocost.slice(0, 8).forEach(t => console.log(`   ✗ no-cost ${t.roll.sku}  item ${t.item}  ${t.n.title.slice(0,38)}`));
+
+  if (!COMMIT) { console.log(`\nDRY — --commit sets roll price on ${priced.length} products. (${nocost.length} no-cost stay unpriced.)`); return; }
+
+  // 4. Set roll variant price.
+  const M = `mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){productVariantsBulkUpdate(productId:$pid,variants:$variants){userErrors{field message}}}`;
+  let ok = 0, fail = 0;
+  for (const t of priced) {
+    const r = await gqlRetry(M, { pid: t.n.id, variants: [{ id: t.roll.id, price: String(t.retail) }] });
+    const e = r?.data?.productVariantsBulkUpdate?.userErrors;
+    if (e && e.length) { fail++; console.log(`  ✗ ${t.roll.sku} ${JSON.stringify(e).slice(0,90)}`); }
+    else { ok++; if (ok % 15 === 0) process.stdout.write(`\r  priced ${ok}/${priced.length}…`); }
+  }
+  console.log(`\nDONE: roll-priced ${ok}, failed ${fail}. ${nocost.length} still no-cost.`);
+})();

← 76b49046 Launch latest 300: activate + tag 'Trending Wallpaper 2026'  ·  back to Designer Wallcoverings  ·  cadence: support MAP-priced vendors (sellExpr) — Kravet fami 4a502d3c →