← back to Designer Wallcoverings
TK-11400: fix false-success in inventory-set-2026-newest sweep
e252ac31be5bd7c22a7ae5efe5a353e4a0d371e1 · 2026-09-10 12:53:26 -0700 · Steve
Root cause: newest priced variants are tracked but NOT activated (stocked) at
the Ventura Blvd location, so inventorySetQuantities silently no-ops (0
userErrors, writes nothing). The script never called inventoryActivate, and
counted "batch had no userErrors" as success (setOk += chunk.length - ue.length,
also treating a null data payload as OK), so it logged "~2000 ok, 0 errors"
while its own re-scan showed 0 landed — and still exited 0, so the hourly job
logged success.
Fix (mirrors the proven sweep-all-active.mjs activate-then-set pattern):
- add inventoryActivate for every not-stocked item before the on_hand set
- honest accounting: relabel the write count "submitted", treat null data as
an error, never as OK
- race-free targeted VERIFY: re-read the exact inventoryItem ids written (by id)
instead of a fresh newest-N scan that can shift between scan and verify
- exit non-zero when the verify shows anything still wrong, so the hourly job
and canaries SEE the failure instead of logging false success
Scan-time $0 guard is unchanged, so activation at QTY cannot re-inflate a
$0/quote-only variant (TK-11357).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M shopify/scripts/inventory-set-2026-newest.mjs
Diff
commit e252ac31be5bd7c22a7ae5efe5a353e4a0d371e1
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Sep 10 12:53:26 2026 -0700
TK-11400: fix false-success in inventory-set-2026-newest sweep
Root cause: newest priced variants are tracked but NOT activated (stocked) at
the Ventura Blvd location, so inventorySetQuantities silently no-ops (0
userErrors, writes nothing). The script never called inventoryActivate, and
counted "batch had no userErrors" as success (setOk += chunk.length - ue.length,
also treating a null data payload as OK), so it logged "~2000 ok, 0 errors"
while its own re-scan showed 0 landed — and still exited 0, so the hourly job
logged success.
Fix (mirrors the proven sweep-all-active.mjs activate-then-set pattern):
- add inventoryActivate for every not-stocked item before the on_hand set
- honest accounting: relabel the write count "submitted", treat null data as
an error, never as OK
- race-free targeted VERIFY: re-read the exact inventoryItem ids written (by id)
instead of a fresh newest-N scan that can shift between scan and verify
- exit non-zero when the verify shows anything still wrong, so the hourly job
and canaries SEE the failure instead of logging false success
Scan-time $0 guard is unchanged, so activation at QTY cannot re-inflate a
$0/quote-only variant (TK-11357).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
shopify/scripts/inventory-set-2026-newest.mjs | 65 +++++++++++++++++++++++----
1 file changed, 56 insertions(+), 9 deletions(-)
diff --git a/shopify/scripts/inventory-set-2026-newest.mjs b/shopify/scripts/inventory-set-2026-newest.mjs
index 8a21d20a..328b45b2 100644
--- a/shopify/scripts/inventory-set-2026-newest.mjs
+++ b/shopify/scripts/inventory-set-2026-newest.mjs
@@ -87,6 +87,13 @@ async function scan() {
// ---- fix ----
const INV_SET = `mutation($input:InventorySetQuantitiesInput!){inventorySetQuantities(input:$input){userErrors{field message code}}}`;
+// inventorySetQuantities can ONLY set on_hand where the inventory item already has an inventory
+// level at the location. For a tracked-but-not-stocked item it does NOT create that level — it
+// no-ops (returns 0 userErrors, writes nothing). That silent no-op was the false-success bug
+// (TK-11400): the newest priced products are tracked but never activated at Ventura Blvd, so
+// every set landed nowhere while the script reported "~N ok, 0 errors". inventoryActivate
+// creates the level (matches the proven sweep-all-active.mjs step 4).
+const INV_ACT = `mutation($id:ID!,$loc:ID!,$qty:Int!){inventoryActivate(inventoryItemId:$id,locationId:$loc,available:$qty){userErrors{message}}}`;
async function fix(fixes) {
const untracked = fixes.filter(f => !f.tracked);
@@ -101,17 +108,50 @@ async function fix(fixes) {
process.stdout.write(` track ${Math.min(i + 20, untracked.length)}/${untracked.length}\r`);
}
if (untracked.length) console.log(`\n tracking enabled: ${trkOk} ok, ${trkErr.length} errors ${trkErr.length ? JSON.stringify(trkErr.slice(0, 3)) : ''}`);
- // 2) inventorySetQuantities on_hand=QTY in chunks of 200
+ // 2) ACTIVATE any not-stocked item at the location (create the inventory level) so the
+ // on_hand set below can actually persist. All flagged variants here are already price>0
+ // (the scan guard skips $0/quote-only), so activating at QTY cannot re-inflate a $0 variant.
+ const notStocked = fixes.filter(f => !f.stocked);
+ let actOk = 0; const actErr = [];
+ for (let i = 0; i < notStocked.length; i++) {
+ const f = notStocked[i];
+ const r = await gql(INV_ACT, { id: f.iid, loc: LOC, qty: QTY });
+ if (!r.data?.inventoryActivate) actErr.push('no-data:' + JSON.stringify(r.errors || r).slice(0, 120));
+ else { const ue = r.data.inventoryActivate.userErrors || []; if (ue.length) actErr.push(...ue.map(e => e.message)); else actOk++; }
+ process.stdout.write(` activate ${i + 1}/${notStocked.length}\r`);
+ }
+ if (notStocked.length) console.log(`\n activated at loc: ${actOk} ok, ${actErr.length} errors ${actErr.length ? JSON.stringify(actErr.slice(0, 3)) : ''}`);
+ // 3) inventorySetQuantities on_hand=QTY in chunks of 200. NOTE: this "submitted" count is NOT
+ // proof of success — a null data payload or a silent no-op both look error-free here. The
+ // authoritative check is the targeted VERIFY re-read below.
const pairs = fixes.map(f => ({ inventoryItemId: f.iid, locationId: LOC, quantity: QTY }));
- let setOk = 0; const setErr = [];
+ let submitted = 0; const setErr = [];
for (let i = 0; i < pairs.length; i += 200) {
const chunk = pairs.slice(i, i + 200);
const r = await gql(INV_SET, { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true, quantities: chunk } });
- const ue = r.data?.inventorySetQuantities?.userErrors || [];
- if (ue.length) setErr.push(...ue); setOk += chunk.length - ue.length;
+ if (!r.data?.inventorySetQuantities) { setErr.push('no-data:' + JSON.stringify(r.errors || r).slice(0, 120)); continue; } // top-level error = NOT ok
+ const ue = r.data.inventorySetQuantities.userErrors || [];
+ if (ue.length) setErr.push(...ue.map(e => (e.message || JSON.stringify(e)))); submitted += chunk.length - ue.length;
process.stdout.write(` set ${Math.min(i + 200, pairs.length)}/${pairs.length}\r`);
}
- console.log(`\n inventory set on_hand=${QTY}: ~${setOk} ok, ${setErr.length} errors ${setErr.length ? JSON.stringify(setErr.slice(0, 3)) : ''}`);
+ console.log(`\n inventory set on_hand=${QTY}: ${submitted}/${pairs.length} submitted, ${setErr.length} errors ${setErr.length ? JSON.stringify(setErr.slice(0, 3)) : ''}`);
+}
+
+// ---- targeted verify: re-read the EXACT items we wrote (by inventoryItem id) and confirm
+// on_hand==QTY at LOC. Avoids the newest-N re-scan race (a new product created between
+// scan and verify would shift the window and mis-measure). This is the source of truth. ----
+async function verifyItems(fixes) {
+ const ids = fixes.map(f => f.iid);
+ const Q = `query($ids:[ID!]!){nodes(ids:$ids){... on InventoryItem{id inventoryLevel(locationId:"${LOC}"){quantities(names:["on_hand"]){name quantity}}}}}`;
+ let ok = 0, bad = 0; const badSample = [];
+ for (let i = 0; i < ids.length; i += 100) {
+ const r = await gql(Q, { ids: ids.slice(i, i + 100) });
+ for (const n of (r.data?.nodes || [])) {
+ const onh = n?.inventoryLevel?.quantities?.find(x => x.name === 'on_hand')?.quantity;
+ if (onh === QTY) ok++; else { bad++; if (badSample.length < 3) badSample.push({ id: n?.id, onh }); }
+ }
+ }
+ return { ok, bad, badSample };
}
(async () => {
@@ -121,8 +161,15 @@ async function fix(fixes) {
console.log(`variants: ${s.totV} already 2026&ok: ${s.correct} NEED FIX: ${s.fixes.length}`);
if (!s.fixes.length) { console.log('nothing to do.'); return; }
if (DRY) { console.log('--dry: no writes. sample:', JSON.stringify(s.fixes.slice(0, 5), null, 1)); return; }
- await fix(s.fixes);
- // verify
- s = await scan();
- console.log(`VERIFY -> variants: ${s.totV} ok: ${s.correct} remaining: ${s.fixes.length}`);
+ const wrote = s.fixes;
+ await fix(wrote);
+ // verify — re-read the exact items we wrote (race-free, authoritative)
+ const v = await verifyItems(wrote);
+ console.log(`VERIFY -> wrote: ${wrote.length} now on_hand=${QTY}: ${v.ok} still wrong: ${v.bad}${v.bad ? ' ' + JSON.stringify(v.badSample) : ''}`);
+ if (v.bad) {
+ console.error(`FAILED: ${v.bad}/${wrote.length} variants still not at on_hand=${QTY} after activate+set — the write did not persist.`);
+ process.exitCode = 1; // non-zero so the hourly job (and any canary) SEES the failure instead of logging success
+ } else {
+ console.log(`OK: all ${wrote.length} written variants confirmed on_hand=${QTY} at ${LOC}.`);
+ }
})();
← cfff3faa TK-11422 + TK-11418: scope VCC's shared-catalog queries to t
·
back to Designer Wallcoverings
·
TK-11418: stamp shopify_product_id by primary key, not by am 020aa93c →