← back to Designer Wallcoverings
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
M DW-Programming/command54-shopify-push.jsM DW-Programming/justindavid-shopify-push.jsM shopify/scripts/cadence/cadence-import.jsA shopify/scripts/lib/inventory-stamp-guard.mjsA shopify/scripts/lib/inventory-stamp-guard.test.mjsM shopify/scripts/templates/new-product-import-template.js
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 Designer Wallcoverings
·
fix(TK-10965): guard remaining importer inventory-stamp site 00ba31fa →