[object Object]

← back to Designer Wallcoverings

TK-11786: cadence setInventory2026 retries index-lag SKUs then records unresolved (exitCode=1) instead of voiding whole batch

e11fa6990ed1e4ec2941ca88bb79e35ae7adc38c · 2026-09-22 13:35:25 -0700 · Steve

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

Files touched

Diff

commit e11fa6990ed1e4ec2941ca88bb79e35ae7adc38c
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Sep 22 13:35:25 2026 -0700

    TK-11786: cadence setInventory2026 retries index-lag SKUs then records unresolved (exitCode=1) instead of voiding whole batch
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01G3ChReG53fwpNgUESv4SY7
---
 shopify/scripts/cadence/cadence-import.js | 52 +++++++++++++++++++++----------
 1 file changed, 35 insertions(+), 17 deletions(-)

diff --git a/shopify/scripts/cadence/cadence-import.js b/shopify/scripts/cadence/cadence-import.js
index 6ddd63db..782119a2 100644
--- a/shopify/scripts/cadence/cadence-import.js
+++ b/shopify/scripts/cadence/cadence-import.js
@@ -899,25 +899,43 @@ async function setInventory2026(skus) {
     if (!Array.isArray(skus) || skus.some(s => typeof s !== 'string' || !s.trim()) || new Set(skus).size !== skus.length) fail('invalid or duplicate requested SKU');
     if (!skus.length) return { set: 0, mapped: 0, errors };
     const itemIds = new Set();
-    // Resolve the entire requested set before making any inventory mutation.
-    for (let i=0;i<skus.length;i+=40) {
-      const batch = skus.slice(i, i+40);
-      const q = batch.map(s => `sku:"${s.replace(/\\/g,'\\\\').replace(/"/g,'\\"')}"`).join(' OR ');
-      const variants = data(await gqlRetry(INV_LOOKUP, { q, loc: INV_LOCATION }), 'productVariants');
-      if (!Array.isArray(variants.edges) || variants.pageInfo?.hasNextPage !== false) fail('incomplete SKU lookup');
-      for (const edge of variants.edges) {
-        const n = edge?.node;
-        if (!n || !batch.includes(n.sku) || map.has(n.sku) || !/^gid:\/\/shopify\/InventoryItem\/\d+$/.test(n.inventoryItem?.id || '') || itemIds.has(n.inventoryItem.id)) fail('unexpected, duplicate or invalid SKU/item mapping');
-        if (n.price == null || !n.product || !Array.isArray(n.product.tags) || typeof n.product.vendor !== 'string') fail(`missing stock guard metadata: ${n.sku}`);
-        if (!Object.hasOwn(n.inventoryItem, 'inventoryLevel')) fail(`missing inventory level field: ${n.sku}`);
-        const level = n.inventoryItem.inventoryLevel;
-        if (level !== null && !levelAtLocation(level)) fail(`wrong inventory location: ${n.sku}`);
-        itemIds.add(n.inventoryItem.id);
-        map.set(n.sku, { id:n.inventoryItem.id, active:level !== null, quantity:safeStampQuantity({title:n.sku,price:n.price}, n.product) });
+    // Resolve the requested set before mutating. Shopify's product SEARCH index lags a few
+    // seconds behind productSet creation, so a just-created SKU may not resolve on the first
+    // pass. HYBRID (Steve-approved 2026-09-22): retry ONLY the still-missing SKUs a few times
+    // with backoff, then stamp everything that DID resolve and record the rest as errors —
+    // instead of the old hard `fail('missing requested SKU')`, which voided the ENTIRE batch's
+    // inventory (every resolved SKU too) because one un-indexed SKU threw out of the whole fn.
+    // NOTE: the per-item INTEGRITY guards below (wrong location, dup item, bad/absent metadata)
+    // still hard-fail — those are real corruption, NOT index lag, and must abort as before.
+    const lookupBatch = async (targetSkus) => {
+      for (let i=0;i<targetSkus.length;i+=40) {
+        const batch = targetSkus.slice(i, i+40);
+        const q = batch.map(s => `sku:"${s.replace(/\\/g,'\\\\').replace(/"/g,'\\"')}"`).join(' OR ');
+        const variants = data(await gqlRetry(INV_LOOKUP, { q, loc: INV_LOCATION }), 'productVariants');
+        if (!Array.isArray(variants.edges) || variants.pageInfo?.hasNextPage !== false) fail('incomplete SKU lookup');
+        for (const edge of variants.edges) {
+          const n = edge?.node;
+          if (!n || !batch.includes(n.sku) || map.has(n.sku) || !/^gid:\/\/shopify\/InventoryItem\/\d+$/.test(n.inventoryItem?.id || '') || itemIds.has(n.inventoryItem.id)) fail('unexpected, duplicate or invalid SKU/item mapping');
+          if (n.price == null || !n.product || !Array.isArray(n.product.tags) || typeof n.product.vendor !== 'string') fail(`missing stock guard metadata: ${n.sku}`);
+          if (!Object.hasOwn(n.inventoryItem, 'inventoryLevel')) fail(`missing inventory level field: ${n.sku}`);
+          const level = n.inventoryItem.inventoryLevel;
+          if (level !== null && !levelAtLocation(level)) fail(`wrong inventory location: ${n.sku}`);
+          itemIds.add(n.inventoryItem.id);
+          map.set(n.sku, { id:n.inventoryItem.id, active:level !== null, quantity:safeStampQuantity({title:n.sku,price:n.price}, n.product) });
+        }
       }
+    };
+    const LOOKUP_ATTEMPTS = Math.max(1, parseInt(process.env.INV_LOOKUP_ATTEMPTS || '3', 10) || 3);
+    for (let attempt=0; attempt<LOOKUP_ATTEMPTS; attempt++) {
+      const missing = skus.filter(s => !map.has(s));
+      if (!missing.length) break;
+      if (attempt > 0) await sleep(Math.min(8000, 1500 * attempt));   // backoff for search-index lag
+      await lookupBatch(missing);
     }
-    if (map.size !== skus.length || skus.some(s => !map.has(s))) fail('missing requested SKU');
-    for (const sku of skus) {
+    // Still-missing after retries => record (caller flags the run incomplete via exitCode=1) but
+    // DO NOT throw — stamping proceeds for the resolved SKUs below (which iterate map, not skus).
+    for (const s of skus) if (!map.has(s)) errors.push({ sku: s, message: 'missing requested SKU (unresolved after retries)' });
+    for (const sku of map.keys()) {   // resolved SKUs only; unresolved ones are already in errors[]
       const item = map.get(sku);
       try {
         if (!item.active) {

← 5c8fd6b6 TK-11786 #1/#2: validate-before-activate shim + debug-clone  ·  back to Designer Wallcoverings  ·  TK-11786: free-samples anchored title match + block-nonpurch 8196c654 →