[object Object]

← back to Dw Validator Debug TK11314

fix(TK-10965): guard importer inventory stamp — never stock a $0/quote-only sellable variant

b83a37b166496b1367278322c1e4e9436307a209 · 2026-08-30 23:23:58 -0700 · Designer Wallcoverings

Vendors the pure inventory-stamp-guard into shopify/scripts/lib/ and applies
safeStampQuantity() at every 2026-stamp site so a SELLABLE variant that is
priced $0 or on a quote-only/price-suppressed line gets 0 stock (not orderable)
instead of the cap-free 2026 literal. Behavior unchanged for normal priced lines.

Live: cadence/cadence-import.js (setInventory2026 restamp) +
templates/new-product-import-template.js (create-payload availableQuantity).
Retired (patched for consistency, not scheduled): command54-shopify-push.js,
justindavid-shopify-push.js. 8/8 unit tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit b83a37b166496b1367278322c1e4e9436307a209
Author: Designer Wallcoverings <steve@designerwallcoverings.com>
Date:   Sun Aug 30 23:23:58 2026 -0700

    fix(TK-10965): guard importer inventory stamp — never stock a $0/quote-only sellable variant
    
    Vendors the pure inventory-stamp-guard into shopify/scripts/lib/ and applies
    safeStampQuantity() at every 2026-stamp site so a SELLABLE variant that is
    priced $0 or on a quote-only/price-suppressed line gets 0 stock (not orderable)
    instead of the cap-free 2026 literal. Behavior unchanged for normal priced lines.
    
    Live: cadence/cadence-import.js (setInventory2026 restamp) +
    templates/new-product-import-template.js (create-payload availableQuantity).
    Retired (patched for consistency, not scheduled): command54-shopify-push.js,
    justindavid-shopify-push.js. 8/8 unit tests pass.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 DW-Programming/command54-shopify-push.js           | 21 ++++--
 DW-Programming/justindavid-shopify-push.js         | 16 ++++-
 shopify/scripts/cadence/cadence-import.js          | 21 +++++-
 shopify/scripts/lib/inventory-stamp-guard.mjs      | 75 ++++++++++++++++++++++
 shopify/scripts/lib/inventory-stamp-guard.test.mjs | 56 ++++++++++++++++
 .../templates/new-product-import-template.js       |  8 ++-
 6 files changed, 185 insertions(+), 12 deletions(-)

diff --git a/DW-Programming/command54-shopify-push.js b/DW-Programming/command54-shopify-push.js
index d43835b4..e58e63d6 100644
--- a/DW-Programming/command54-shopify-push.js
+++ b/DW-Programming/command54-shopify-push.js
@@ -24,6 +24,7 @@
 const { Pool } = require('pg');
 const https = require('https');
 const fs = require('fs');
