[object Object]

← back to Designer Wallcoverings

auto-data-snapshot: 2026-09-10T14:47:21 (1 data files) — DW-Programming/ImportNewSkufromURL/lib/PENDING-WIREIN-price-gate.md

187b165f26ad6a804696922b7a782bf428de19b5 · 2026-09-10 14:48:38 -0700 · auto-commit-fleet

Files touched

Diff

commit 187b165f26ad6a804696922b7a782bf428de19b5
Author: auto-commit-fleet <steve@designerwallcoverings.com>
Date:   Thu Sep 10 14:48:38 2026 -0700

    auto-data-snapshot: 2026-09-10T14:47:21 (1 data files) — DW-Programming/ImportNewSkufromURL/lib/PENDING-WIREIN-price-gate.md
---
 .../lib/PENDING-WIREIN-price-gate.md               | 100 +++++++++++++++++++++
 1 file changed, 100 insertions(+)

diff --git a/DW-Programming/ImportNewSkufromURL/lib/PENDING-WIREIN-price-gate.md b/DW-Programming/ImportNewSkufromURL/lib/PENDING-WIREIN-price-gate.md
new file mode 100644
index 00000000..2622829d
--- /dev/null
+++ b/DW-Programming/ImportNewSkufromURL/lib/PENDING-WIREIN-price-gate.md
@@ -0,0 +1,100 @@
+# PENDING WIRE-IN — price-integrity gate into `addSampleVariant` (TK-11403)
+
+> **STATUS: NOT APPLIED.** This is a GATED, customer-facing change (it decides
+> whether a product publishes to the LIVE store). It must be applied by the memo
+> authors / a Steve-gated pass, NOT by the build subagent. This file documents
+> the exact minimal wire-in so applying it is mechanical.
+
+## Where
+
+`lib/shopify.ts` → `async function addSampleVariant(...)` (starts ~line 932).
+
+The two write calls to guard are, in order:
+
+1. `productVariantsBulkUpdate` — updates the **sellable** per-unit variant with
+   `price: numericUnitPrice, inventoryPolicy: 'CONTINUE'` then
+   `setInventoryQuantity(iid, 2025)` → this is what makes it ORDERABLE.
+2. `productVariantsBulkCreate` — creates the **Sample** variant at `'4.25'`.
+
+The gate must run **BEFORE both** GraphQL calls so a bad product never writes.
+
+## What the gate needs
+
+- `enforcePriceIntegrity(input)` from `./price-integrity-gate` (throws
+  `PriceIntegrityError` with `.violations` on any block).
+- `resolveNetCost(vendor, sku)` from `./price-integrity-cost` (returns
+  `number | null`; null → gate blocks via Class B `cost-unknown`).
+
+### Threading `vendor`
+
+`addSampleVariant` currently has **no `vendor` parameter**. The memo authors must
+thread it in — either:
+
+- add a `vendor: string` param and pass it from the caller (the caller already
+  has the `ProductDTO` / vendor context), **or**
+- pass the vendor via the `ProductDTO` the caller holds.
+
+Until vendor is available, `resolveNetCost` cannot run and the gate blocks
+fail-closed (Class B `cost-unknown`) — which is the correct safe default, but it
+would block ALL publishes, so threading vendor is required for the gate to pass
+real products.
+
+## The ~10-line diff (conceptual)
+
+```ts
+// at top of lib/shopify.ts
+import { enforcePriceIntegrity } from './price-integrity-gate';
+import { resolveNetCost } from './price-integrity-cost';
+
+// inside addSampleVariant(productId, sku, price, unitLabel, unitKind, vendor),
+// AFTER computing `numericUnitPrice` and BEFORE the productVariantsBulkUpdate call:
+
+const netCost = await resolveNetCost(vendor, sku);        // number | null (fail-closed)
+try {
+  enforcePriceIntegrity({
+    dwSku: sku,
+    netCost,
+    variants: [
+      // sellable: CONTINUE + qty 2025 ⇒ orderable:true
+      { role: 'sellable', price: Number(numericUnitPrice), orderable: true, sku: `${sku}-${unitKind}` },
+      // sample: fixed 4.25, not orderable
+      { role: 'sample', price: 4.25, orderable: false, sku: `${sku}-Sample` },
+    ],
+  });
+} catch (err) {
+  const violations = (err as any)?.violations ?? [];
+  console.error(JSON.stringify({ event: 'price_integrity_block', dwSku: sku, vendor, violations }));
+  return; // DO NOT WRITE — block publish
+}
+
+// ... existing productVariantsBulkUpdate + productVariantsBulkCreate calls unchanged ...
+```
+
+## Behavior on block
+
+- Structured log: `{event:'price_integrity_block', dwSku, vendor, violations}`.
+- `return` WITHOUT calling either `productVariantsBulk*` mutation — the product
+  is not priced/published. (Product stays whatever status it was; nothing
+  orderable is written.)
+
+## Why each guarded value maps this way
+
+| Gate field | Source in `addSampleVariant` | Note |
+|---|---|---|
+| sellable `price` | `Number(numericUnitPrice)` | `numericUnitPrice = numericPrice \|\| '0.00'` — a missing scrape becomes 0 ⇒ Class C catches it |
+| sellable `orderable` | `true` | because the update sets `inventoryPolicy:'CONTINUE'` + `setInventoryQuantity(iid,2025)` |
+| sample `price` | `4.25` | fixed literal in the create call |
+| sample `orderable` | `false` | sample is not stocked/orderable as a sellable unit |
+| `netCost` | `resolveNetCost(vendor, sku)` | null ⇒ Class B `cost-unknown` (fail-closed) |
+
+## Verification after wire-in
+
+Re-run the pure gate suite (unaffected by the wire-in, proves the gate logic):
+
+```
+npx tsx --test __tests__/price-integrity-gate.test.ts
+```
+
+Then a live smoke on ONE known-good vendor SKU to confirm `resolveNetCost`
+returns a number and the product publishes, and ONE known-$0/known-at-cost SKU
+to confirm it blocks with a `price_integrity_block` log line.

← 55c6546f TK-11400: inventory sweep prefers the full-access credential  ·  back to Designer Wallcoverings  ·  auto-data-snapshot: 2026-09-10T15:21:44 (1 data files) — DW- 85002803 →