[object Object]

← back to Designer Wallcoverings

TK-10302: Knoll reprice — roll variant cost→cost/0.65/0.85 (607 products, sample-safe)

ffbfb192d03133ffadd586af809a7d4345fb931a · 2026-08-08 08:51:03 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit ffbfb192d03133ffadd586af809a7d4345fb931a
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Sat Aug 8 08:51:03 2026 -0700

    TK-10302: Knoll reprice — roll variant cost→cost/0.65/0.85 (607 products, sample-safe)
---
 shopify/scripts/knoll-reprice.js | 74 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 74 insertions(+)

diff --git a/shopify/scripts/knoll-reprice.js b/shopify/scripts/knoll-reprice.js
new file mode 100644
index 00000000..302c5954
--- /dev/null
+++ b/shopify/scripts/knoll-reprice.js
@@ -0,0 +1,74 @@
+#!/usr/bin/env node
+/**
+ * TK-10302 — Knoll reprice: roll variant priced AT COST (importer bug) → cost/0.65/0.85.
+ * Knoll is NOT Kravet-family, so standard DW markup applies (NOT MAP). Sample stays $4.25.
+ *
+ * SAFETY: only reprices a product's ROLL variant (sku NOT ending -Sample) whose CURRENT
+ * price ≈ its knoll_catalog cost (the bug signature). Any roll already priced != cost is
+ * SKIPPED (never touched). Never touches the Sample variant. Target is always > current.
+ *
+ *   node knoll-reprice.js            # DRY (all)
+ *   node knoll-reprice.js 3          # DRY, first 3 (canary preview)
+ *   node knoll-reprice.js 3 --commit # CANARY: commit 3
+ *   node knoll-reprice.js --commit   # commit all fixable
+ */
+const https = require('https'); const fs = require('fs'); const os = require('os');
+const { execFileSync } = require('child_process');
+const argN = process.argv.find(a => /^\d+$/.test(a));
+const N = argN ? parseInt(argN, 10) : Infinity;
+const COMMIT = process.argv.includes('--commit');
+const SAMPLE = 4.25;
+const COST_TOL = 0.02;                        // current price must be within 2¢ of cost to qualify
+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 < 6; 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 {}; }
+
+// Preload Knoll costs: dw_sku -> price_retail (raw cost)
+const costMap = {};
+for (const line of psql("SELECT dw_sku, price_retail FROM knoll_catalog WHERE price_retail>0;").split('\n')) {
+  const [sku, c] = line.split('\t'); if (sku) costMap[sku.trim()] = parseFloat(c);
+}
+
+(async () => {
+  if (!TOKEN) { console.error('no token'); process.exit(1); }
+  const Q = `query($after:String){products(first:60,query:"vendor:Knoll",after:$after){pageInfo{hasNextPage endCursor} edges{node{id title variants(first:6){edges{node{id sku price}}}}}}}`;
+  const prods = []; let after = null;
+  while (true) { const r = await gqlRetry(Q, { after }); const p = r?.data?.products; if (!p) break; for (const e of p.edges) prods.push(e.node); if (!p.pageInfo.hasNextPage) break; after = p.pageInfo.endCursor; }
+  console.log(`fetched ${prods.length} Knoll products live`);
+
+  const fixable = [], skip_priced = [], nocost = [];
+  for (const n of prods) {
+    const vs = (n.variants?.edges || []).map(e => e.node);
+    const roll = vs.find(v => v.sku && !/-sample$/i.test(v.sku));
+    if (!roll) continue;
+    const cost = costMap[roll.sku];
+    if (!cost) { nocost.push(roll.sku); continue; }
+    const cur = parseFloat(roll.price);
+    const target = Math.round((cost / 0.65 / 0.85) * 100) / 100;
+    if (Math.abs(cur - cost) <= COST_TOL && target > cur) fixable.push({ pid: n.id, roll, cost, cur, target, title: n.title });
+    else skip_priced.push({ sku: roll.sku, cur, cost });        // already != cost → leave alone
+  }
+  const list = Number.isFinite(N) ? fixable.slice(0, N) : fixable;
+  console.log(`fixable (roll priced at cost) ${fixable.length} | skip already-repriced/other ${skip_priced.length} | no-cost ${nocost.length}`);
+  list.slice(0, 6).forEach(t => console.log(`   ${t.roll.sku}  $${t.cur} (cost) → $${t.target}   ${t.title.slice(0,38)}`));
+  if (skip_priced.length) console.log(`   e.g. skipped: ${skip_priced.slice(0,4).map(s=>s.sku+'@$'+s.cur).join(', ')}`);
+  if (!COMMIT) { console.log(`\nDRY — --commit will reprice ${list.length} roll variants (Sample untouched).`); return; }
+
+  const M = `mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){productVariantsBulkUpdate(productId:$pid,variants:$variants){userErrors{field message}}}`;
+  let ok = 0, fail = 0;
+  for (const t of list) {
+    const r = await gqlRetry(M, { pid: t.pid, variants: [{ id: t.roll.id, price: String(t.target) }] });
+    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 % 25 === 0) process.stdout.write(`\r  repriced ${ok}/${list.length}…`); }
+  }
+  console.log(`\nDONE: repriced ${ok} roll variants, failed ${fail}. Sample variants untouched.`);
+})();

← 376199ed auto-data-snapshot: 2026-08-08T06:23:20 (2 data files) — sho  ·  back to Designer Wallcoverings  ·  auto-data-snapshot: 2026-08-08T15:15:29 (1 data files) — sho 1faa7077 →