[object Object]

← back to Designer Wallcoverings

TK-11857 R1/R4: price-integrity gate fails closed on non-boolean orderable; shopify.ts uses real product_type

76f11f71037c82b0c74a6924be26768383cbab70 · 2026-09-22 13:35:26 -0700 · Steve

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3ChReG53fwpNgUESv4SY7

Files touched

Diff

commit 76f11f71037c82b0c74a6924be26768383cbab70
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Sep 22 13:35:26 2026 -0700

    TK-11857 R1/R4: price-integrity gate fails closed on non-boolean orderable; shopify.ts uses real product_type
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01G3ChReG53fwpNgUESv4SY7
---
 .../__tests__/price-integrity-gate.test.ts         | 76 ++++++++++++++++++++++
 .../lib/price-integrity-gate.ts                    | 14 +++-
 DW-Programming/ImportNewSkufromURL/lib/shopify.ts  |  9 ++-
 3 files changed, 96 insertions(+), 3 deletions(-)

diff --git a/DW-Programming/ImportNewSkufromURL/__tests__/price-integrity-gate.test.ts b/DW-Programming/ImportNewSkufromURL/__tests__/price-integrity-gate.test.ts
index cb1b8ce1..1bec3712 100644
--- a/DW-Programming/ImportNewSkufromURL/__tests__/price-integrity-gate.test.ts
+++ b/DW-Programming/ImportNewSkufromURL/__tests__/price-integrity-gate.test.ts
@@ -202,6 +202,31 @@ describe('assertPriceIntegrity', () => {
     const r = assertPriceIntegrity(clean);
     expect(r.warnings).toHaveLength(0);
   });
+
+  // ---- 15/16/17. NEGATIVE class C (empty/missing variants = fail-OPEN) --------
+  // TK-11357 (vp-engineering review 2026-09-14): the gate ran to completion on a
+  // zero-variant input having checked nothing and returned ok:true — a malformed
+  // payload sailed straight through. These prove it now fails CLOSED whether the
+  // array is empty, the field is absent, or the field is a non-array. (RED-goes-
+  // on-injected-fault, CLAUDE.md TK-11431 rule 3.)
+  it('15. empty variants array → ok:false, class C empty-variants (was fail-open)', () => {
+    const r = assertPriceIntegrity({ dwSku: 'DWX-1', netCost: 30, variants: [] });
+    expect(r.ok).toBe(false);
+    expect(r.violations.some(v => v.class === 'C' && v.code === 'empty-variants')).toBe(true);
+  });
+
+  it('16. missing variants field → ok:false, class C empty-variants', () => {
+    // Cast: exercises the runtime guard against a caller that omits `variants`.
+    const r = assertPriceIntegrity({ dwSku: 'DWX-1', netCost: 30 } as unknown as PriceIntegrityInput);
+    expect(r.ok).toBe(false);
+    expect(r.violations.some(v => v.code === 'empty-variants')).toBe(true);
+  });
+
+  it('17. non-array variants → ok:false, class C empty-variants (no throw)', () => {
+    const r = assertPriceIntegrity({ dwSku: 'DWX-1', netCost: 30, variants: null } as unknown as PriceIntegrityInput);
+    expect(r.ok).toBe(false);
+    expect(r.violations.some(v => v.code === 'empty-variants')).toBe(true);
+  });
 });
 
 describe('enforcePriceIntegrity', () => {
@@ -224,3 +249,54 @@ describe('enforcePriceIntegrity', () => {
     expect((caught as PriceIntegrityError).violations.length).toBeGreaterThan(0);
   });
 });
