[object Object]

← back to Majilite Onboard

publish_shopify: reconcile-in-place (DWMJ->DWCC SKU rename + add Per-Yard retail variant on 153 live products), collision-safe create for 7 missing (TK-10403)

abda2a6c3de953bce276be457f7e546cf237f67c · 2026-08-10 16:02:54 -0700 · Steve Abrams

Files touched

Diff

commit abda2a6c3de953bce276be457f7e546cf237f67c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 10 16:02:54 2026 -0700

    publish_shopify: reconcile-in-place (DWMJ->DWCC SKU rename + add Per-Yard retail variant on 153 live products), collision-safe create for 7 missing (TK-10403)
---
 scripts/publish_shopify.js | 76 +++++++++++++++++++++++++++++-----------------
 1 file changed, 48 insertions(+), 28 deletions(-)

diff --git a/scripts/publish_shopify.js b/scripts/publish_shopify.js
index 015a861..e3fa13e 100644
--- a/scripts/publish_shopify.js
+++ b/scripts/publish_shopify.js
@@ -99,42 +99,62 @@ function bodyHtml(p) {
     + `<li>Durability: ${s.durability}</li><li>Flammability: ${s.flammability_inherent}</li>`
     + `<li>Cleanability: ${s.cleanability_code} · Crocking ${s.crocking}</li>`
     + `<li>PFAS-, PVC- and plasticizer-free · no off-gassing</li></ul>`
-    + `<p><em>Sample swatch. Sold by the yard (54&quot; wide) — contact us for yardage pricing.</em></p>`;
+    + `<p><em>Available by the yard (54&quot; wide) at $${p.pricing.retail}/yd, or order a $4.25 sample swatch.</em></p>`;
 }
 
 async function publishOne(p) {
+  const num = p.dw_sku.replace('DWCC-', '');
+  const oldKey = `DWMJ-${num}`;             // key under which it was first published (153 of these)
+  const newKey = p.dw_sku;                  // DWCC-600xxx
   const sampleSku = `${p.dw_sku}-Sample`;
-  if (published[p.dw_sku] && published[p.dw_sku].productId) return { skip: 'recorded' };
-  const exists = await existingVariant(sampleSku);
-  if (exists) { published[p.dw_sku] = { productId: exists.product.id, handle: exists.product.handle, status: exists.product.status, note: 'pre-existing' }; savePub(); return { skip: 'exists' }; }
-  if (DRY) return { dry: true, title: p.title, sku: sampleSku };
+  const yardSku = p.dw_sku;
 
-  const handle = `majilite-${slugify(p.pattern + '-' + p.color)}`;
-  const tags = Array.from(new Set([...p.tags, p.dw_sku, 'display_variant', 'Sample'])).join(', ');
+  if (published[newKey] && published[newKey].reconciled) return { skip: 'done' };
+  const prior = published[oldKey];          // existing live DWMJ product, if any
+  const mode = (prior && prior.productId) ? 'update' : 'create';
+  if (DRY) return { dry: true, mode, title: p.title };
 
-  const input = {
+  const baseHandle = `majilite-${slugify(p.pattern + '-' + p.color)}`;
+  const tags = Array.from(new Set([...p.tags, p.dw_sku, 'display_variant', 'Sample', 'By The Yard']));
+  const buildInput = (handle) => ({
+    ...(mode === 'update' ? { id: prior.productId } : {}),
     handle, title: p.title, vendor: p.vendor, productType: p.product_type, status: STATUS,
-    descriptionHtml: bodyHtml(p), tags: tags.split(', '),
-    productOptions: [{ name: 'Type', values: [{ name: 'Sample' }] }],
-    variants: [{ optionValues: [{ optionName: 'Type', name: 'Sample' }], price: '4.25', sku: sampleSku, inventoryPolicy: 'CONTINUE', taxable: true }],
+    descriptionHtml: bodyHtml(p), tags,
+    productOptions: [{ name: 'Type', values: [{ name: 'Sample' }, { name: 'Per Yard' }] }],
+    variants: [
+      { optionValues: [{ optionName: 'Type', name: 'Sample' }], price: '4.25', sku: sampleSku, inventoryPolicy: 'CONTINUE', taxable: true },
+      { optionValues: [{ optionName: 'Type', name: 'Per Yard' }], price: String(p.pricing.retail), sku: yardSku, inventoryPolicy: 'CONTINUE', taxable: true },
+    ],
     metafields: metafields(p),
+  });
+  const run = async (handle) => {
+    const d = await gql(`mutation($input:ProductSetInput!){ productSet(synchronous:true, input:$input){ product{ id handle status } userErrors{ field message } } }`, { input: buildInput(handle) });
+    return d.productSet;
   };
-  const d = await gql(`mutation($input:ProductSetInput!){ productSet(synchronous:true, input:$input){ product{ id handle status } userErrors{ field message } } }`, { input });
-  const ue = d.productSet.userErrors; if (ue && ue.length) throw new Error('productSet: ' + JSON.stringify(ue));
-  const prod = d.productSet.product;
-
-  // attach swatch image
-  let media = 'none';
-  try {
-    const resourceUrl = await uploadImage(path.join(ROOT, 'public', p.image), `${p.dw_sku}.png`);
-    const m = await gql(`mutation($id:ID!,$media:[CreateMediaInput!]!){ productCreateMedia(productId:$id, media:$media){ media{ status } mediaUserErrors{ field message } } }`,
-      { id: prod.id, media: [{ originalSource: resourceUrl, alt: `${p.title} — ${p.vendor} ${p.brand}`, mediaContentType: 'IMAGE' }] });
-    const me = m.productCreateMedia.mediaUserErrors; media = (me && me.length) ? 'err:' + JSON.stringify(me) : 'ok';
-  } catch (e) { media = 'img-fail:' + e.message.slice(0, 80); }
-
-  published[p.dw_sku] = { productId: prod.id, handle: prod.handle, status: prod.status, sample_sku: sampleSku, image: media, at: new Date().toISOString() };
+
+  let ps = await run(baseHandle);
+  if (ps.userErrors && ps.userErrors.length) {
+    const collision = JSON.stringify(ps.userErrors).toLowerCase().includes('handle');
+    if (mode === 'create' && collision) ps = await run(`${baseHandle}-${num}`);   // collision-safe create
+    if (ps.userErrors && ps.userErrors.length) throw new Error('productSet: ' + JSON.stringify(ps.userErrors));
+  }
+  const prod = ps.product;
+
+  // image: creates get a fresh swatch; updates keep the swatch they already have
+  let media = 'kept';
+  if (mode === 'create') {
+    media = 'none';
+    try {
+      const resourceUrl = await uploadImage(path.join(ROOT, 'public', p.image), `${p.dw_sku}.png`);
+      const m = await gql(`mutation($id:ID!,$media:[CreateMediaInput!]!){ productCreateMedia(productId:$id, media:$media){ media{ status } mediaUserErrors{ field message } } }`,
+        { id: prod.id, media: [{ originalSource: resourceUrl, alt: `${p.title} — ${p.vendor} ${p.brand}`, mediaContentType: 'IMAGE' }] });
+      const me = m.productCreateMedia.mediaUserErrors; media = (me && me.length) ? 'err:' + JSON.stringify(me) : 'ok';
+    } catch (e) { media = 'img-fail:' + e.message.slice(0, 80); }
+  }
+
+  published[newKey] = { productId: prod.id, handle: prod.handle, status: prod.status, sample_sku: sampleSku, yard_sku: yardSku, retail: p.pricing.retail, mode, from: (mode === 'update' ? oldKey : 'new'), image: media, reconciled: true, at: new Date().toISOString() };
   savePub();
-  return { created: prod.id, handle: prod.handle, image: media };
+  return { [mode === 'update' ? 'updated' : 'created']: prod.id, handle: prod.handle, image: media, mode };
 }
 
 (async () => {
@@ -144,8 +164,8 @@ async function publishOne(p) {
     try {
       const r = await publishOne(p);
       if (r.skip) { skipped++; process.stdout.write(`· ${p.dw_sku} skip(${r.skip})\n`); }
-      else if (r.dry) { process.stdout.write(`plan ${p.dw_sku} ${p.title} → ${r.sku}\n`); }
-      else { created++; process.stdout.write(`✓ ${p.dw_sku} ${p.title} → ${r.handle} [img:${r.image}]  (${created})\n`); }
+      else if (r.dry) { process.stdout.write(`plan ${p.dw_sku} [${r.mode}] ${p.title}\n`); }
+      else { created++; const verb = r.updated ? '↑upd' : '✓new'; process.stdout.write(`${verb} ${p.dw_sku} ${p.title} → ${r.handle} [img:${r.image}]  (${created})\n`); }
     } catch (e) { failed++; process.stdout.write(`✗ ${p.dw_sku} ${p.title} — ${e.message.slice(0, 160)}\n`); }
     await sleep(350);
   }

← 2f00dac Majilite Metallic Specialties I: rename series DWMJ->DWCC (k  ·  back to Majilite Onboard  ·  auto-data-snapshot: 2026-08-10T16:09:17 (1 data files) — dat a6acf50 →