[object Object]

← back to Designerwallcoverings

guard TK-11357 Fix D: don't stock a $0 Roll in osborne roll-add-apply

077ac096854eaf789013afaaf0efb9ed789adb82 · 2026-09-10 09:27:04 -0700 · Steve

PRE-EXISTING GUARD STATE (verified): this file DID already have a price guard, but it protects
the wrong side — the 2026-06-18 belt-and-suspenders check only refuses to overwrite a SAMPLE
whose live price isn't $4.25. Nothing checked the price of the Roll variant it CREATES: it
creates it at `price: String(r.retail)` straight from out/roll-add-plan.jsonl and then
unconditionally sets that new Roll to 2026. A null/0/absent `retail` in the plan therefore
produces a $0 Roll that is immediately stocked = orderable at $0.

FIX: judge the price Shopify ACTUALLY landed (M_CREATE now returns `title price`, not just
id/sku) through the shared guard, and skip the inventory write entirely when the safe quantity
is 0, logging the refusal. Q_VARIANTS also now carries `vendor tags` so the guard's quote-only
tag/vendor rule has real inputs.

Proven by shopify/scripts/tk11357-source-fix-proof/predicate-proof.mjs: 4/4 + 3/3.
SOURCE-ONLY. Reversible: git revert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files touched

Diff

commit 077ac096854eaf789013afaaf0efb9ed789adb82
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Sep 10 09:27:04 2026 -0700

    guard TK-11357 Fix D: don't stock a $0 Roll in osborne roll-add-apply
    
    PRE-EXISTING GUARD STATE (verified): this file DID already have a price guard, but it protects
    the wrong side — the 2026-06-18 belt-and-suspenders check only refuses to overwrite a SAMPLE
    whose live price isn't $4.25. Nothing checked the price of the Roll variant it CREATES: it
    creates it at `price: String(r.retail)` straight from out/roll-add-plan.jsonl and then
    unconditionally sets that new Roll to 2026. A null/0/absent `retail` in the plan therefore
    produces a $0 Roll that is immediately stocked = orderable at $0.
    
    FIX: judge the price Shopify ACTUALLY landed (M_CREATE now returns `title price`, not just
    id/sku) through the shared guard, and skip the inventory write entirely when the safe quantity
    is 0, logging the refusal. Q_VARIANTS also now carries `vendor tags` so the guard's quote-only
    tag/vendor rule has real inputs.
    
    Proven by shopify/scripts/tk11357-source-fix-proof/predicate-proof.mjs: 4/4 + 3/3.
    SOURCE-ONLY. Reversible: git revert.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 scripts/osborne-onboard/roll-add-apply.mjs | 39 ++++++++++++++++++++++++++----
 1 file changed, 34 insertions(+), 5 deletions(-)

diff --git a/scripts/osborne-onboard/roll-add-apply.mjs b/scripts/osborne-onboard/roll-add-apply.mjs
index 7f1e496..991ca86 100644
--- a/scripts/osborne-onboard/roll-add-apply.mjs
+++ b/scripts/osborne-onboard/roll-add-apply.mjs
@@ -43,13 +43,36 @@ async function gql(query, variables) {
 }
 const capHit = s => /exceed|limit|too many|maximum/i.test(s) && /variant|product|day/i.test(s);
 
-const Q_VARIANTS = `query($id:ID!){ product(id:$id){ id
-  variants(first:10){ nodes{ id sku price selectedOptions{name value} inventoryItem{id} } } } }`;
+const Q_VARIANTS = `query($id:ID!){ product(id:$id){ id vendor tags
+  variants(first:10){ nodes{ id sku title price selectedOptions{name value} 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_RENAME = `mutation($pid:ID!,$v:[ProductVariantsBulkInput!]!){
   productVariantsBulkUpdate(productId:$pid, variants:$v){ userErrors{field message} } }`;
 const M_CREATE = `mutation($pid:ID!,$v:[ProductVariantsBulkInput!]!){
   productVariantsBulkCreate(productId:$pid, variants:$v){
-    productVariants{ id sku inventoryItem{id} } userErrors{field message} } }`;
+    productVariants{ id sku title price inventoryItem{id} } userErrors{field message} } }`;
 const M_INV = `mutation($input:InventorySetQuantitiesInput!){
   inventorySetQuantities(input:$input){ userErrors{field message} } }`;
 
@@ -108,9 +131,15 @@ async function main() {
       }
       const rollVar = cre.productVariantsBulkCreate.productVariants[0];
       // 3. inventory 2026 on the new Roll
+      // GUARD TK-11357: the pre-existing $4.25-sample guard above protects the SAMPLE's price, but
+      // nothing checked that r.retail (the plan's price for the NEW Roll) is > 0 — a null/0 retail
+      // would create a $0 Roll and then stock it 2026 = orderable at $0. Judge the price Shopify
+      // actually landed, not the plan's field, and skip the write when the safe quantity is 0.
       if (rollVar?.inventoryItem?.id) {
-        await gql(M_INV, { input: { name: 'available', reason: 'correction', ignoreCompareQuantity: true,
-          quantities: [{ inventoryItemId: rollVar.inventoryItem.id, locationId: LOCATION, quantity: 2026 }] } });
+        const quantities = safeQuantities(cur?.product, [rollVar], LOCATION, 2026)
+          .filter(q => q.quantity > 0);
+        if (quantities.length) await gql(M_INV, { input: { name: 'available', reason: 'correction', ignoreCompareQuantity: true, quantities } });
+        else console.error(`  ⛔ ${r.wcode}: new Roll priced $${rollVar.price} — NOT stocked (TK-11357 $0-orderable guard)`);
       }
       fs.writeSync(fd, JSON.stringify({ product_id: r.product_id, wcode: r.wcode, roll_sku: `${r.wcode}-OSB`, retail: r.retail, roll_variant: rollVar?.id, status: 'roll-added', at: new Date().toISOString() }) + '\n');
       ok++;

← 2fe073d guard TK-11357 Fix D: per-variant $0 gate in the two activat  ·  back to Designerwallcoverings  ·  chore: lint, refactor, v0.1.12 (session close) — TK-11307 6e51d27 →