← back to Dw Yolo Loop
Artmura go-live: inventory=2026 both variants + publish to all 13 DW channels + status=ACTIVE
8f441ef92ab5098dcb2fb9c8485f330fe6bd433b · 2026-06-12 10:24:06 -0700 · Steve Abrams
- inventoryItemUpdate tracked + inventoryActivate + inventorySetQuantities on_hand=2026 @ Ventura Blvd location (5795643504)
- publishablePublish to all 13 publications (Online Store, POS, Google, FB/IG, Houzz, Fabricut, Pinterest, Rakuten, Shop, Inbox, TikTok, Buy Button, AutoPostBlog)
- productUpdate status ACTIVE; idempotent (skips already-active); canary verified (Ardesia Silver: active, 2026, 13 channels)
Files touched
A scripts/artmura-onboard/go-live-artmura.js
Diff
commit 8f441ef92ab5098dcb2fb9c8485f330fe6bd433b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 12 10:24:06 2026 -0700
Artmura go-live: inventory=2026 both variants + publish to all 13 DW channels + status=ACTIVE
- inventoryItemUpdate tracked + inventoryActivate + inventorySetQuantities on_hand=2026 @ Ventura Blvd location (5795643504)
- publishablePublish to all 13 publications (Online Store, POS, Google, FB/IG, Houzz, Fabricut, Pinterest, Rakuten, Shop, Inbox, TikTok, Buy Button, AutoPostBlog)
- productUpdate status ACTIVE; idempotent (skips already-active); canary verified (Ardesia Silver: active, 2026, 13 channels)
---
scripts/artmura-onboard/go-live-artmura.js | 106 +++++++++++++++++++++++++++++
1 file changed, 106 insertions(+)
diff --git a/scripts/artmura-onboard/go-live-artmura.js b/scripts/artmura-onboard/go-live-artmura.js
new file mode 100644
index 0000000..033f655
--- /dev/null
+++ b/scripts/artmura-onboard/go-live-artmura.js
@@ -0,0 +1,106 @@
+#!/usr/bin/env node
+/**
+ * Go-live for the 161 Artmura draft products. Per product:
+ * 1. enable inventory tracking on both variants (inventoryItemUpdate tracked:true)
+ * 2. activate at location + set on_hand = 2026 (inventoryActivate + inventorySetQuantities)
+ * 3. publish to all DW sales channels (publishablePublish, 13 publications)
+ * 4. set status = ACTIVE
+ *
+ * node go-live-artmura.js # dry-run summary
+ * node go-live-artmura.js --apply --limit=1 # canary
+ * node go-live-artmura.js --apply # all remaining drafts
+ *
+ * Idempotent-ish: skips products already status=active (re-runnable on failures).
+ */
+const fs = require('fs');
+const { Pool } = require('pg');
+
+const SHOP = process.env.SHOPIFY_STORE || 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const API = '2024-10';
+const EP = `https://${SHOP}/admin/api/${API}/graphql.json`;
+const DB = process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp';
+const APPLY = process.argv.includes('--apply');
+const LIMIT = parseInt((process.argv.find(a => a.startsWith('--limit=')) || '').split('=')[1] || '0', 10);
+
+const LOCATION_ID = 'gid://shopify/Location/5795643504'; // 15442 Ventura Blvd.
+const TARGET_QTY = 2026;
+const PUBLICATIONS = [
+ 22208643184, 22497296496, 29646651457, 29739483201, 29776969793, 37904089153,
+ 43657658419, 44234276915, 44317474867, 44317507635, 71898464307, 115856375859, 140027723827,
+].map(n => `gid://shopify/Publication/${n}`);
+
+if (!TOKEN) { console.error('FATAL: set SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const pool = new Pool({ connectionString: DB });
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+async function gql(query, variables = {}) {
+ for (let attempt = 0; attempt < 4; attempt++) {
+ try {
+ const res = await fetch(EP, { method: 'POST',
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query, variables }) });
+ const json = await res.json();
+ if (json.errors) { if (attempt === 3) throw new Error(JSON.stringify(json.errors)); await sleep(900 * (attempt + 1)); continue; }
+ const rem = json.extensions?.cost?.throttleStatus?.currentlyAvailable;
+ if (rem !== undefined && rem < 250) await sleep(600);
+ return json.data;
+ } catch (e) { if (attempt === 3) throw e; await sleep(900 * (attempt + 1)); }
+ }
+}
+
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status variants(first:10){edges{node{id sku inventoryItem{id}}}}}}`;
+const M_TRACK = `mutation($id:ID!){ inventoryItemUpdate(id:$id, input:{tracked:true}){ userErrors{message} } }`;
+const M_ACTIVATE = `mutation($iid:ID!,$loc:ID!){ inventoryActivate(inventoryItemId:$iid, locationId:$loc){ userErrors{message} } }`;
+const M_SETQTY = `mutation($input:InventorySetQuantitiesInput!){ inventorySetQuantities(input:$input){ userErrors{field message code} } }`;
+const M_PUBLISH = `mutation($id:ID!,$pubs:[PublicationInput!]!){ publishablePublish(id:$id, input:$pubs){ userErrors{message} } }`;
+const M_ACTIVE = `mutation($id:ID!){ productUpdate(input:{id:$id, status:ACTIVE}){ product{status} userErrors{message} } }`;
+
+async function goLive(pid) {
+ const gidP = `gid://shopify/Product/${pid}`;
+ const errs = [];
+ const d = await gql(Q_VARIANTS, { id: gidP });
+ if (d.product?.status === 'ACTIVE') return { skipped: true };
+ const items = d.product.variants.edges.map(e => e.node.inventoryItem.id);
+ // 1+2: track, activate, set 2026 per item
+ for (const iid of items) {
+ let r = await gql(M_TRACK, { id: iid }); (r.inventoryItemUpdate?.userErrors || []).forEach(e => errs.push('track:' + e.message));
+ r = await gql(M_ACTIVATE, { iid, loc: LOCATION_ID });
+ const ae = (r.inventoryActivate?.userErrors || []).filter(e => !/already/i.test(e.message));
+ ae.forEach(e => errs.push('activate:' + e.message));
+ }
+ const r2 = await gql(M_SETQTY, { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true,
+ quantities: items.map(iid => ({ inventoryItemId: iid, locationId: LOCATION_ID, quantity: TARGET_QTY })) } });
+ (r2.inventorySetQuantities?.userErrors || []).forEach(e => errs.push('setqty:' + e.message));
+ // 3: publish to all channels
+ const r3 = await gql(M_PUBLISH, { id: gidP, pubs: PUBLICATIONS.map(p => ({ publicationId: p })) });
+ (r3.publishablePublish?.userErrors || []).forEach(e => errs.push('publish:' + e.message));
+ // 4: activate product
+ const r4 = await gql(M_ACTIVE, { id: gidP });
+ (r4.productUpdate?.userErrors || []).forEach(e => errs.push('active:' + e.message));
+ return { status: r4.productUpdate?.product?.status, errs };
+}
+
+(async () => {
+ const { rows } = await pool.query(
+ `SELECT handle, dw_sku, shopify_product_id FROM newwall_catalog WHERE vendor_name='Artmura' AND shopify_product_id IS NOT NULL AND shopify_product_id<>'' ORDER BY dw_sku`);
+ let todo = rows;
+ if (LIMIT > 0) todo = todo.slice(0, LIMIT);
+ console.log(`go-live: ${todo.length} products · location ${LOCATION_ID} · qty ${TARGET_QTY} · channels ${PUBLICATIONS.length} · ${APPLY ? 'APPLY' : 'DRY-RUN'}`);
+ if (!APPLY) { console.log('first 3:', todo.slice(0, 3).map(r => r.dw_sku).join(', ')); console.log('DRY-RUN. --apply to execute.'); await pool.end(); return; }
+
+ let ok = 0, skip = 0, withErr = 0;
+ for (const r of todo) {
+ try {
+ const res = await goLive(r.shopify_product_id);
+ if (res.skipped) { skip++; }
+ else if (res.errs && res.errs.length) { withErr++; console.error(` ⚠ ${r.dw_sku} ${res.status||'?'}: ${res.errs.slice(0,3).join(' | ')}`); }
+ else { ok++; }
+ if (!res.skipped && res.status === 'ACTIVE') await pool.query(`UPDATE newwall_catalog SET on_shopify=true, updated_at=now() WHERE handle=$1`, [r.handle]);
+ if ((ok + skip + withErr) % 20 === 0) console.log(` ...${ok + skip + withErr}/${todo.length} (active=${ok} skip=${skip} err=${withErr})`);
+ await sleep(350);
+ } catch (e) { withErr++; console.error(` ERR ${r.dw_sku}: ${e.message.slice(0, 160)}`); await sleep(600); }
+ }
+ console.log(`\nDONE. active=${ok} skipped=${skip} with-errors=${withErr} of ${todo.length}`);
+ await pool.end();
+})().catch(e => { console.error(e); process.exit(1); });
← 52f8e55 Artmura titles: insert 'Wallcovering' → '<name> Wallcovering
·
back to Dw Yolo Loop
·
Vendor-landing template: config-driven + links to live Shopi 8489a63 →