← 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
M shopify/scripts/cadence/cadence-import.js
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 →