+
+// ============================================================================
+// TK-11857 R1 — FAIL-CLOSED on a NON-BOOLEAN `orderable`.
+// The VariantCheck type says boolean, but untyped JS callers (import-queue-runner.js)
+// can pass undefined / 1 / 'true' at runtime. The old strict `=== true` fail-OPENED:
+// a $0 variant with a non-boolean orderable SKIPPED the Class-C block. These are the
+// negative tests (CLAUDE.md TK-11431 amendment 3): they INJECT that fault and prove
+// the gate goes RED. Revert `!== false` back to `=== true` and both go GREEN→RED.
+// ============================================================================
+describe('assertPriceIntegrity — R1 non-boolean orderable fails CLOSED', () => {
+  it('R1a. sellable $0 with orderable:undefined → ok:false, class C (was fail-open)', () => {
+    const r = assertPriceIntegrity({
+      dwSku: 'DWX-R1a',
+      netCost: 30,
+      // orderable omitted → undefined at runtime, exactly what an untyped JS caller sends
+      variants: [
+        { role: 'sample', price: 4.25, orderable: false },
+        { role: 'sellable', price: 0 } as any,
+      ],
+    });
+    expect(r.ok).toBe(false);
+    expect(r.violations.some(v => v.class === 'C' && v.code === 'zero-price-orderable')).toBe(true);
+  });
+
+  it('R1b. sellable $2 (cost unknown) with orderable:1 (truthy non-bool) → ok:false, class B floor', () => {
+    const r = assertPriceIntegrity({
+      dwSku: 'DWX-R1b',
+      netCost: null,
+      variants: [
+        { role: 'sample', price: 4.25, orderable: false },
+        { role: 'sellable', price: 2, orderable: 1 as any },
+      ],
+    });
+    expect(r.ok).toBe(false);
+    expect(r.violations.some(v => v.class === 'B' && v.code === 'below-absolute-floor')).toBe(true);
+  });
+
+  it('R1c. explicitly-non-orderable ($0 orderable:false) is still NOT blocked (no over-block)', () => {
+    const r = assertPriceIntegrity({
+      dwSku: 'DWX-R1c',
+      netCost: 30,
+      variants: [
+        { role: 'sample', price: 4.25, orderable: false },
+        { role: 'sellable', price: 59, orderable: true },
+        { role: 'sample', price: 0, orderable: false }, // genuinely non-orderable $0 → fine
+      ],
+    });
+    // no class-C zero-price-orderable finding for the explicitly non-orderable $0 variant
+    expect(r.violations.some(v => v.code === 'zero-price-orderable')).toBe(false);
+  });
+});
diff --git a/DW-Programming/ImportNewSkufromURL/lib/price-integrity-gate.ts b/DW-Programming/ImportNewSkufromURL/lib/price-integrity-gate.ts
index 2e980b1e..44f9f7c9 100644
--- a/DW-Programming/ImportNewSkufromURL/lib/price-integrity-gate.ts
+++ b/DW-Programming/ImportNewSkufromURL/lib/price-integrity-gate.ts
@@ -419,7 +419,15 @@ export function assertPriceIntegrity(input: PriceIntegrityInput): GateResult {
     // standard DW import. Samples already have their own floor enforced above
     // ('sample-below-vendor-floor', the vendor-declared price). The absolute
     // floor is an anomaly guard for SELLABLES, where no legitimate item is $2.
-    if (v.role === 'sellable' && v.orderable === true && price > 0 && price < absoluteFloor) {
+    // TK-11857 R1 — FAIL CLOSED on a non-boolean `orderable`. The VariantCheck type
+    // says boolean, but untyped JS callers (e.g. scripts/import-queue-runner.js) can
+    // pass 1 / 'true' / undefined at runtime; strict `=== true` would SKIP this block
+    // for them (fail-OPEN — the inverse of CLAUDE.md TK-11431 rule 1, "an unmeasured
+    // input is never PASS"). `!== false` treats anything not explicitly false as
+    // orderable, so the guard still fires. A genuinely non-orderable variant MUST
+    // pass orderable:false. (Latent today — all enumerated call sites pass a strict
+    // boolean — proven by the negative test in __tests__/price-integrity-gate.test.ts.)
+    if (v.role === 'sellable' && v.orderable !== false && price > 0 && price < absoluteFloor) {
       findings.push({
         class: 'B',
         code: 'below-absolute-floor',
@@ -436,7 +444,9 @@ export function assertPriceIntegrity(input: PriceIntegrityInput): GateResult {
     // ---- Class C: any orderable variant (any role) MUST be priced > 0 — BLOCK ----
     // Diagnose honestly: a NEGATIVE price is a different fault than a $0/NaN
     // price and gets its own code so the report isn't misleading.
-    if (v.orderable === true && !(price > 0)) {
+    // TK-11857 R1 — FAIL CLOSED on non-boolean `orderable` (see the Class-B comment
+    // above): `!== false` so an unmeasured orderable still gets the $0/negative block.
+    if (v.orderable !== false && !(price > 0)) {
       if (price < 0) {
         findings.push({
           class: 'C',
diff --git a/DW-Programming/ImportNewSkufromURL/lib/shopify.ts b/DW-Programming/ImportNewSkufromURL/lib/shopify.ts
index 60d47cb1..48546f47 100644
--- a/DW-Programming/ImportNewSkufromURL/lib/shopify.ts
+++ b/DW-Programming/ImportNewSkufromURL/lib/shopify.ts
@@ -841,10 +841,17 @@ async function createProductWithMetafield(dto: ProductDTO, priceUnit: string, un
 
   // TK-00134: brand-aware banned-word sweep on every customer-facing field
   const { cleanWallpaper } = require('./clean-wallpaper');
+  // TK-11857 R4 — use the REAL product type instead of a hardcoded 'Wallcovering'.
+  // A Fabric imported via this single-product path was stored as 'Wallcovering' (the
+  // weight guard already reads dto.specs?.product_type at ~L2027; the batch path
+  // derives PRODUCT_TYPE from product.product_type). Mirror both: real type when
+  // present, same banned-word "Wallpaper"->"Wallcovering" sweep as the batch path,
+  // default 'Wallcovering' when absent.
+  const resolvedProductType = (dto.specs?.product_type || 'Wallcovering').replace(/\bWallpaper\b/gi, 'Wallcovering');
   const input = {
     title: cleanWallpaper(enhancedTitle),
     descriptionHtml: cleanWallpaper(enhancedDescription),
-    productType: 'Wallcovering', // banned word "Wallpaper" removed per standing rule
+    productType: resolvedProductType, // TK-11857 R4: real type (was hardcoded 'Wallcovering')
     vendor,
     tags: statusTags.map((t: string) => cleanWallpaper(t)),
     status: productStatus,

← 8196c654 TK-11786: free-samples anchored title match + block-nonpurch  ·  back to Designer Wallcoverings  ·  TK-11783: strip hardcoded Google OAuth client secret from sh 82c219e2 →