[object Object]

← back to Designerwallcoverings

TK-10484: activate 14 Jeffrey Stevens NuWallpaper drafts on live DW store (update-only, restore maps + rollback)

30f26d7e673817e29d76ee534194975aeced7673 · 2026-09-24 10:50:00 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSbNqkYWZiCRzvVe6mGz94

Files touched

Diff

commit 30f26d7e673817e29d76ee534194975aeced7673
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 24 10:50:00 2026 -0700

    TK-10484: activate 14 Jeffrey Stevens NuWallpaper drafts on live DW store (update-only, restore maps + rollback)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LSbNqkYWZiCRzvVe6mGz94
---
 ...tivate-14-restore-2026-09-24T17-47-29-249Z.json |  11 ++
 ...tivate-14-restore-2026-09-24T17-48-00-815Z.json | 119 +++++++++++++++++++++
 scripts/tk10484-activate-14-rollback.mjs           |  30 ++++++
 scripts/tk10484-activate-14.mjs                    |  80 ++++++++++++++
 4 files changed, 240 insertions(+)

diff --git a/data/tk10484-activate-14-restore-2026-09-24T17-47-29-249Z.json b/data/tk10484-activate-14-restore-2026-09-24T17-47-29-249Z.json
new file mode 100644
index 0000000..a80d396
--- /dev/null
+++ b/data/tk10484-activate-14-restore-2026-09-24T17-47-29-249Z.json
@@ -0,0 +1,11 @@
+[
+  {
+    "sku": "DWAA-506110-Roll",
+    "productId": "gid://shopify/Product/7884568035379",
+    "variantId": "gid://shopify/ProductVariant/44532023099443",
+    "oldStatus": "DRAFT",
+    "oldPrice": "4.25",
+    "hadNeedsPrice": true,
+    "wasPublished": false
+  }
+]
\ No newline at end of file
diff --git a/data/tk10484-activate-14-restore-2026-09-24T17-48-00-815Z.json b/data/tk10484-activate-14-restore-2026-09-24T17-48-00-815Z.json
new file mode 100644
index 0000000..fcc4351
--- /dev/null
+++ b/data/tk10484-activate-14-restore-2026-09-24T17-48-00-815Z.json
@@ -0,0 +1,119 @@
+[
+  {
+    "sku": "DWAA-506120-Roll",
+    "productId": "gid://shopify/Product/7884572688435",
+    "variantId": "gid://shopify/ProductVariant/44532035059763",
+    "oldStatus": "DRAFT",
+    "oldPrice": "4.25",
+    "hadNeedsPrice": true,
+    "wasPublished": false
+  },
+  {
+    "sku": "DWAA-506130-Roll",
+    "productId": "gid://shopify/Product/7884577472563",
+    "variantId": "gid://shopify/ProductVariant/44532044202035",
+    "oldStatus": "DRAFT",
+    "oldPrice": "4.25",
+    "hadNeedsPrice": true,
+    "wasPublished": false
+  },
+  {
+    "sku": "DWAA-506140-Roll",
+    "productId": "gid://shopify/Product/7884581601331",
+    "variantId": "gid://shopify/ProductVariant/44532053835827",
+    "oldStatus": "DRAFT",
+    "oldPrice": "4.25",
+    "hadNeedsPrice": true,
+    "wasPublished": false
+  },
+  {
+    "sku": "DWAA-506150-Roll",
+    "productId": "gid://shopify/Product/7884585238579",
+    "variantId": "gid://shopify/ProductVariant/44532063371315",
+    "oldStatus": "DRAFT",
+    "oldPrice": "4.25",
+    "hadNeedsPrice": true,
+    "wasPublished": false
+  },
+  {
+    "sku": "DWAA-506160-Roll",
+    "productId": "gid://shopify/Product/7884590710835",
+    "variantId": "gid://shopify/ProductVariant/44532074479667",
+    "oldStatus": "DRAFT",
+    "oldPrice": "4.25",
+    "hadNeedsPrice": true,
+    "wasPublished": false
+  },
+  {
+    "sku": "DWAA-506170-Roll",
+    "productId": "gid://shopify/Product/7884595494963",
+    "variantId": "gid://shopify/ProductVariant/44532099612723",
+    "oldStatus": "DRAFT",
+    "oldPrice": "4.25",
+    "hadNeedsPrice": true,
+    "wasPublished": false
+  },
+  {
+    "sku": "DWAA-506180-Roll",
+    "productId": "gid://shopify/Product/7884599230515",
+    "variantId": "gid://shopify/ProductVariant/44532104691763",
+    "oldStatus": "DRAFT",
+    "oldPrice": "4.25",
+    "hadNeedsPrice": true,
+    "wasPublished": false
+  },
+  {
+    "sku": "DWAA-506190-Roll",
+    "productId": "gid://shopify/Product/7884604899379",
+    "variantId": "gid://shopify/ProductVariant/44532128120883",
+    "oldStatus": "DRAFT",
+    "oldPrice": "4.25",
+    "hadNeedsPrice": true,
+    "wasPublished": false
+  },
+  {
+    "sku": "DWAA-506200-Roll",
+    "productId": "gid://shopify/Product/7884614107187",
+    "variantId": "gid://shopify/ProductVariant/44532161413171",
+    "oldStatus": "DRAFT",
+    "oldPrice": "4.25",
+    "hadNeedsPrice": true,
+    "wasPublished": false
+  },
+  {
+    "sku": "DWAA-506230-Roll",
+    "productId": "gid://shopify/Product/7884626395187",
+    "variantId": "gid://shopify/ProductVariant/44532206403635",
+    "oldStatus": "DRAFT",
+    "oldPrice": "4.25",
+    "hadNeedsPrice": true,
+    "wasPublished": false
+  },
+  {
+    "sku": "DWAA-506240-Roll",
+    "productId": "gid://shopify/Product/7884629344307",
+    "variantId": "gid://shopify/ProductVariant/44532220067891",
+    "oldStatus": "DRAFT",
+    "oldPrice": "4.25",
+    "hadNeedsPrice": true,
+    "wasPublished": false
+  },
+  {
+    "sku": "DWAA-506250-Roll",
+    "productId": "gid://shopify/Product/7884632752179",
+    "variantId": "gid://shopify/ProductVariant/44532224720947",
+    "oldStatus": "DRAFT",
+    "oldPrice": "4.25",
+    "hadNeedsPrice": true,
+    "wasPublished": false
+  },
+  {
+    "sku": "DWAA-506260-Roll",
+    "productId": "gid://shopify/Product/7884635144243",
+    "variantId": "gid://shopify/ProductVariant/44532229013555",
+    "oldStatus": "DRAFT",
+    "oldPrice": "4.25",
+    "hadNeedsPrice": true,
+    "wasPublished": false
+  }
+]
\ No newline at end of file
diff --git a/scripts/tk10484-activate-14-rollback.mjs b/scripts/tk10484-activate-14-rollback.mjs
new file mode 100644
index 0000000..0dbe3db
--- /dev/null
+++ b/scripts/tk10484-activate-14-rollback.mjs
@@ -0,0 +1,30 @@
+// TK-10484 rollback: revert products activated by tk10484-activate-14.mjs using its restore map(s).
+// Per product: unpublish from Online Store (if it wasn't published before), restore Roll variant
+// price, re-add Needs-Price tag (if it had it), status back to the old status (DRAFT).
+//   node scripts/tk10484-activate-14-rollback.mjs data/tk10484-activate-14-restore-*.json [--dry]
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+const DRY = process.argv.includes('--dry');
+const files = process.argv.slice(2).filter(a => a.endsWith('.json'));
+if (!files.length) { console.error('usage: rollback.mjs <restore.json>... [--dry]'); process.exit(1); }
+const API = 'https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
+const envTxt = fs.readFileSync(path.join(os.homedir(), 'Projects/secrets-manager/.env'), 'utf8');
+const TOKEN = (envTxt.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1]?.replace(/^["']|["']$/g, '').trim();
+async function gql(query, variables = {}) {
+  const r = await fetch(API, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }) });
+  if (!r.ok) throw new Error(`HTTP ${r.status}`);
+  const j = await r.json(); if (j.errors) throw new Error(JSON.stringify(j.errors)); return j.data;
+}
+const pub = (await gql(`{ publications(first: 25) { nodes { id name } } }`)).publications.nodes.find(p => p.name === 'Online Store').id;
+const entries = files.flatMap(f => JSON.parse(fs.readFileSync(f, 'utf8')));
+for (const e of entries) {
+  const steps = [`price->${e.oldPrice}`, `status->${e.oldStatus}`, e.hadNeedsPrice && 'tag+Needs-Price', !e.wasPublished && 'unpublish OnlineStore'].filter(Boolean).join(', ');
+  if (DRY) { console.log(`[dry] ${e.sku}: ${steps}`); continue; }
+  if (!e.wasPublished) await gql(`mutation($id: ID!, $pub: ID!) { publishableUnpublish(id: $id, input: {publicationId: $pub}) { userErrors { message } } }`, { id: e.productId, pub });
+  await gql(`mutation($pid: ID!, $v: [ProductVariantsBulkInput!]!) { productVariantsBulkUpdate(productId: $pid, variants: $v) { userErrors { message } } }`, { pid: e.productId, v: [{ id: e.variantId, price: e.oldPrice }] });
+  if (e.hadNeedsPrice) await gql(`mutation($id: ID!) { tagsAdd(id: $id, tags: ["Needs-Price"]) { userErrors { message } } }`, { id: e.productId });
+  await gql(`mutation($id: ID!, $s: ProductStatus!) { productUpdate(input: {id: $id, status: $s}) { userErrors { message } } }`, { id: e.productId, s: e.oldStatus });
+  console.log(`reverted ${e.sku}: ${steps}`);
+}
diff --git a/scripts/tk10484-activate-14.mjs b/scripts/tk10484-activate-14.mjs
new file mode 100644
index 0000000..648866d
--- /dev/null
+++ b/scripts/tk10484-activate-14.mjs
@@ -0,0 +1,80 @@
+// TK-10484: activate the 14 EXISTING Jeffrey Stevens NuWallpaper DRAFT products on the
+// LIVE DW store (designer-laboratory-sandbox.myshopify.com) — Steve approved "mint it" +
+// channel = Main DW Online Store (2026-09-24).
+// Per product: verify it is the expected DRAFT with the expected Roll SKU, snapshot to a
+// restore map, then set Roll variant price, drop the Needs-Price tag, status ACTIVE, and
+// publish to the Online Store. UPDATE only — never creates products.
+//
+//   node scripts/tk10484-activate-14.mjs --dry           # read-only: verify + print plan
+//   node scripts/tk10484-activate-14.mjs --limit 1       # canary
+//   node scripts/tk10484-activate-14.mjs                 # the rest (already-active are skipped)
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+const DRY = process.argv.includes('--dry');
+const li = process.argv.indexOf('--limit');
+const LIMIT = li > -1 ? parseInt(process.argv[li + 1], 10) : null;
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const API = `https://${STORE}/admin/api/2024-10/graphql.json`;
+const HERE = path.dirname(new URL(import.meta.url).pathname);
+const TARGETS = path.join(HERE, '../data/tk10484-activate-14-targets.csv');
+
+const envTxt = fs.readFileSync(path.join(os.homedir(), 'Projects/secrets-manager/.env'), 'utf8');
+const TOKEN = (envTxt.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1]?.replace(/^["']|["']$/g, '').trim();
+if (!TOKEN) { console.error('ABORT: SHOPIFY_ADMIN_TOKEN missing'); process.exit(1); }
+
+async function gql(query, variables = {}) {
+  const r = await fetch(API, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }) });
+  if (!r.ok) throw new Error(`HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);
+  const j = await r.json();
+  if (j.errors) throw new Error('GraphQL: ' + JSON.stringify(j.errors).slice(0, 300));
+  return j.data;
+}
+const userErr = (d, k) => { const e = d?.[k]?.userErrors; if (e && e.length) throw new Error(`${k}: ${JSON.stringify(e)}`); };
+
+const rows = fs.readFileSync(TARGETS, 'utf8').trim().split('\n').map(l => { const [sku, id, price] = l.split(','); return { sku, id, price: Number(price) }; });
+
+// Online Store publication id
+const pubs = await gql(`{ publications(first: 25) { nodes { id name } } }`);
+const onlineStore = pubs.publications.nodes.find(p => p.name === 'Online Store');
+if (!onlineStore) throw new Error('Online Store publication not found');
+
+const ts = new Date().toISOString().replace(/[:.]/g, '-');
+const restoreFile = path.join(HERE, `../data/tk10484-activate-14-restore-${ts}.json`);
+const restore = [];
+let done = 0, skipped = 0, failed = 0, acted = 0;
+
+for (const t of rows) {
+  if (LIMIT && acted >= LIMIT) break;
+  try {
+    const d = await gql(`query($id: ID!, $pub: ID!) { product(id: $id) { id title status tags publishedOnPublication(publicationId: $pub)
+      variants(first: 10) { nodes { id sku price } } } }`, { id: t.id, pub: onlineStore.id });
+    const p = d.product;
+    if (!p) { console.log(`  ${t.sku}: NOT FOUND ${t.id} — skip`); failed++; continue; }
+    const v = p.variants.nodes.find(x => x.sku === t.sku);
+    if (!v) { console.log(`  ${t.sku}: no variant with sku ${t.sku} (have ${p.variants.nodes.map(x => x.sku).join('/')}) — skip`); failed++; continue; }
+    if (p.status === 'ACTIVE' && Number(v.price) === t.price && p.publishedOnPublication) { console.log(`  ${t.sku}: already ACTIVE @ $${v.price} + published — skip`); skipped++; continue; }
+    if (p.status !== 'DRAFT') { console.log(`  ${t.sku}: status ${p.status}, expected DRAFT — skip (needs a human look)`); failed++; continue; }
+    const plan = `${p.status}->ACTIVE, $${v.price}->$${t.price.toFixed(2)}, tag Needs-Price ${p.tags.includes('Needs-Price') ? 'remove' : 'absent'}, OnlineStore ${p.publishedOnPublication ? 'already' : 'publish'}`;
+    if (DRY) { console.log(`  [dry] ${t.sku} "${p.title.slice(0, 50)}" — ${plan} (other variants: ${p.variants.nodes.filter(x => x.id !== v.id).map(x => `${x.sku}@${x.price}`).join(', ') || 'none'})`); acted++; continue; }
+
+    restore.push({ sku: t.sku, productId: p.id, variantId: v.id, oldStatus: p.status, oldPrice: v.price, hadNeedsPrice: p.tags.includes('Needs-Price'), wasPublished: p.publishedOnPublication });
+    fs.writeFileSync(restoreFile, JSON.stringify(restore, null, 2)); // restore map written BEFORE each write
+
+    userErr(await gql(`mutation($pid: ID!, $v: [ProductVariantsBulkInput!]!) { productVariantsBulkUpdate(productId: $pid, variants: $v) { userErrors { field message } } }`,
+      { pid: p.id, v: [{ id: v.id, price: t.price.toFixed(2) }] }), 'productVariantsBulkUpdate');
+    if (p.tags.includes('Needs-Price')) userErr(await gql(`mutation($id: ID!) { tagsRemove(id: $id, tags: ["Needs-Price"]) { userErrors { field message } } }`, { id: p.id }), 'tagsRemove');
+    userErr(await gql(`mutation($id: ID!) { productUpdate(input: {id: $id, status: ACTIVE}) { userErrors { field message } } }`, { id: p.id }), 'productUpdate');
+    if (!p.publishedOnPublication) userErr(await gql(`mutation($id: ID!, $pub: ID!) { publishablePublish(id: $id, input: {publicationId: $pub}) { userErrors { field message } } }`, { id: p.id, pub: onlineStore.id }), 'publishablePublish');
+
+    // read back
+    const c = (await gql(`query($id: ID!, $pub: ID!) { product(id: $id) { status handle publishedOnPublication(publicationId: $pub) tags variants(first: 10) { nodes { sku price } } } }`, { id: p.id, pub: onlineStore.id })).product;
+    const cv = c.variants.nodes.find(x => x.sku === t.sku);
+    const ok = c.status === 'ACTIVE' && Number(cv.price) === t.price && c.publishedOnPublication && !c.tags.includes('Needs-Price');
+    console.log(`  ${ok ? 'OK ' : 'MISMATCH'} ${t.sku} -> ${c.status} $${cv.price} published=${c.publishedOnPublication} https://designerwallcoverings.com/products/${c.handle}`);
+    ok ? done++ : failed++; acted++;
+  } catch (e) { console.log(`  ERR ${t.sku}: ${e.message}`); failed++; acted++; }
+}
+console.log(`\n${DRY ? '[dry] ' : ''}activated=${done} skipped=${skipped} failed=${failed}${DRY ? '' : ` restore=${restoreFile}`}`);
+if (failed) process.exit(3);

← 2c63e0a auto-data-snapshot: 2026-09-24T10:46:49 (1 data files) — dat  ·  back to Designerwallcoverings  ·  TK-12129 rollout: 320 Harlequin WC metafields written; live 3f78311 →