← back to Designerwallcoverings
guard TK-11357 Fix D: never stock a $0/quote-only variant in the 9 go-live activators
fab7c61945ddd358be8d78b141f56bfe717af15a · 2026-09-10 09:25:42 -0700 · Steve
The $0-orderable defect (lineage TK-10825/10965/11140/11299/11301/11357) recurs because
activators stamp qty=2026 on EVERY variant of a product and apply any price check only at
the PRODUCT level (or, in these 9, nowhere at all). Positive stock is what flips
availableForSale=true, so a $0 variant riding alongside a priced one becomes orderable at $0.
PRE-EXISTING GUARD STATE, verified per file before patching (not assumed):
maharam/knoll/osborne/artmura/fallingstar/tres-tintas — NO price gate anywhere on the write
path (only heldNoMfr + heldNoDesc holds, which are metafield/description checks).
muralsource/stout/stroheim — call validateBeforeActivate upstream, but that validator has NO
price check (it checks specs/description/images/sample-variant presence), and it reads the
original draft-mint PAYLOAD, not the live product. So: no price gate either.
maharam-onboard/go-live.mjs is the highest priority of the set: com.steve.dw-maharam-daily is
LOADED in launchd and runs `go-live.mjs --apply` daily with NO --limit, on a QUOTE-ONLY line.
$0 is the LIVE theme's deliberate quote-only sentinel (snippets/product-form-content.liquid
renders the "Contact Us" button iff variant.price == 0), so the remedy is NEVER a placeholder
price — it is "do not stock it". Steve's 2026-06-20 "active products are never out of stock"
rule is preserved for PRICED goods: a priced variant still gets TARGET_QTY.
Adopts the shared guard lib/inventory-stamp-guard.mjs (safeStampQuantity) rather than
reinventing it — vendored into this repo (not cross-repo-imported) so a moved tree cannot
hard-crash the scheduled maharam cadence; the proof harness hashes all copies and fails on
drift. Adds one strictly-safer rule on top: price <= 0 / NaN is ALWAYS 0, even on a variant
labelled "Sample", because a $0 "sample" is the same $0-orderable defect (the shared guard
alone exempts samples). The real $4.25 memo sample keeps its existing 2026 — deliberately
unchanged, out of scope.
Also adds `title price` to each variant selection and `vendor tags` to the product selection,
which these queries did not fetch — without them the filter is structurally impossible.
Proven by shopify/scripts/tk11357-source-fix-proof/predicate-proof.mjs in the sibling
Designer-Wallcoverings repo: 4/4 required + 3/3 regression on all 9, against the predicate
EXTRACTED from each file (not hand-copied).
SOURCE-ONLY: no script was run with --apply, no Shopify write, no dw_unified write.
Reversible: git revert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Files touched
M scripts/artmura-onboard/go-live-artmura.jsM scripts/fallingstar-onboard/go-live-fallingstar.jsM scripts/knoll-onboard/go-live.mjsA scripts/lib/inventory-stamp-guard.mjsM scripts/maharam-onboard/go-live.mjsM scripts/muralsource-onboard/go-live.mjsM scripts/osborne-onboard/go-live.mjsM scripts/stout-onboard/go-live.mjsM scripts/stroheim-onboard/go-live.mjsM scripts/tres-tintas-onboard/go-live-tres-tintas.js
Diff
commit fab7c61945ddd358be8d78b141f56bfe717af15a
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Sep 10 09:25:42 2026 -0700
guard TK-11357 Fix D: never stock a $0/quote-only variant in the 9 go-live activators
The $0-orderable defect (lineage TK-10825/10965/11140/11299/11301/11357) recurs because
activators stamp qty=2026 on EVERY variant of a product and apply any price check only at
the PRODUCT level (or, in these 9, nowhere at all). Positive stock is what flips
availableForSale=true, so a $0 variant riding alongside a priced one becomes orderable at $0.
PRE-EXISTING GUARD STATE, verified per file before patching (not assumed):
maharam/knoll/osborne/artmura/fallingstar/tres-tintas — NO price gate anywhere on the write
path (only heldNoMfr + heldNoDesc holds, which are metafield/description checks).
muralsource/stout/stroheim — call validateBeforeActivate upstream, but that validator has NO
price check (it checks specs/description/images/sample-variant presence), and it reads the
original draft-mint PAYLOAD, not the live product. So: no price gate either.
maharam-onboard/go-live.mjs is the highest priority of the set: com.steve.dw-maharam-daily is
LOADED in launchd and runs `go-live.mjs --apply` daily with NO --limit, on a QUOTE-ONLY line.
$0 is the LIVE theme's deliberate quote-only sentinel (snippets/product-form-content.liquid
renders the "Contact Us" button iff variant.price == 0), so the remedy is NEVER a placeholder
price — it is "do not stock it". Steve's 2026-06-20 "active products are never out of stock"
rule is preserved for PRICED goods: a priced variant still gets TARGET_QTY.
Adopts the shared guard lib/inventory-stamp-guard.mjs (safeStampQuantity) rather than
reinventing it — vendored into this repo (not cross-repo-imported) so a moved tree cannot
hard-crash the scheduled maharam cadence; the proof harness hashes all copies and fails on
drift. Adds one strictly-safer rule on top: price <= 0 / NaN is ALWAYS 0, even on a variant
labelled "Sample", because a $0 "sample" is the same $0-orderable defect (the shared guard
alone exempts samples). The real $4.25 memo sample keeps its existing 2026 — deliberately
unchanged, out of scope.
Also adds `title price` to each variant selection and `vendor tags` to the product selection,
which these queries did not fetch — without them the filter is structurally impossible.
Proven by shopify/scripts/tk11357-source-fix-proof/predicate-proof.mjs in the sibling
Designer-Wallcoverings repo: 4/4 required + 3/3 regression on all 9, against the predicate
EXTRACTED from each file (not hand-copied).
SOURCE-ONLY: no script was run with --apply, no Shopify write, no dw_unified write.
Reversible: git revert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
scripts/artmura-onboard/go-live-artmura.js | 31 ++++++++-
scripts/fallingstar-onboard/go-live-fallingstar.js | 31 ++++++++-
scripts/knoll-onboard/go-live.mjs | 31 ++++++++-
scripts/lib/inventory-stamp-guard.mjs | 80 ++++++++++++++++++++++
scripts/maharam-onboard/go-live.mjs | 31 ++++++++-
scripts/muralsource-onboard/go-live.mjs | 31 ++++++++-
scripts/osborne-onboard/go-live.mjs | 31 ++++++++-
scripts/stout-onboard/go-live.mjs | 31 ++++++++-
scripts/stroheim-onboard/go-live.mjs | 31 ++++++++-
scripts/tres-tintas-onboard/go-live-tres-tintas.js | 31 ++++++++-
10 files changed, 332 insertions(+), 27 deletions(-)
diff --git a/scripts/artmura-onboard/go-live-artmura.js b/scripts/artmura-onboard/go-live-artmura.js
index 51111c3..ad007fe 100644
--- a/scripts/artmura-onboard/go-live-artmura.js
+++ b/scripts/artmura-onboard/go-live-artmura.js
@@ -62,7 +62,30 @@ async function gql(query, variables = {}) {
}
}
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status variants(first:10){edges{node{id sku inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status vendor tags variants(first:10){edges{node{id sku title price inventoryItem{id}}}}}}`;
+const { safeStampQuantity } = require('../lib/inventory-stamp-guard.mjs'); // GUARD TK-11357 (shared guard)
+// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
+// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
+// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
+// 11140 -> 11299 -> 11301 -> 11357). $0 is the LIVE theme's deliberate quote-only SENTINEL
+// (snippets/product-form-content.liquid renders the "Contact Us" button iff variant.price == 0),
+// so the remedy is NEVER to write a placeholder price - it is "do not stock it".
+// Steve's 2026-06-20 "active products are never out of stock" rule is PRESERVED for PRICED goods:
+// a priced variant still gets `desired`. The quote-only tag/vendor decision is delegated to the
+// shared guard (lib/inventory-stamp-guard.mjs); this adds one strictly-safer rule of its own -
+// price <= 0 / NaN is ALWAYS 0, even on a variant labelled "Sample" (a $0 "sample" is the same
+// $0-orderable defect). The real $4.25 memo sample is unaffected and keeps its existing quantity.
+function safeQuantities(product, variants, locationId, desired) {
+ return (variants || []).map(v => ({
+ inventoryItemId: v.inventoryItem.id,
+ locationId,
+ quantity: Number(v.price) > 0
+ ? safeStampQuantity({ title: v.title, price: v.price }, product, desired)
+ : 0,
+ }));
+}
+// ── GUARD TK-11357 END ────────────────────────────────────────────
+
const M_TRACK = `mutation($id:ID!){ inventoryItemUpdate(id:$id, input:{tracked:true}){ userErrors{message} } }`;
const M_ACTIVATE = `mutation($iid:ID!,$loc:ID!){ inventoryActivate(inventoryItemId:$iid, locationId:$loc){ userErrors{message} } }`;
const M_SETQTY = `mutation($input:InventorySetQuantitiesInput!){ inventorySetQuantities(input:$input){ userErrors{field message code} } }`;
@@ -74,7 +97,9 @@ async function goLive(pid) {
const errs = [];
const d = await gql(Q_VARIANTS, { id: gidP });
if (d.product?.status === 'ACTIVE') return { skipped: true };
- const items = d.product.variants.edges.map(e => e.node.inventoryItem.id);
+ const vnodes = d.product.variants.edges.map(e => e.node);
+ const items = vnodes.map(v => v.inventoryItem.id); // track+activate: unchanged
+ const quantities = safeQuantities(d.product, vnodes, LOCATION_ID, TARGET_QTY); // GUARD TK-11357
// 1+2: track, activate, set 2026 per item
for (const iid of items) {
let r = await gql(M_TRACK, { id: iid }); (r.inventoryItemUpdate?.userErrors || []).forEach(e => errs.push('track:' + e.message));
@@ -83,7 +108,7 @@ async function goLive(pid) {
ae.forEach(e => errs.push('activate:' + e.message));
}
const r2 = await gql(M_SETQTY, { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true,
- quantities: items.map(iid => ({ inventoryItemId: iid, locationId: LOCATION_ID, quantity: TARGET_QTY })) } });
+ quantities } }); // GUARD TK-11357
(r2.inventorySetQuantities?.userErrors || []).forEach(e => errs.push('setqty:' + e.message));
// 3: publish to all channels
const r3 = await gql(M_PUBLISH, { id: gidP, pubs: PUBLICATIONS.filter(_pid => _pid !== 29646651457 || !_isShowroomGuard("Artmura")).map(p => ({ publicationId: p })) });
diff --git a/scripts/fallingstar-onboard/go-live-fallingstar.js b/scripts/fallingstar-onboard/go-live-fallingstar.js
index fff00c7..aae60e3 100644
--- a/scripts/fallingstar-onboard/go-live-fallingstar.js
+++ b/scripts/fallingstar-onboard/go-live-fallingstar.js
@@ -56,7 +56,30 @@ async function gql(query, variables = {}) {
} catch (e) { if (a === 3) throw e; await sleep(900 * (a + 1)); }
}
}
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status variants(first:10){edges{node{id sku inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status vendor tags variants(first:10){edges{node{id sku title price inventoryItem{id}}}}}}`;
+const { safeStampQuantity } = require('../lib/inventory-stamp-guard.mjs'); // GUARD TK-11357 (shared guard)
+// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
+// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
+// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
+// 11140 -> 11299 -> 11301 -> 11357). $0 is the LIVE theme's deliberate quote-only SENTINEL
+// (snippets/product-form-content.liquid renders the "Contact Us" button iff variant.price == 0),
+// so the remedy is NEVER to write a placeholder price - it is "do not stock it".
+// Steve's 2026-06-20 "active products are never out of stock" rule is PRESERVED for PRICED goods:
+// a priced variant still gets `desired`. The quote-only tag/vendor decision is delegated to the
+// shared guard (lib/inventory-stamp-guard.mjs); this adds one strictly-safer rule of its own -
+// price <= 0 / NaN is ALWAYS 0, even on a variant labelled "Sample" (a $0 "sample" is the same
+// $0-orderable defect). The real $4.25 memo sample is unaffected and keeps its existing quantity.
+function safeQuantities(product, variants, locationId, desired) {
+ return (variants || []).map(v => ({
+ inventoryItemId: v.inventoryItem.id,
+ locationId,
+ quantity: Number(v.price) > 0
+ ? safeStampQuantity({ title: v.title, price: v.price }, product, desired)
+ : 0,
+ }));
+}
+// ── GUARD TK-11357 END ────────────────────────────────────────────
+
const M_TRACK = `mutation($id:ID!){ inventoryItemUpdate(id:$id, input:{tracked:true}){ userErrors{message} } }`;
const M_ACTIVATE = `mutation($iid:ID!,$loc:ID!){ inventoryActivate(inventoryItemId:$iid, locationId:$loc){ userErrors{message} } }`;
const M_SETQTY = `mutation($input:InventorySetQuantitiesInput!){ inventorySetQuantities(input:$input){ userErrors{field message code} } }`;
@@ -68,14 +91,16 @@ async function goLive(pid) {
const errs = [];
const d = await gql(Q_VARIANTS, { id: gidP });
if (!d?.product) return ['product not found'];
- const items = d.product.variants.edges.map(e => e.node.inventoryItem.id);
+ const vnodes = d.product.variants.edges.map(e => e.node);
+ const items = vnodes.map(v => v.inventoryItem.id); // track+activate: unchanged
+ const quantities = safeQuantities(d.product, vnodes, LOCATION_ID, TARGET_QTY); // GUARD TK-11357
for (const iid of items) {
let r = await gql(M_TRACK, { id: iid }); (r.inventoryItemUpdate?.userErrors || []).forEach(e => errs.push('track:' + e.message));
r = await gql(M_ACTIVATE, { iid, loc: LOCATION_ID });
(r.inventoryActivate?.userErrors || []).filter(e => !/already/i.test(e.message)).forEach(e => errs.push('activate:' + e.message));
}
const r2 = await gql(M_SETQTY, { input: { name: 'available', reason: 'correction', ignoreCompareQuantity: true,
- quantities: items.map(iid => ({ inventoryItemId: iid, locationId: LOCATION_ID, quantity: TARGET_QTY })) } });
+ quantities } }); // GUARD TK-11357
(r2.inventorySetQuantities?.userErrors || []).forEach(e => errs.push('setqty:' + e.message));
const r3 = await gql(M_PUBLISH, { id: gidP, pubs: PUBLICATIONS.filter(_pid => _pid !== 29646651457 || !_isShowroomGuard("Falling Star")).map(p => ({ publicationId: p })) });
(r3.publishablePublish?.userErrors || []).forEach(e => errs.push('publish:' + e.message));
diff --git a/scripts/knoll-onboard/go-live.mjs b/scripts/knoll-onboard/go-live.mjs
index a5e9be8..aac6e05 100644
--- a/scripts/knoll-onboard/go-live.mjs
+++ b/scripts/knoll-onboard/go-live.mjs
@@ -37,7 +37,30 @@ const PUBLICATIONS = [
].map(n => `gid://shopify/Publication/${n}`);
const sleep = ms => new Promise(r => setTimeout(r, ms));
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle variants(first:10){edges{node{inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle vendor tags variants(first:10){edges{node{title price inventoryItem{id}}}}}}`;
+import { safeStampQuantity } from '../lib/inventory-stamp-guard.mjs'; // GUARD TK-11357 (shared guard)
+// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
+// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
+// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
+// 11140 -> 11299 -> 11301 -> 11357). $0 is the LIVE theme's deliberate quote-only SENTINEL
+// (snippets/product-form-content.liquid renders the "Contact Us" button iff variant.price == 0),
+// so the remedy is NEVER to write a placeholder price - it is "do not stock it".
+// Steve's 2026-06-20 "active products are never out of stock" rule is PRESERVED for PRICED goods:
+// a priced variant still gets `desired`. The quote-only tag/vendor decision is delegated to the
+// shared guard (lib/inventory-stamp-guard.mjs); this adds one strictly-safer rule of its own -
+// price <= 0 / NaN is ALWAYS 0, even on a variant labelled "Sample" (a $0 "sample" is the same
+// $0-orderable defect). The real $4.25 memo sample is unaffected and keeps its existing quantity.
+function safeQuantities(product, variants, locationId, desired) {
+ return (variants || []).map(v => ({
+ inventoryItemId: v.inventoryItem.id,
+ locationId,
+ quantity: Number(v.price) > 0
+ ? safeStampQuantity({ title: v.title, price: v.price }, product, desired)
+ : 0,
+ }));
+}
+// ── GUARD TK-11357 END ────────────────────────────────────────────
+
const M_TRACK = `mutation($id:ID!){ inventoryItemUpdate(id:$id, input:{tracked:true}){ userErrors{message} } }`;
const M_ACTIVATE = `mutation($iid:ID!,$loc:ID!){ inventoryActivate(inventoryItemId:$iid, locationId:$loc){ userErrors{message} } }`;
const M_SETQTY = `mutation($input:InventorySetQuantitiesInput!){ inventorySetQuantities(input:$input){ userErrors{message code} } }`;
@@ -66,14 +89,16 @@ async function goLive(pid, sku) {
const d = await gql(Q_VARIANTS, { id: gidP });
if (!d?.product) return { missing: true };
if (d.product.status === 'ACTIVE') { dbWriteBack(sku, pid, d.product.handle); return { skipped: true }; }
- const items = d.product.variants.edges.map(e => e.node.inventoryItem.id);
+ const vnodes = d.product.variants.edges.map(e => e.node);
+ const items = vnodes.map(v => v.inventoryItem.id); // track+activate: unchanged
+ const quantities = safeQuantities(d.product, vnodes, LOCATION_ID, TARGET_QTY); // GUARD TK-11357
for (const iid of items) {
let r = await gql(M_TRACK, { id: iid }); (r.inventoryItemUpdate?.userErrors || []).forEach(e => errs.push('track:' + e.message));
r = await gql(M_ACTIVATE, { iid, loc: LOCATION_ID });
(r.inventoryActivate?.userErrors || []).filter(e => !/already/i.test(e.message)).forEach(e => errs.push('activate:' + e.message));
}
const r2 = await gql(M_SETQTY, { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true,
- quantities: items.map(iid => ({ inventoryItemId: iid, locationId: LOCATION_ID, quantity: TARGET_QTY })) } });
+ quantities } }); // GUARD TK-11357
(r2.inventorySetQuantities?.userErrors || []).forEach(e => errs.push('setqty:' + e.message));
const r3 = await gql(M_PUBLISH, { id: gidP, pubs: PUBLICATIONS.filter(_pid => _pid !== 29646651457 || !_isShowroomGuard("Knoll")).map(p => ({ publicationId: p })) });
(r3.publishablePublish?.userErrors || []).forEach(e => errs.push('publish:' + e.message));
diff --git a/scripts/lib/inventory-stamp-guard.mjs b/scripts/lib/inventory-stamp-guard.mjs
new file mode 100644
index 0000000..29d1810
--- /dev/null
+++ b/scripts/lib/inventory-stamp-guard.mjs
@@ -0,0 +1,80 @@
+// VENDORED COPY — canonical source: Designer-Wallcoverings/shopify/scripts/lib/inventory-stamp-guard.mjs
+// Vendored (not cross-repo-imported) on purpose: this repo ships and runs independently, and a
+// cross-repo relative import would hard-crash the scheduled cadence if either tree moved.
+// KEEP IN SYNC — the TK-11357 fixture harness hashes both copies and FAILS on drift.
+// TK-11357 (lineage TK-10825/10965/11140/11299/11301/11357).
+// 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/scripts/maharam-onboard/go-live.mjs b/scripts/maharam-onboard/go-live.mjs
index e66b1cc..dd989c7 100644
--- a/scripts/maharam-onboard/go-live.mjs
+++ b/scripts/maharam-onboard/go-live.mjs
@@ -40,7 +40,30 @@ const PUBLICATIONS = [
].map(n => `gid://shopify/Publication/${n}`);
const sleep = ms => new Promise(r => setTimeout(r, ms));
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle variants(first:10){edges{node{inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle vendor tags variants(first:10){edges{node{title price inventoryItem{id}}}}}}`;
+import { safeStampQuantity } from '../lib/inventory-stamp-guard.mjs'; // GUARD TK-11357 (shared guard)
+// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
+// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
+// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
+// 11140 -> 11299 -> 11301 -> 11357). $0 is the LIVE theme's deliberate quote-only SENTINEL
+// (snippets/product-form-content.liquid renders the "Contact Us" button iff variant.price == 0),
+// so the remedy is NEVER to write a placeholder price - it is "do not stock it".
+// Steve's 2026-06-20 "active products are never out of stock" rule is PRESERVED for PRICED goods:
+// a priced variant still gets `desired`. The quote-only tag/vendor decision is delegated to the
+// shared guard (lib/inventory-stamp-guard.mjs); this adds one strictly-safer rule of its own -
+// price <= 0 / NaN is ALWAYS 0, even on a variant labelled "Sample" (a $0 "sample" is the same
+// $0-orderable defect). The real $4.25 memo sample is unaffected and keeps its existing quantity.
+function safeQuantities(product, variants, locationId, desired) {
+ return (variants || []).map(v => ({
+ inventoryItemId: v.inventoryItem.id,
+ locationId,
+ quantity: Number(v.price) > 0
+ ? safeStampQuantity({ title: v.title, price: v.price }, product, desired)
+ : 0,
+ }));
+}
+// ── GUARD TK-11357 END ────────────────────────────────────────────
+
const M_TRACK = `mutation($id:ID!){ inventoryItemUpdate(id:$id, input:{tracked:true}){ userErrors{message} } }`;
const M_ACTIVATE = `mutation($iid:ID!,$loc:ID!){ inventoryActivate(inventoryItemId:$iid, locationId:$loc){ userErrors{message} } }`;
const M_SETQTY = `mutation($input:InventorySetQuantitiesInput!){ inventorySetQuantities(input:$input){ userErrors{message code} } }`;
@@ -71,14 +94,16 @@ async function goLive(pid, sku, mfrSku) {
const d = await gql(Q_VARIANTS, { id: gidP });
if (!d?.product) return { missing: true };
if (d.product.status === 'ACTIVE') { dbWriteBack(sku, pid, d.product.handle, mfrSku); return { skipped: true }; }
- const items = d.product.variants.edges.map(e => e.node.inventoryItem.id);
+ const vnodes = d.product.variants.edges.map(e => e.node);
+ const items = vnodes.map(v => v.inventoryItem.id); // track+activate: unchanged
+ const quantities = safeQuantities(d.product, vnodes, LOCATION_ID, TARGET_QTY); // GUARD TK-11357
for (const iid of items) {
let r = await gql(M_TRACK, { id: iid }); (r.inventoryItemUpdate?.userErrors || []).forEach(e => errs.push('track:' + e.message));
r = await gql(M_ACTIVATE, { iid, loc: LOCATION_ID });
(r.inventoryActivate?.userErrors || []).filter(e => !/already/i.test(e.message)).forEach(e => errs.push('activate:' + e.message));
}
const r2 = await gql(M_SETQTY, { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true,
- quantities: items.map(iid => ({ inventoryItemId: iid, locationId: LOCATION_ID, quantity: TARGET_QTY })) } });
+ quantities } }); // GUARD TK-11357
(r2.inventorySetQuantities?.userErrors || []).forEach(e => errs.push('setqty:' + e.message));
const r3 = await gql(M_PUBLISH, { id: gidP, pubs: PUBLICATIONS.filter(_pid => _pid !== 29646651457 || !_isShowroomGuard("Maharam")).map(p => ({ publicationId: p })) });
(r3.publishablePublish?.userErrors || []).forEach(e => errs.push('publish:' + e.message));
diff --git a/scripts/muralsource-onboard/go-live.mjs b/scripts/muralsource-onboard/go-live.mjs
index 04fce25..3753c22 100644
--- a/scripts/muralsource-onboard/go-live.mjs
+++ b/scripts/muralsource-onboard/go-live.mjs
@@ -45,7 +45,30 @@ const PUBLICATIONS = [
].map(n => `gid://shopify/Publication/${n}`);
const sleep = ms => new Promise(r => setTimeout(r, ms));
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle variants(first:10){edges{node{inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle vendor tags variants(first:10){edges{node{title price inventoryItem{id}}}}}}`;
+import { safeStampQuantity } from '../lib/inventory-stamp-guard.mjs'; // GUARD TK-11357 (shared guard)
+// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
+// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
+// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
+// 11140 -> 11299 -> 11301 -> 11357). $0 is the LIVE theme's deliberate quote-only SENTINEL
+// (snippets/product-form-content.liquid renders the "Contact Us" button iff variant.price == 0),
+// so the remedy is NEVER to write a placeholder price - it is "do not stock it".
+// Steve's 2026-06-20 "active products are never out of stock" rule is PRESERVED for PRICED goods:
+// a priced variant still gets `desired`. The quote-only tag/vendor decision is delegated to the
+// shared guard (lib/inventory-stamp-guard.mjs); this adds one strictly-safer rule of its own -
+// price <= 0 / NaN is ALWAYS 0, even on a variant labelled "Sample" (a $0 "sample" is the same
+// $0-orderable defect). The real $4.25 memo sample is unaffected and keeps its existing quantity.
+function safeQuantities(product, variants, locationId, desired) {
+ return (variants || []).map(v => ({
+ inventoryItemId: v.inventoryItem.id,
+ locationId,
+ quantity: Number(v.price) > 0
+ ? safeStampQuantity({ title: v.title, price: v.price }, product, desired)
+ : 0,
+ }));
+}
+// ── GUARD TK-11357 END ────────────────────────────────────────────
+
const M_TRACK = `mutation($id:ID!){ inventoryItemUpdate(id:$id, input:{tracked:true}){ userErrors{message} } }`;
const M_ACTIVATE = `mutation($iid:ID!,$loc:ID!){ inventoryActivate(inventoryItemId:$iid, locationId:$loc){ userErrors{message} } }`;
const M_SETQTY = `mutation($input:InventorySetQuantitiesInput!){ inventorySetQuantities(input:$input){ userErrors{message code} } }`;
@@ -91,14 +114,16 @@ async function goLive(pid, sku, mfrSku) {
const d = await gql(Q_VARIANTS, { id: gidP });
if (!d?.product) return { missing: true };
if (d.product.status === 'ACTIVE') { dbWriteBack(sku, mfrSku, pid, d.product.handle); return { skipped: true }; }
- const items = d.product.variants.edges.map(e => e.node.inventoryItem.id);
+ const vnodes = d.product.variants.edges.map(e => e.node);
+ const items = vnodes.map(v => v.inventoryItem.id); // track+activate: unchanged
+ const quantities = safeQuantities(d.product, vnodes, LOCATION_ID, TARGET_QTY); // GUARD TK-11357
for (const iid of items) {
let r = await gql(M_TRACK, { id: iid }); (r.inventoryItemUpdate?.userErrors || []).forEach(e => errs.push('track:' + e.message));
r = await gql(M_ACTIVATE, { iid, loc: LOCATION_ID });
(r.inventoryActivate?.userErrors || []).filter(e => !/already/i.test(e.message)).forEach(e => errs.push('activate:' + e.message));
}
const r2 = await gql(M_SETQTY, { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true,
- quantities: items.map(iid => ({ inventoryItemId: iid, locationId: LOCATION_ID, quantity: TARGET_QTY })) } });
+ quantities } }); // GUARD TK-11357
(r2.inventorySetQuantities?.userErrors || []).forEach(e => errs.push('setqty:' + e.message));
const r3 = await gql(M_PUBLISH, { id: gidP, pubs: PUBLICATIONS.filter(_pid => _pid !== 29646651457 || !_isShowroomGuard("Mural Source")).map(p => ({ publicationId: p })) });
(r3.publishablePublish?.userErrors || []).forEach(e => errs.push('publish:' + e.message));
diff --git a/scripts/osborne-onboard/go-live.mjs b/scripts/osborne-onboard/go-live.mjs
index a98d77c..6608384 100644
--- a/scripts/osborne-onboard/go-live.mjs
+++ b/scripts/osborne-onboard/go-live.mjs
@@ -51,7 +51,30 @@ async function gql(query, variables = {}) {
} catch (e) { if (a === 3) throw e; await sleep(900 * (a + 1)); }
}
}
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status variants(first:10){edges{node{inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status vendor tags variants(first:10){edges{node{title price inventoryItem{id}}}}}}`;
+import { safeStampQuantity } from '../lib/inventory-stamp-guard.mjs'; // GUARD TK-11357 (shared guard)
+// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
+// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
+// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
+// 11140 -> 11299 -> 11301 -> 11357). $0 is the LIVE theme's deliberate quote-only SENTINEL
+// (snippets/product-form-content.liquid renders the "Contact Us" button iff variant.price == 0),
+// so the remedy is NEVER to write a placeholder price - it is "do not stock it".
+// Steve's 2026-06-20 "active products are never out of stock" rule is PRESERVED for PRICED goods:
+// a priced variant still gets `desired`. The quote-only tag/vendor decision is delegated to the
+// shared guard (lib/inventory-stamp-guard.mjs); this adds one strictly-safer rule of its own -
+// price <= 0 / NaN is ALWAYS 0, even on a variant labelled "Sample" (a $0 "sample" is the same
+// $0-orderable defect). The real $4.25 memo sample is unaffected and keeps its existing quantity.
+function safeQuantities(product, variants, locationId, desired) {
+ return (variants || []).map(v => ({
+ inventoryItemId: v.inventoryItem.id,
+ locationId,
+ quantity: Number(v.price) > 0
+ ? safeStampQuantity({ title: v.title, price: v.price }, product, desired)
+ : 0,
+ }));
+}
+// ── GUARD TK-11357 END ────────────────────────────────────────────
+
const M_TRACK = `mutation($id:ID!){ inventoryItemUpdate(id:$id, input:{tracked:true}){ userErrors{message} } }`;
const M_ACTIVATE = `mutation($iid:ID!,$loc:ID!){ inventoryActivate(inventoryItemId:$iid, locationId:$loc){ userErrors{message} } }`;
const M_SETQTY = `mutation($input:InventorySetQuantitiesInput!){ inventorySetQuantities(input:$input){ userErrors{message code} } }`;
@@ -64,14 +87,16 @@ async function goLive(pid) {
const d = await gql(Q_VARIANTS, { id: gidP });
if (!d?.product) return { missing: true };
if (d.product.status === 'ACTIVE') return { skipped: true };
- const items = d.product.variants.edges.map(e => e.node.inventoryItem.id);
+ const vnodes = d.product.variants.edges.map(e => e.node);
+ const items = vnodes.map(v => v.inventoryItem.id); // track+activate: unchanged
+ const quantities = safeQuantities(d.product, vnodes, LOCATION_ID, TARGET_QTY); // GUARD TK-11357
for (const iid of items) {
let r = await gql(M_TRACK, { id: iid }); (r.inventoryItemUpdate?.userErrors || []).forEach(e => errs.push('track:' + e.message));
r = await gql(M_ACTIVATE, { iid, loc: LOCATION_ID });
(r.inventoryActivate?.userErrors || []).filter(e => !/already/i.test(e.message)).forEach(e => errs.push('activate:' + e.message));
}
const r2 = await gql(M_SETQTY, { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true,
- quantities: items.map(iid => ({ inventoryItemId: iid, locationId: LOCATION_ID, quantity: TARGET_QTY })) } });
+ quantities } }); // GUARD TK-11357
(r2.inventorySetQuantities?.userErrors || []).forEach(e => errs.push('setqty:' + e.message));
const r3 = await gql(M_PUBLISH, { id: gidP, pubs: PUBLICATIONS.filter(_pid => _pid !== 29646651457 || !_isShowroomGuard("Osborne & Little")).map(p => ({ publicationId: p })) });
(r3.publishablePublish?.userErrors || []).forEach(e => errs.push('publish:' + e.message));
diff --git a/scripts/stout-onboard/go-live.mjs b/scripts/stout-onboard/go-live.mjs
index 89c606d..f429d8d 100644
--- a/scripts/stout-onboard/go-live.mjs
+++ b/scripts/stout-onboard/go-live.mjs
@@ -48,7 +48,30 @@ const PUBLICATIONS = [
].map(n => `gid://shopify/Publication/${n}`);
const sleep = ms => new Promise(r => setTimeout(r, ms));
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle variants(first:10){edges{node{inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle vendor tags variants(first:10){edges{node{title price inventoryItem{id}}}}}}`;
+import { safeStampQuantity } from '../lib/inventory-stamp-guard.mjs'; // GUARD TK-11357 (shared guard)
+// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
+// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
+// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
+// 11140 -> 11299 -> 11301 -> 11357). $0 is the LIVE theme's deliberate quote-only SENTINEL
+// (snippets/product-form-content.liquid renders the "Contact Us" button iff variant.price == 0),
+// so the remedy is NEVER to write a placeholder price - it is "do not stock it".
+// Steve's 2026-06-20 "active products are never out of stock" rule is PRESERVED for PRICED goods:
+// a priced variant still gets `desired`. The quote-only tag/vendor decision is delegated to the
+// shared guard (lib/inventory-stamp-guard.mjs); this adds one strictly-safer rule of its own -
+// price <= 0 / NaN is ALWAYS 0, even on a variant labelled "Sample" (a $0 "sample" is the same
+// $0-orderable defect). The real $4.25 memo sample is unaffected and keeps its existing quantity.
+function safeQuantities(product, variants, locationId, desired) {
+ return (variants || []).map(v => ({
+ inventoryItemId: v.inventoryItem.id,
+ locationId,
+ quantity: Number(v.price) > 0
+ ? safeStampQuantity({ title: v.title, price: v.price }, product, desired)
+ : 0,
+ }));
+}
+// ── GUARD TK-11357 END ────────────────────────────────────────────
+
const M_TRACK = `mutation($id:ID!){ inventoryItemUpdate(id:$id, input:{tracked:true}){ userErrors{message} } }`;
const M_ACTIVATE = `mutation($iid:ID!,$loc:ID!){ inventoryActivate(inventoryItemId:$iid, locationId:$loc){ userErrors{message} } }`;
const M_SETQTY = `mutation($input:InventorySetQuantitiesInput!){ inventorySetQuantities(input:$input){ userErrors{message code} } }`;
@@ -94,14 +117,16 @@ async function goLive(pid, sku, mfrSku) {
const d = await gql(Q_VARIANTS, { id: gidP });
if (!d?.product) return { missing: true };
if (d.product.status === 'ACTIVE') { dbWriteBack(sku, mfrSku, pid, d.product.handle); return { skipped: true }; }
- const items = d.product.variants.edges.map(e => e.node.inventoryItem.id);
+ const vnodes = d.product.variants.edges.map(e => e.node);
+ const items = vnodes.map(v => v.inventoryItem.id); // track+activate: unchanged
+ const quantities = safeQuantities(d.product, vnodes, LOCATION_ID, TARGET_QTY); // GUARD TK-11357
for (const iid of items) {
let r = await gql(M_TRACK, { id: iid }); (r.inventoryItemUpdate?.userErrors || []).forEach(e => errs.push('track:' + e.message));
r = await gql(M_ACTIVATE, { iid, loc: LOCATION_ID });
(r.inventoryActivate?.userErrors || []).filter(e => !/already/i.test(e.message)).forEach(e => errs.push('activate:' + e.message));
}
const r2 = await gql(M_SETQTY, { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true,
- quantities: items.map(iid => ({ inventoryItemId: iid, locationId: LOCATION_ID, quantity: TARGET_QTY })) } });
+ quantities } }); // GUARD TK-11357
(r2.inventorySetQuantities?.userErrors || []).forEach(e => errs.push('setqty:' + e.message));
const r3 = await gql(M_PUBLISH, { id: gidP, pubs: PUBLICATIONS.filter(_pid => _pid !== 29646651457 || !_isShowroomGuard("Stout")).map(p => ({ publicationId: p })) });
(r3.publishablePublish?.userErrors || []).forEach(e => errs.push('publish:' + e.message));
diff --git a/scripts/stroheim-onboard/go-live.mjs b/scripts/stroheim-onboard/go-live.mjs
index fcb3a62..9985a26 100644
--- a/scripts/stroheim-onboard/go-live.mjs
+++ b/scripts/stroheim-onboard/go-live.mjs
@@ -50,7 +50,30 @@ const PUBLICATIONS = [
].map(n => `gid://shopify/Publication/${n}`);
const sleep = ms => new Promise(r => setTimeout(r, ms));
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle variants(first:10){edges{node{inventoryItem{id}}}}}}`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ status handle vendor tags variants(first:10){edges{node{title price inventoryItem{id}}}}}}`;
+import { safeStampQuantity } from '../lib/inventory-stamp-guard.mjs'; // GUARD TK-11357 (shared guard)
+// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
+// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
+// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
+// 11140 -> 11299 -> 11301 -> 11357). $0 is the LIVE theme's deliberate quote-only SENTINEL
+// (snippets/product-form-content.liquid renders the "Contact Us" button iff variant.price == 0),
+// so the remedy is NEVER to write a placeholder price - it is "do not stock it".
+// Steve's 2026-06-20 "active products are never out of stock" rule is PRESERVED for PRICED goods:
+// a priced variant still gets `desired`. The quote-only tag/vendor decision is delegated to the
+// shared guard (lib/inventory-stamp-guard.mjs); this adds one strictly-safer rule of its own -
+// price <= 0 / NaN is ALWAYS 0, even on a variant labelled "Sample" (a $0 "sample" is the same
+// $0-orderable defect). The real $4.25 memo sample is unaffected and keeps its existing quantity.
+function safeQuantities(product, variants, locationId, desired) {
+ return (variants || []).map(v => ({
+ inventoryItemId: v.inventoryItem.id,
+ locationId,
+ quantity: Number(v.price) > 0
+ ? safeStampQuantity({ title: v.title, price: v.price }, product, desired)
+ : 0,
+ }));
+}
+// ── GUARD TK-11357 END ────────────────────────────────────────────
+
const M_TRACK = `mutation($id:ID!){ inventoryItemUpdate(id:$id, input:{tracked:true}){ userErrors{message} } }`;
const M_ACTIVATE = `mutation($iid:ID!,$loc:ID!){ inventoryActivate(inventoryItemId:$iid, locationId:$loc){ userErrors{message} } }`;
const M_SETQTY = `mutation($input:InventorySetQuantitiesInput!){ inventorySetQuantities(input:$input){ userErrors{message code} } }`;
@@ -95,14 +118,16 @@ async function goLive(pid, sku, mfrSku) {
const d = await gql(Q_VARIANTS, { id: gidP });
if (!d?.product) return { missing: true };
if (d.product.status === 'ACTIVE') { dbWriteBack(sku, mfrSku, pid, d.product.handle); return { skipped: true }; }
- const items = d.product.variants.edges.map(e => e.node.inventoryItem.id);
+ const vnodes = d.product.variants.edges.map(e => e.node);
+ const items = vnodes.map(v => v.inventoryItem.id); // track+activate: unchanged
+ const quantities = safeQuantities(d.product, vnodes, LOCATION_ID, TARGET_QTY); // GUARD TK-11357
for (const iid of items) {
let r = await gql(M_TRACK, { id: iid }); (r.inventoryItemUpdate?.userErrors || []).forEach(e => errs.push('track:' + e.message));
r = await gql(M_ACTIVATE, { iid, loc: LOCATION_ID });
(r.inventoryActivate?.userErrors || []).filter(e => !/already/i.test(e.message)).forEach(e => errs.push('activate:' + e.message));
}
const r2 = await gql(M_SETQTY, { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true,
- quantities: items.map(iid => ({ inventoryItemId: iid, locationId: LOCATION_ID, quantity: TARGET_QTY })) } });
+ quantities } }); // GUARD TK-11357
(r2.inventorySetQuantities?.userErrors || []).forEach(e => errs.push('setqty:' + e.message));
const r3 = await gql(M_PUBLISH, { id: gidP, pubs: PUBLICATIONS.filter(_pid => _pid !== 29646651457 || !_isShowroomGuard("Stroheim")).map(p => ({ publicationId: p })) });
(r3.publishablePublish?.userErrors || []).forEach(e => errs.push('publish:' + e.message));
diff --git a/scripts/tres-tintas-onboard/go-live-tres-tintas.js b/scripts/tres-tintas-onboard/go-live-tres-tintas.js
index a277757..2be4bb8 100644
--- a/scripts/tres-tintas-onboard/go-live-tres-tintas.js
+++ b/scripts/tres-tintas-onboard/go-live-tres-tintas.js
@@ -58,7 +58,30 @@ async function gql(query, variables = {}) {
} catch (e) { if (a === 3) throw e; await sleep(900 * (a + 1)); }
}
}
-const Q_V = `query($id:ID!){ product(id:$id){ status variants(first:10){edges{node{id inventoryItem{id}}}} } }`;
+const Q_V = `query($id:ID!){ product(id:$id){ status vendor tags variants(first:10){edges{node{id title price inventoryItem{id}}}} } }`;
+const { safeStampQuantity } = require('../lib/inventory-stamp-guard.mjs'); // GUARD TK-11357 (shared guard)
+// ── GUARD TK-11357 BEGIN ─ do not edit without re-running the fixture proof ──────────
+// A $0 / quote-only sellable variant must NEVER receive positive stock: positive stock is what
+// flips availableForSale=true, making it checkout-orderable at $0 (lineage TK-10825 -> 10965 ->
+// 11140 -> 11299 -> 11301 -> 11357). $0 is the LIVE theme's deliberate quote-only SENTINEL
+// (snippets/product-form-content.liquid renders the "Contact Us" button iff variant.price == 0),
+// so the remedy is NEVER to write a placeholder price - it is "do not stock it".
+// Steve's 2026-06-20 "active products are never out of stock" rule is PRESERVED for PRICED goods:
+// a priced variant still gets `desired`. The quote-only tag/vendor decision is delegated to the
+// shared guard (lib/inventory-stamp-guard.mjs); this adds one strictly-safer rule of its own -
+// price <= 0 / NaN is ALWAYS 0, even on a variant labelled "Sample" (a $0 "sample" is the same
+// $0-orderable defect). The real $4.25 memo sample is unaffected and keeps its existing quantity.
+function safeQuantities(product, variants, locationId, desired) {
+ return (variants || []).map(v => ({
+ inventoryItemId: v.inventoryItem.id,
+ locationId,
+ quantity: Number(v.price) > 0
+ ? safeStampQuantity({ title: v.title, price: v.price }, product, desired)
+ : 0,
+ }));
+}
+// ── GUARD TK-11357 END ────────────────────────────────────────────
+
const M_TRACK = `mutation($id:ID!){ inventoryItemUpdate(id:$id, input:{tracked:true}){ userErrors{message} } }`;
const M_ACT = `mutation($iid:ID!,$loc:ID!){ inventoryActivate(inventoryItemId:$iid, locationId:$loc){ userErrors{message} } }`;
const M_QTY = `mutation($input:InventorySetQuantitiesInput!){ inventorySetQuantities(input:$input){ userErrors{message code} } }`;
@@ -71,14 +94,16 @@ async function goLive(pid) {
const d = await gql(Q_V, { id: gid });
if (!d.product) return { errs: ['not found'] };
if (d.product.status === 'ACTIVE') return { skipped: true };
- const items = d.product.variants.edges.map(e => e.node.inventoryItem.id);
+ const vnodes = d.product.variants.edges.map(e => e.node);
+ const items = vnodes.map(v => v.inventoryItem.id); // track+activate: unchanged
+ const quantities = safeQuantities(d.product, vnodes, LOCATION_ID, TARGET_QTY); // GUARD TK-11357
for (const iid of items) {
let r = await gql(M_TRACK, { id: iid }); (r.inventoryItemUpdate?.userErrors || []).forEach(e => errs.push('track:' + e.message));
r = await gql(M_ACT, { iid, loc: LOCATION_ID });
(r.inventoryActivate?.userErrors || []).filter(e => !/already/i.test(e.message)).forEach(e => errs.push('act:' + e.message));
}
const rq = await gql(M_QTY, { input: { name: 'on_hand', reason: 'correction', ignoreCompareQuantity: true,
- quantities: items.map(iid => ({ inventoryItemId: iid, locationId: LOCATION_ID, quantity: TARGET_QTY })) } });
+ quantities } }); // GUARD TK-11357
(rq.inventorySetQuantities?.userErrors || []).forEach(e => errs.push('qty:' + e.message));
const rp = await gql(M_PUB, { id: gid, pubs: PUBLICATIONS.filter(_pid => _pid !== 29646651457 || !_isShowroomGuard("Tres Tintas")).map(p => ({ publicationId: p })) });
(rp.publishablePublish?.userErrors || []).forEach(e => errs.push('pub:' + e.message));
← d5d3db4 TK-10895: hardened Tranche B executor for the verified 269
·
back to Designerwallcoverings
·
guard TK-11357 Fix D: per-variant $0 gate in the two activat 2fe073d →