[object Object]

← back to Designer Wallcoverings

TK-11539: apply weight guard to Schumacher sample-variant chokepoint

335c4269a1eb4d3e43924c3c5f8d17176a751eca · 2026-09-15 08:40:24 -0700 · Steve

Create the Sample via productVariantsBulkCreate with inventoryItemWithWeight so
it never ships zero-weight (mirrors lib/shopify.ts; prevents TK-11414 recurrence
on this standalone addSampleVariant path). tracked flag is single-sourced so the
price gate's orderable and the write's tracked cannot drift.

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

Files touched

Diff

commit 335c4269a1eb4d3e43924c3c5f8d17176a751eca
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Sep 15 08:40:24 2026 -0700

    TK-11539: apply weight guard to Schumacher sample-variant chokepoint
    
    Create the Sample via productVariantsBulkCreate with inventoryItemWithWeight so
    it never ships zero-weight (mirrors lib/shopify.ts; prevents TK-11414 recurrence
    on this standalone addSampleVariant path). tracked flag is single-sourced so the
    price gate's orderable and the write's tracked cannot drift.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .../add-sample-variant-schumacher.ts               | 76 ++++++++++++++++++----
 1 file changed, 64 insertions(+), 12 deletions(-)

diff --git a/DW-Programming/ImportNewSkufromURL/add-sample-variant-schumacher.ts b/DW-Programming/ImportNewSkufromURL/add-sample-variant-schumacher.ts
index 18ca08f9..ec0ffe1e 100644
--- a/DW-Programming/ImportNewSkufromURL/add-sample-variant-schumacher.ts
+++ b/DW-Programming/ImportNewSkufromURL/add-sample-variant-schumacher.ts
@@ -4,10 +4,19 @@
 
 import * as dotenv from 'dotenv';
 import * as path from 'path';
+// Price-integrity gate (TK-11403) — shared assertion before the variant price write.
+import { assertPriceIntegrity, resolveSampleFloor } from './lib/price-integrity-gate';
+import { recordOutcome } from './lib/price-integrity-record';
+// Weight guard (TK-11539) — this standalone addSampleVariant chokepoint must also inject a
+// positive inventoryItem.measurement.weight, or the Sample variant it creates ships zero-weight
+// (the exact TK-11414 recurrence the shared engine already fixed in lib/shopify.ts).
+import { inventoryItemWithWeight } from './lib/weight-guard';
 
 // Load .env.local file
 dotenv.config({ path: path.join(__dirname, '.env.local') });
 
+const VENDOR = 'Schumacher';
+
 interface Product {
   id: string;
   title: string;
@@ -148,11 +157,13 @@ async function addSampleVariant(productId: string, productTitle: string): Promis
       return false;
     }
 
-    // Now create the Sample variant
+    // Now create the Sample variant.
+    // TK-11539: use productVariantsBulkCreate (not the deprecated productVariantCreate) so we can
+    // attach inventoryItem.measurement.weight via the shared weight guard — mirrors lib/shopify.ts.
     const variantMutation = `
-      mutation productVariantCreate($input: ProductVariantInput!) {
-        productVariantCreate(input: $input) {
-          productVariant {
+      mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
+        productVariantsBulkCreate(productId: $productId, variants: $variants) {
+          productVariants {
             id
             title
             price
@@ -165,6 +176,40 @@ async function addSampleVariant(productId: string, productTitle: string): Promis
       }
     `;
 
