[object Object]

← back to Dw Sku Integrity

TK-11002: fix live write — productVariantUpdate removed in API 2024-10; use productVariantsBulkUpdate(productId,variants) with inventoryItem.sku, grouped per product; rollback path too. Schema-validated + dry-run clean.

233fb7fa6e7ddc6977288e7283200cb108034df2 · 2026-08-31 09:53:11 -0700 · steve

Files touched

Diff

commit 233fb7fa6e7ddc6977288e7283200cb108034df2
Author: steve <steve@designerwallcoverings.com>
Date:   Mon Aug 31 09:53:11 2026 -0700

    TK-11002: fix live write — productVariantUpdate removed in API 2024-10; use productVariantsBulkUpdate(productId,variants) with inventoryItem.sku, grouped per product; rollback path too. Schema-validated + dry-run clean.
---
 tk11002-harlequin-prefix-fix.mjs | 52 +++++++++++++++++++++++++---------------
 1 file changed, 33 insertions(+), 19 deletions(-)

diff --git a/tk11002-harlequin-prefix-fix.mjs b/tk11002-harlequin-prefix-fix.mjs
index 647701b..5a11173 100644
--- a/tk11002-harlequin-prefix-fix.mjs
+++ b/tk11002-harlequin-prefix-fix.mjs
@@ -86,24 +86,33 @@ async function runForward() {
 
   if (!APPLY) { console.log('\nDRY-RUN — no writes. Re-run with --apply (Steve `!`) to execute.'); return; }
 
-  console.log('\n--apply: writing LIVE (Shopify first, then mirror, CAS-guarded)...');
+  console.log('\n--apply: writing LIVE (Shopify productVariantsBulkUpdate per product, then mirror, CAS-guarded)...');
   let ok = 0, skip = 0;
-  for (const p of plan) {
-    // CAS: re-read this variant live; abort this variant if it drifted off DWHQ-
-    const chk = await gql(`query($id:ID!){ productVariant(id:$id){ id sku } }`, { id: p.variant });
-    if (!chk.productVariant || chk.productVariant.sku !== p.old_sku) { console.log(`  CAS-drift SKIP ${p.old_sku}`); skip++; continue; }
+  const byProduct = new Map();
+  for (const p of plan) { if (!byProduct.has(p.product)) byProduct.set(p.product, []); byProduct.get(p.product).push(p); }
+  for (const [productId, items] of byProduct) {
+    // CAS: re-read each variant live; only include those still on DWHQ- (SKU set via inventoryItem.sku in 2024-10)
+    const variants = [];
+    for (const p of items) {
+      const chk = await gql(`query($id:ID!){ productVariant(id:$id){ id sku } }`, { id: p.variant });
+      if (!chk.productVariant || chk.productVariant.sku !== p.old_sku) { console.log(`  CAS-drift SKIP ${p.old_sku}`); skip++; continue; }
+      variants.push({ id: p.variant, inventoryItem: { sku: p.new_sku } });
+    }
+    if (!variants.length) continue;
     const res = await gql(
-      `mutation($input:ProductVariantInput!){ productVariantUpdate(input:$input){ productVariant{ id sku } userErrors{ field message } } }`,
-      { input: { id: p.variant, sku: p.new_sku } }
+      `mutation($productId:ID!,$variants:[ProductVariantsBulkInput!]!){ productVariantsBulkUpdate(productId:$productId,variants:$variants){ productVariants{ id sku } userErrors{ field message } } }`,
+      { productId, variants }
     );
-    const ue = res.productVariantUpdate.userErrors;
-    if (ue.length) { console.log(`  ERROR ${p.old_sku}: ${JSON.stringify(ue)}`); skip++; continue; }
-    // mirror follows (CAS on old value)
-    psql(`update shopify_products set sku=case when sku=${q(p.mirror_sku)} then ${q(restamp(p.mirror_sku))} else sku end, ` +
-         `variant_sku=case when variant_sku like 'DWHQ-%' then replace(variant_sku,'DWHQ-','DWHF-') else variant_sku end, ` +
-         `dw_sku=case when dw_sku like 'DWHQ-%' then replace(dw_sku,'DWHQ-','DWHF-') else dw_sku end ` +
-         `where id=${p.mirror_id}`);
-    ok++;
+    const ue = res.productVariantsBulkUpdate.userErrors;
+    if (ue.length) { console.log(`  ERROR product ${productId}: ${JSON.stringify(ue)}`); skip += variants.length; continue; }
+    // mirror follows once per product row (idempotent DWHQ->DWHF replace)
+    for (const mid of new Set(items.map(x => x.mirror_id))) {
+      psql(`update shopify_products set sku=case when sku like 'DWHQ-%' then replace(sku,'DWHQ-','DWHF-') else sku end, ` +
+           `variant_sku=case when variant_sku like 'DWHQ-%' then replace(variant_sku,'DWHQ-','DWHF-') else variant_sku end, ` +
+           `dw_sku=case when dw_sku like 'DWHQ-%' then replace(dw_sku,'DWHQ-','DWHF-') else dw_sku end ` +
+           `where id=${mid}`);
+    }
+    ok += variants.length;
   }
   console.log(`\nDone: ${ok} written, ${skip} skipped.`);
   console.log('\nPost-check — re-running prefix-integrity canary:');
@@ -117,10 +126,15 @@ async function runRollback(mapPath) {
   const plan = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
   console.log(`ROLLBACK ${plan.length} variants from ${mapPath}`);
   if (!APPLY) { console.log('DRY-RUN rollback — re-run with --apply to revert.'); return; }
-  for (const p of plan) {
-    await gql(`mutation($input:ProductVariantInput!){ productVariantUpdate(input:$input){ userErrors{ message } } }`,
-              { input: { id: p.variant, sku: p.old_sku } });
-    psql(`update shopify_products set sku=${q(p.mirror_sku)}, variant_sku=${q(p.mirror_variant_sku)}, dw_sku=${q(p.mirror_dw_sku)} where id=${p.mirror_id}`);
+  const byP = new Map();
+  for (const p of plan) { if (!byP.has(p.product)) byP.set(p.product, []); byP.get(p.product).push(p); }
+  for (const [productId, items] of byP) {
+    await gql(`mutation($productId:ID!,$variants:[ProductVariantsBulkInput!]!){ productVariantsBulkUpdate(productId:$productId,variants:$variants){ userErrors{ message } } }`,
+              { productId, variants: items.map(p => ({ id: p.variant, inventoryItem: { sku: p.old_sku } })) });
+    for (const mid of new Set(items.map(p => p.mirror_id))) {
+      const p = items.find(x => x.mirror_id === mid);
+      psql(`update shopify_products set sku=${q(p.mirror_sku)}, variant_sku=${q(p.mirror_variant_sku)}, dw_sku=${q(p.mirror_dw_sku)} where id=${mid}`);
+    }
   }
   console.log('Rollback complete.');
 }

← 289793b TK-11002: Harlequin DWHQ-->DWHF- prefix re-stamp (Option A)  ·  back to Dw Sku Integrity  ·  auto-data-snapshot: 2026-08-31T10:06:07 (4 data files) — dat 0b977b0 →