← 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
A shopify/scripts/coleson-add-sample-variants.js
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 →