+    // ---- PRICE-INTEGRITY GATE (TK-11403) — BEFORE the variant price write ----
+    // This site only writes a Sample variant at $4.25; the shared assertion
+    // guards Class A (sample must == $4.25). FAIL-SAFE: never throws out of the
+    // path; a real violation blocks the write, an infra error just proceeds.
+    // TK-11403: per-vendor declared sample price (default $4.25). Same value gates AND is written.
+    const sampleFloor = resolveSampleFloor(VENDOR);
+    // Single source of truth for the Sample variant's inventory tracking, so the
+    // gate's `orderable` and the write's `tracked` can never drift apart. The
+    // Sample is written UNTRACKED (DW convention) → Shopify sells it at checkout
+    // regardless of inventory, i.e. it IS orderable. Telling the gate `orderable:
+    // true` keeps its Class-C $0-orderable guard armed for this path (a future $0
+    // sample would be caught) instead of silently disarmed by an `orderable:false`
+    // that contradicts what ships.
+    const SAMPLE_TRACKED = false;
+    const sampleOrderable = !SAMPLE_TRACKED; // untracked ⇒ sellable at checkout
+    try {
+      const gate = assertPriceIntegrity({
+        dwSku: productId,
+        netCost: null, // no sellable price written here → cost not needed
+        sampleFloor,
+        variants: [{ role: 'sample', price: sampleFloor, orderable: sampleOrderable, sku: `${productId}-Sample` }],
+      });
+      if (gate.warnings.length) {
+        recordOutcome({ vendor: VENDOR, dwSku: productId, outcome: 'warn', codes: gate.warnings.map(w => w.code) });
+      }
+      if (!gate.ok) {
+        console.log(`  ⛔ price_integrity_block: ${JSON.stringify(gate.violations.map(v => v.code))}`);
+        recordOutcome({ vendor: VENDOR, dwSku: productId, outcome: 'block', codes: gate.violations.map(v => v.code) });
+        return false; // do NOT write the bad price
+      }
+    } catch (gateErr) {
+      console.log(`  ⚠️ price_integrity_gate_error (proceeding): ${gateErr instanceof Error ? gateErr.message : String(gateErr)}`);
+    }
+
     const variantResponse = await fetch(GRAPHQL_ENDPOINT, {
       method: 'POST',
       headers: {
@@ -174,23 +219,30 @@ async function addSampleVariant(productId: string, productTitle: string): Promis
       body: JSON.stringify({
         query: variantMutation,
         variables: {
-          input: {
-            productId: productId,
-            options: ['Sample'],
-            price: '4.25',
-          },
+          productId,
+          variants: [
+            {
+              // TK-11403: per-vendor declared sample price ($4.25 for Schumacher).
+              price: String(sampleFloor),
+              optionValues: [{ name: 'Sample', optionName: 'Size' }],
+              // TK-11539: positive measurement.weight (0.25 lb) so the Sample variant is never
+              // created zero-weight. SAMPLE_TRACKED (untracked) matches DW's convention AND is the
+              // same flag the price-integrity gate's `orderable` is derived from above.
+              inventoryItem: inventoryItemWithWeight({ tracked: SAMPLE_TRACKED }, { role: 'sample' }),
+            },
+          ],
         },
       }),
     });
 
     const variantResult: any = await variantResponse.json();
 
-    if (variantResult.errors || variantResult.data?.productVariantCreate?.userErrors?.length > 0) {
-      console.log(`  ⚠️  Error creating variant: ${JSON.stringify(variantResult.errors || variantResult.data.productVariantCreate.userErrors)}`);
+    if (variantResult.errors || variantResult.data?.productVariantsBulkCreate?.userErrors?.length > 0) {
+      console.log(`  ⚠️  Error creating variant: ${JSON.stringify(variantResult.errors || variantResult.data.productVariantsBulkCreate.userErrors)}`);
       return false;
     }
 
-    console.log(`  ✅ Sample variant created ($4.25)`);
+    console.log(`  ✅ Sample variant created ($${sampleFloor})`);
     return true;
   } catch (error) {
     console.log(`  ❌ Error: ${error}`);

← 7c89163b docs: correct stale --dry-run comment across 9 write-scripts  ·  back to Designer Wallcoverings  ·  auto-data-snapshot: 2026-09-15T08:44:47 (4 data files) — DW- 56b3577d →