+const { safeStampQuantity } = require('../shopify/scripts/lib/inventory-stamp-guard.mjs'); // TK-10965: never stock a $0/quote-only sellable variant (RETIRED file — patched for consistency)
 
 const pool = new Pool({
   connectionString: (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified')
@@ -98,11 +99,20 @@ async function setInventory2026(skus) {
   if (!SHOPIFY_TOKEN || !skus.length) return false;
   const map = new Map();
   const q = skus.map(s => `sku:"${String(s).replace(/"/g, '\\"')}"`).join(' OR ');
-  const r = await shopifyGql(`query($q:String!){productVariants(first:250,query:$q){edges{node{sku inventoryItem{id}}}}}`, { q });
+  // TK-10965: pull price + parent tags/vendor so the guard can decide per-variant.
+  const r = await shopifyGql(`query($q:String!){productVariants(first:250,query:$q){edges{node{sku price inventoryItem{id} product{tags vendor}}}}}`, { q });
   for (const e of (r?.data?.productVariants?.edges || [])) {
-    if (e.node.sku && e.node.inventoryItem?.id) map.set(e.node.sku, e.node.inventoryItem.id);
+    if (e.node.sku && e.node.inventoryItem?.id) map.set(e.node.sku, {
+      inventoryItemId: e.node.inventoryItem.id,
+      variant: { title: e.node.sku, price: e.node.price }, // sku ends in -Sample for the sample variant → guard's isSellableVariant reads it
+      product: { tags: e.node.product?.tags || [], vendor: e.node.product?.vendor || '' },
+    });
   }
-  const pairs = skus.filter(s => map.has(s)).map(s => ({ inventoryItemId: map.get(s), locationId: INV_LOCATION_2026, quantity: 2026 }));
+  // TK-10965: 0 for the $0/quote-only sellable class, 2026 otherwise.
+  const pairs = skus.filter(s => map.has(s)).map(s => {
+    const m = map.get(s);
+    return { inventoryItemId: m.inventoryItemId, locationId: INV_LOCATION_2026, quantity: safeStampQuantity(m.variant, m.product) };
+  });
   if (!pairs.length) return false;
   await shopifyGql(`mutation($input:InventorySetQuantitiesInput!){inventorySetQuantities(input:$input){userErrors{field message code}}}`,
     { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true, quantities: pairs } });
@@ -382,7 +392,7 @@ function buildVariants(row) {
       weight_unit: 'lb',
       requires_shipping: true,
       taxable: true,
-      inventory_quantity: 2026
+      inventory_quantity: safeStampQuantity({ option1: 'Sample', price: '4.25' }, { vendor: VENDOR_DISPLAY, tags: buildTags(row) })
     },
     {
       option1: 'Full Roll',
@@ -392,7 +402,8 @@ function buildVariants(row) {
       weight_unit: 'lb',
       requires_shipping: true,
       taxable: true,
-      inventory_quantity: 2026
+      // TK-10965: $0 quote-only sellable 'Full Roll' → guard returns 0 (never $0-orderable).
+      inventory_quantity: safeStampQuantity({ option1: 'Full Roll', price: '0.00' }, { vendor: VENDOR_DISPLAY, tags: buildTags(row) })
     }
   ];
 }
diff --git a/DW-Programming/justindavid-shopify-push.js b/DW-Programming/justindavid-shopify-push.js
index d3052dbb..5403e145 100644
--- a/DW-Programming/justindavid-shopify-push.js
+++ b/DW-Programming/justindavid-shopify-push.js
@@ -21,6 +21,7 @@
 const { Pool } = require('pg');
 const https = require('https');
 const fs = require('fs');
+const { safeStampQuantity } = require('../shopify/scripts/lib/inventory-stamp-guard.mjs'); // TK-10965: never stock a $0/quote-only sellable variant (RETIRED file — patched for consistency)
 
 const pool = new Pool({
   connectionString: (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified')
@@ -91,11 +92,20 @@ async function setInventory2026(skus) {
   if (!SHOPIFY_TOKEN || !skus.length) return false;
   const map = new Map();
   const q = skus.map(s => `sku:"${String(s).replace(/"/g, '\\"')}"`).join(' OR ');
-  const r = await shopifyGql(`query($q:String!){productVariants(first:250,query:$q){edges{node{sku inventoryItem{id}}}}}`, { q });
+  // TK-10965: pull price + parent tags/vendor so the guard can decide per-variant.
+  const r = await shopifyGql(`query($q:String!){productVariants(first:250,query:$q){edges{node{sku price inventoryItem{id} product{tags vendor}}}}}`, { q });
   for (const e of (r?.data?.productVariants?.edges || [])) {
-    if (e.node.sku && e.node.inventoryItem?.id) map.set(e.node.sku, e.node.inventoryItem.id);
+    if (e.node.sku && e.node.inventoryItem?.id) map.set(e.node.sku, {
+      inventoryItemId: e.node.inventoryItem.id,
+      variant: { title: e.node.sku, price: e.node.price }, // sku ends in -Sample for the sample variant → guard's isSellableVariant reads it
+      product: { tags: e.node.product?.tags || [], vendor: e.node.product?.vendor || '' },
+    });
   }
-  const pairs = skus.filter(s => map.has(s)).map(s => ({ inventoryItemId: map.get(s), locationId: INV_LOCATION_2026, quantity: 2026 }));
+  // TK-10965: 0 for the $0/quote-only sellable class, 2026 otherwise.
+  const pairs = skus.filter(s => map.has(s)).map(s => {
+    const m = map.get(s);
+    return { inventoryItemId: m.inventoryItemId, locationId: INV_LOCATION_2026, quantity: safeStampQuantity(m.variant, m.product) };
+  });
   if (!pairs.length) return false;
   await shopifyGql(`mutation($input:InventorySetQuantitiesInput!){inventorySetQuantities(input:$input){userErrors{field message code}}}`,
     { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true, quantities: pairs } });
diff --git a/shopify/scripts/cadence/cadence-import.js b/shopify/scripts/cadence/cadence-import.js
index 2485d814..7debe3fe 100644
--- a/shopify/scripts/cadence/cadence-import.js
+++ b/shopify/scripts/cadence/cadence-import.js
@@ -31,6 +31,7 @@ const { execFileSync } = require('child_process');
 const VENDORS = require('./vendors.js');
 const { validateBeforeActivate } = require('../lib/validate-before-activate.js'); // single activation gate (Steve 2026-06-20)
 const runLog = require('../lib/scraper-run-log.js'); // TK-10374: per-run ground-truth for scraper liveness (writes scraper_run_log)
+const { safeStampQuantity } = require('../lib/inventory-stamp-guard.mjs'); // TK-10965: never stock a $0/quote-only sellable variant
 
 // ---- args ----
 const args = process.argv.slice(2);
@@ -833,7 +834,10 @@ function createdTodayForTable(table) {
 // Reuses inventory-set-2026.js's exact mechanism: look up inventoryItem ids by SKU, then
 // inventorySetQuantities on_hand=2026 at the Ventura Blvd location. Inventory sets do NOT
 // consume the variant-creation cap (they touch existing variants), so this is cap-free.
-const INV_LOOKUP = `query($q:String!){productVariants(first:250,query:$q){edges{node{sku inventoryItem{id}}}}}`;
+// TK-10965: also select price + parent product tags/vendor so the inventory-stamp
+// guard can decide per-variant whether positive stock is safe (a $0 / quote-only
+// SELLABLE variant must get 0, never 2026, or it becomes $0-orderable).
+const INV_LOOKUP = `query($q:String!){productVariants(first:250,query:$q){edges{node{sku price inventoryItem{id} product{tags vendor}}}}}`;
 const INV_SET = `mutation($input:InventorySetQuantitiesInput!){inventorySetQuantities(input:$input){userErrors{field message code}}}`;
 async function setInventory2026(skus) {
   if (!skus.length) return { set: 0, errors: [] };
@@ -843,10 +847,21 @@ async function setInventory2026(skus) {
     const q = batch.map(s => `sku:"${String(s).replace(/"/g,'\\"')}"`).join(' OR ');
     const r = await gqlRetry(INV_LOOKUP, { q });
     for (const e of (r.json?.data?.productVariants?.edges || [])) {
-      if (e.node.sku && e.node.inventoryItem?.id) map.set(e.node.sku, e.node.inventoryItem.id);
+      if (e.node.sku && e.node.inventoryItem?.id) {
+        map.set(e.node.sku, {
+          inventoryItemId: e.node.inventoryItem.id,
+          variant: { title: e.node.sku, price: e.node.price }, // sku carries -Sample suffix → guard's isSellableVariant reads it
+          product: { tags: e.node.product?.tags || [], vendor: e.node.product?.vendor || '' },
+        });
+      }
     }
   }
-  const pairs = skus.filter(s => map.has(s)).map(s => ({ inventoryItemId: map.get(s), locationId: INV_LOCATION, quantity: 2026 }));
+  // Per-variant quantity via the guard: 2026 for normal priced variants, 0 for the
+  // $0/quote-only sellable class (the zero-price-orderable defect this closes).
+  const pairs = skus.filter(s => map.has(s)).map(s => {
+    const m = map.get(s);
+    return { inventoryItemId: m.inventoryItemId, locationId: INV_LOCATION, quantity: safeStampQuantity(m.variant, m.product) };
+  });
   const errors = [];
   for (let i=0;i<pairs.length;i+=250) {
     const r = await gqlRetry(INV_SET, { input: { name:'on_hand', reason:'correction', ignoreCompareQuantity:true, quantities: pairs.slice(i,i+250) } });
diff --git a/shopify/scripts/lib/inventory-stamp-guard.mjs b/shopify/scripts/lib/inventory-stamp-guard.mjs
new file mode 100644
index 00000000..9cc16799
--- /dev/null
+++ b/shopify/scripts/lib/inventory-stamp-guard.mjs
@@ -0,0 +1,75 @@
+// TK-10965 — Fix B (prevention): the inventory-stamp invariant, as a pure guard.
+//
+// ROOT CAUSE (see ../FINDINGS.md): importers stamp a positive "cap-free" stock
+// number (the year literal 2026) on the SELLABLE non-Sample variant of every
+// activated product — both in the product-create payload (`inventory_quantity: 2026`)
+// and on reconcile (`setInventory2026()`). When that sellable variant is ALSO
+// priced $0 (quote-only / contact-for-price lines like Phillipe Romano, Fentucci
+// Naturals), positive stock makes it `availableForSale` → checkout-orderable for $0.
+//
+// THE INVARIANT this module enforces (one place, both call sites):
+//   A sellable variant that is priced $0 OR belongs to a quote-only / price-
+//   suppressed line must NEVER receive positive inventory. It gets 0 → not orderable.
+//   (The $4.25 Sample variant is unaffected — it is not the sellable variant and is
+//    already qty=0/non-orderable by design.)
+//
+// PURE + dependency-free on purpose: no network, no env, no Shopify client, so it
+// unit-tests offline and drops into any importer runtime unchanged. $0 (local).
+
+// Tag family that means "this line has no public retail price" — a superset of the
+// single `quote-only` tag the standing canary keyed on (which is why Fentucci, tagged
+// `quotes`/`Needs-Price`, was the canary's 462-product blind spot).
+export const PRICE_SUPPRESSED_TAGS = new Set([
+  'quote-only', 'quote only', 'quote_only',
+  'quotes', 'contact-for-price', 'contact for price', 'needs-price', 'needs price',
+]);
+
+const norm = t => String(t).trim().toLowerCase();
+
+/**
+ * Is this product a quote-only / price-suppressed line?
+ * @param {{tags?: string[]|string, vendor?: string}} product
+ */
+export function isPriceSuppressed(product = {}) {
+  const tags = Array.isArray(product.tags)
+    ? product.tags
+    : String(product.tags || '').split(',');
+  if (tags.some(t => PRICE_SUPPRESSED_TAGS.has(norm(t)))) return true;
+  // Vendor fallback for untagged cohorts (Fentucci Naturals ships quote-only with
+  // zero quote-only tags). Extend as new price-on-request lines are onboarded.
+  return norm(product.vendor) === 'fentucci naturals';
+}
+
+/**
+ * A variant is the "sellable" one iff it is NOT the Sample variant.
+ * (Importers create exactly two variants: `Sample` @ $4.25 and the real unit @ price.)
+ * @param {{title?: string, option1?: string}} variant
+ */
+export function isSellableVariant(variant = {}) {
+  const label = variant.title ?? variant.option1 ?? '';
+  return !/sample/i.test(label);
+}
+
+/**
+ * Would giving this sellable variant positive stock make it a $0-orderable defect?
+ * True iff it's the sellable variant AND (price is 0 OR the line is price-suppressed).
+ * @param {object} variant  the variant about to be stamped
+ * @param {object} product  its parent (for tags/vendor)
+ */
+export function isZeroPriceOrderableRisk(variant = {}, product = {}) {
+  if (!isSellableVariant(variant)) return false;
+  const price = Number(variant.price);
+  return price === 0 || Number.isNaN(price) || isPriceSuppressed(product);
+}
+
+/**
+ * THE GUARD. Return the inventory quantity that is SAFE to stamp on this variant.
+ * Drop-in replacement for the literal `2026` at both call sites:
+ *   - create payload:  inventory_quantity: safeStampQuantity(variant, product)
+ *   - setInventory2026: quantity:        safeStampQuantity(variant, product)
+ * Returns `desired` (2026) for normal priced variants; 0 for the defect class.
+ * @returns {number} 0 for a zero-price-orderable risk, else `desired`
+ */
+export function safeStampQuantity(variant, product, desired = 2026) {
+  return isZeroPriceOrderableRisk(variant, product) ? 0 : desired;
+}
diff --git a/shopify/scripts/lib/inventory-stamp-guard.test.mjs b/shopify/scripts/lib/inventory-stamp-guard.test.mjs
new file mode 100644
index 00000000..f661320f
--- /dev/null
+++ b/shopify/scripts/lib/inventory-stamp-guard.test.mjs
@@ -0,0 +1,56 @@
+// TK-10965 — Fix B guard unit test. Pure/offline. Run: node prevention/inventory-stamp-guard.test.mjs
+// exit 0 = all pass, exit 1 = a case regressed. $0 (local).
+import assert from 'node:assert/strict';
+import { safeStampQuantity, isZeroPriceOrderableRisk, isPriceSuppressed } from './inventory-stamp-guard.mjs';
+
+let pass = 0;
+const t = (name, fn) => { fn(); pass++; console.log(`  ✓ ${name}`); };
+
+// --- The defect classes the guard MUST neutralize (stamp 0, not 2026) ---
+t('PR quote-only $0 Full Roll → 0', () => {
+  const product = { vendor: 'Phillipe Romano', tags: ['quote-only', 'contract-vinyl'] };
+  const variant = { title: 'Full Roll', price: '0.00' };
+  assert.equal(isZeroPriceOrderableRisk(variant, product), true);
+  assert.equal(safeStampQuantity(variant, product), 0);
+});
+
+t('Fentucci UNTAGGED (canary blind spot) $0 → 0 via vendor fallback', () => {
+  const product = { vendor: 'Fentucci Naturals', tags: ['quotes', 'Needs-Price'] };
+  const variant = { title: 'Full Roll', price: '0.00' };
+  assert.equal(isPriceSuppressed(product), true);
+  assert.equal(safeStampQuantity(variant, product), 0);
+});
+
+t('price-suppressed even if price were nonzero → 0', () => {
+  // Belt-and-suspenders: a quote-only line should never advertise stock regardless of price.
+  const product = { vendor: 'X', tags: ['contact-for-price'] };
+  assert.equal(safeStampQuantity({ title: 'Full Roll', price: '12.00' }, product), 0);
+});
+
+t('NaN/missing price on sellable variant → 0 (fail safe)', () => {
+  assert.equal(safeStampQuantity({ title: 'Full Roll', price: undefined }, { vendor: 'X', tags: [] }), 0);
+});
+
+t('tags as comma-string (Shopify REST shape) still detected → 0', () => {
+  const product = { vendor: 'X', tags: 'contract-vinyl, quote_only, new' };
+  assert.equal(safeStampQuantity({ title: 'Full Roll', price: '0.00' }, product), 0);
+});
+
+// --- The rows the guard MUST NOT touch (still stamp 2026) ---
+t('normal priced line (De Gournay-style) → 2026', () => {
+  const product = { vendor: 'De Gournay', tags: ['hand-painted'] };
+  assert.equal(safeStampQuantity({ title: 'Full Roll', price: '480.00' }, product), 2026);
+});
+
+t('Sample variant is never the sellable one → untouched (2026)', () => {
+  // The $4.25 Sample variant is not the sellable variant; guard is a no-op on it.
+  const product = { vendor: 'Phillipe Romano', tags: ['quote-only'] };
+  assert.equal(isZeroPriceOrderableRisk({ title: 'Sample', price: '4.25' }, product), false);
+  assert.equal(safeStampQuantity({ title: 'Sample', price: '4.25' }, product), 2026);
+});
+
+t('custom desired value is honored for safe rows', () => {
+  assert.equal(safeStampQuantity({ title: 'Full Roll', price: '99.00' }, { tags: [] }, 100), 100);
+});
+
+console.log(`\nALL ${pass} CASES PASS ✅  — guard stamps 0 on the $0-orderable class, 2026 otherwise.`);
diff --git a/shopify/scripts/templates/new-product-import-template.js b/shopify/scripts/templates/new-product-import-template.js
index f1f4e4f3..73a3d8ec 100644
--- a/shopify/scripts/templates/new-product-import-template.js
+++ b/shopify/scripts/templates/new-product-import-template.js
@@ -14,6 +14,7 @@
 
 const fetch = globalThis.fetch; // node-fetch shim removed -- Node 26 global fetch
 const { checkDuplicate, registerSku, skuTakenOnActiveProduct, updateShopifyInfo, getStats, close } = require('../lib/sku-registry');
+const { safeStampQuantity } = require('../lib/inventory-stamp-guard.mjs'); // TK-10965: never stock a $0/quote-only sellable variant
 
 // ==================== CONFIGURATION ====================
 // UPDATE THESE VALUES FOR YOUR VENDOR
@@ -173,7 +174,12 @@ async function importProducts() {
           price: price,
           inventoryPolicy: 'CONTINUE',
           inventoryQuantities: {
-            availableQuantity: 2026,
+            // TK-10965: guard the stock stamp — a $0 / quote-only SELLABLE variant gets 0
+            // (never orderable at $0); a normal priced variant still gets 2026.
+            availableQuantity: safeStampQuantity(
+              { title: dwSku, price: price },
+              { vendor: VENDOR_NAME, tags: [VENDOR_PREFIX, dwSku, mfrSku, color, pattern].filter(Boolean) }
+            ),
             locationId: 'gid://shopify/Location/YOUR_LOCATION_ID',
           },
         }],

← 8d8a8c63 Record TK-10832 Codex comparison evidence  ·  back to Dw Validator Debug TK11314  ·  fix(TK-10965): guard remaining importer inventory-stamp site 00ba31fa →