[object Object]

← back to Designer Wallcoverings

Cole & Son: add mandatory $4.25 Sample variant to 26 products (skip 3 memo=NO)

ec49ab477bf7a6d54829a4baa3294ef0bc42ad1b · 2026-05-26 10:17:28 -0700 · SteveStudio2

Files touched

Diff

commit ec49ab477bf7a6d54829a4baa3294ef0bc42ad1b
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 26 10:17:28 2026 -0700

    Cole & Son: add mandatory $4.25 Sample variant to 26 products (skip 3 memo=NO)
---
 shopify/scripts/coleson-add-sample-variants.js | 110 +++++++++++++++++++++++++
 1 file changed, 110 insertions(+)

diff --git a/shopify/scripts/coleson-add-sample-variants.js b/shopify/scripts/coleson-add-sample-variants.js
new file mode 100644
index 00000000..19f444a7
--- /dev/null
+++ b/shopify/scripts/coleson-add-sample-variants.js
@@ -0,0 +1,110 @@
+#!/usr/bin/env node
+// Add the mandatory $4.25 Sample variant to Cole & Son products that have a
+// bolt/panel variant but NO Sample (standing DW rule: every product MUST have a
+// Sample variant). SKIPS any product whose legacy `global.Memo Sample Available`
+// metafield is "NO" — those cannot be fulfilled and must not offer a sample.
+//
+//   dry-run (default):  node coleson-add-sample-variants.js
+//   live:               node coleson-add-sample-variants.js --live
+//
+// Idempotent: re-derives the target set live each run and skips anything that
+// already has a Sample variant.
+
+const SHOP = process.env.SHOPIFY_STORE;
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const LIVE = process.argv.includes('--live');
+const ENDPOINT = `https://${SHOP}/admin/api/2024-10/graphql.json`;
+const SAMPLE_PRICE = '4.25';
+
+async function gql(query, variables = {}) {
+  for (let i = 0; i < 4; i++) {
+    try {
+      const r = await fetch(ENDPOINT, { method:'POST', headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'}, body: JSON.stringify({query, variables}) });
+      const j = await r.json();
+      if (j.errors) throw new Error(JSON.stringify(j.errors));
+      return j;
+    } catch (e) {
+      if (i === 3) throw e;
+      await new Promise(s => setTimeout(s, 500 * (i+1)));
+    }
+  }
+}
+
+const PAGE = `query P($c:String){products(first:100,after:$c,query:"vendor:\\"Cole & Son\\""){
+  edges{cursor node{
+    id title status
+    options{name optionValues{name}}
+    variants(first:15){edges{node{title sku price taxable inventoryItem{tracked}}}}
+    memo:metafield(namespace:"global",key:"Memo Sample Available"){value}
+  }}
+  pageInfo{hasNextPage endCursor}
+}}`;
+
+const CREATE = `mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){
+  productVariantsBulkCreate(productId:$pid, variants:$variants){
+    productVariants{ id title sku price }
+    userErrors{ field message code }
+  }
+}`;
+
+function baseSku(sku) {
+  const m = (sku||'').match(/^(DWKK-\d+)/);
+  return m ? m[1] : null;
+}
+
+(async () => {
+  let cursor = null, all = [];
+  while (true) {
+    const d = await gql(PAGE, { c: cursor });
+    for (const e of d.data.products.edges) all.push(e.node);
+    if (!d.data.products.pageInfo.hasNextPage) break;
+    cursor = d.data.products.pageInfo.endCursor;
+  }
+
+  const targets = [];
+  let skipMemoNo = 0, skipHasSample = 0, skipArchived = 0, skipNoBase = 0;
+  for (const p of all) {
+    const vs = p.variants.edges.map(e => e.node);
+    const hasSample = vs.some(v => v.title === 'Sample' || (v.sku||'').endsWith('-Sample'));
+    if (hasSample) { skipHasSample++; continue; }
+    if (p.status !== 'ACTIVE') { skipArchived++; continue; }
+    const memo = (p.memo && p.memo.value) ? p.memo.value.trim().toUpperCase() : null;
+    if (memo === 'NO') { skipMemoNo++; continue; }
+    const base = baseSku(vs[0] && vs[0].sku);
+    if (!base) { skipNoBase++; continue; }
+    const optName = (p.options[0] && p.options[0].name) || 'Title';
+    targets.push({ id: p.id, title: p.title, optName, sampleSku: `${base}-Sample`, modelTaxable: vs[0].taxable });
+  }
+
+  console.log(`mode=${LIVE ? 'LIVE' : 'DRY-RUN'}`);
+  console.log(`scanned=${all.length}  targets=${targets.length}`);
+  console.log(`skipped: hasSample=${skipHasSample} archived/draft=${skipArchived} memo=NO=${skipMemoNo} no-base-sku=${skipNoBase}`);
+
+  if (!LIVE) {
+    targets.slice(0, 30).forEach(t => console.log(`  + ${t.sampleSku}  [opt:${t.optName}=Sample $${SAMPLE_PRICE}]  ${t.title}`));
+    if (targets.length > 30) console.log(`  …+${targets.length-30} more`);
+    return;
+  }
+
+  let ok = 0, fail = 0;
+  for (const t of targets) {
+    const variants = [{
+      optionValues: [{ optionName: t.optName, name: 'Sample' }],
+      price: SAMPLE_PRICE,
+      taxable: t.modelTaxable,
+      inventoryItem: { tracked: false, requiresShipping: true, sku: t.sampleSku },
+      inventoryPolicy: 'CONTINUE',
+    }];
+    try {
+      const d = await gql(CREATE, { pid: t.id, variants });
+      const errs = d.data.productVariantsBulkCreate.userErrors || [];
+      const made = d.data.productVariantsBulkCreate.productVariants || [];
+      if (errs.length) { fail++; console.log(`  FAIL ${t.title}: ${JSON.stringify(errs.slice(0,2))}`); }
+      else { ok++; console.log(`  ok   ${made[0].sku} ($${made[0].price})  ${t.title}`); }
+    } catch (e) {
+      fail++; console.log(`  ERR  ${t.title}: ${String(e).slice(0,200)}`);
+    }
+    await new Promise(s => setTimeout(s, 200));
+  }
+  console.log(`\nDONE: ${ok} sample variants added, ${fail} failures`);
+})();

← b8cb44d6 Cole & Son: backfill spec metafields (874 products, 11362 mf  ·  back to Designer Wallcoverings  ·  grassclothwallpaper: add density slider beside sort + corner 1b25aa75 →