[object Object]

← back to Gmc Titlefix

fix(mc-read): stop 400 on legacy bare-variant-id offers; tag non-404 as notMeasured (TK-11846)

c9c9e59f4f533fa65f9b34e98070fbdb01f2df56 · 2026-09-16 15:32:06 -0700 · Steve

toV1Name unconditionally prepended 'online~', so a legacy bare-variant feed
name (en~US~<offerId>, e.g. all 945 MDC offers) became online~en~US~<id> /
online~<id> and getProduct() returned HTTP 400 '[name] Invalid' for every one.
A caller treating that throw as 'no override present' gets a false negative on
a compliance check.

- toV1Name now: strips a full 'accounts/<mid>/products/' prefix; passes a
  '~'-delimited v1 name through verbatim (legacy en~US~x AND online~en~US~x);
  swaps colon v2.1 rids; defaults a bare offerId to the online feed.
- getProduct tags a non-404 failure e.notMeasured=true so a 400/401/403/429/5xx
  reads as UNKNOWN (NOT_MEASURED), never as a genuine absence. Only a clean 404
  is 'product absent'.

Verified live: en~US~<id> and accounts/.../products/en~US~<id> now 200; colon
form unchanged (200); bogus id -> 404 notMeasured=false. 6/6 toV1Name unit cases pass.

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

Files touched

Diff

commit c9c9e59f4f533fa65f9b34e98070fbdb01f2df56
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Sep 16 15:32:06 2026 -0700

    fix(mc-read): stop 400 on legacy bare-variant-id offers; tag non-404 as notMeasured (TK-11846)
    
    toV1Name unconditionally prepended 'online~', so a legacy bare-variant feed
    name (en~US~<offerId>, e.g. all 945 MDC offers) became online~en~US~<id> /
    online~<id> and getProduct() returned HTTP 400 '[name] Invalid' for every one.
    A caller treating that throw as 'no override present' gets a false negative on
    a compliance check.
    
    - toV1Name now: strips a full 'accounts/<mid>/products/' prefix; passes a
      '~'-delimited v1 name through verbatim (legacy en~US~x AND online~en~US~x);
      swaps colon v2.1 rids; defaults a bare offerId to the online feed.
    - getProduct tags a non-404 failure e.notMeasured=true so a 400/401/403/429/5xx
      reads as UNKNOWN (NOT_MEASURED), never as a genuine absence. Only a clean 404
      is 'product absent'.
    
    Verified live: en~US~<id> and accounts/.../products/en~US~<id> now 200; colon
    form unchanged (200); bogus id -> 404 notMeasured=false. 6/6 toV1Name unit cases pass.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_013EXLAV5g4HHux54nArJfyX
---
 _mc-read-v1.js | 29 +++++++++++++++++++++++++++--
 1 file changed, 27 insertions(+), 2 deletions(-)

diff --git a/_mc-read-v1.js b/_mc-read-v1.js
index 6142def..cc8e830 100644
--- a/_mc-read-v1.js
+++ b/_mc-read-v1.js
@@ -15,7 +15,22 @@ const { token, MERCHANT } = require('./_auth.js');
 const BASE = 'https://merchantapi.googleapis.com';
 
 async function H() { return { Authorization: 'Bearer ' + (await token()) }; }
-const toV1Name = rid => 'online~' + String(rid).replace(/^online:/, '').replace(/:/g, '~'); // 'online:en:US:x' → 'online~en~US~x'; bare offerId also ok
+// Normalize any caller-supplied product id/name to the v1 product-name SEGMENT (the part after
+// 'accounts/<mid>/products/'). Accepts, in order: a full resource name
+// 'accounts/<mid>/products/<name>'; a '~'-delimited v1 name verbatim — the ONLINE feed
+// 'online~en~US~<offerId>' OR the LEGACY bare-variant feed 'en~US~<offerId>' (this is the TK-11846
+// fix: those legacy names must NOT get an extra 'online~' or they 400 '[name] Invalid'); a
+// colon-delimited v2.1 rid 'online:en:US:<offerId>' → 'online~en~US~<offerId>'; or a bare offerId,
+// which lacks feed/lang/country so it defaults to the online feed 'online~en~US~<offerId>' (a bare
+// id cannot address a LEGACY offer — pass that offer's full gmcName instead).
+const toV1Name = rid => {
+  let s = String(rid);
+  const m = s.match(/\/products\/(.+)$/); if (m) s = m[1]; // strip 'accounts/<mid>/products/' if a full name was passed
+  s = s.replace(/^online:/, '');                          // tolerate a legacy 'online:' colon prefix
+  if (s.includes('~')) return s;                          // already a v1 name (legacy en~US~x OR online~en~US~x) → verbatim
+  if (s.includes(':')) return 'online~' + s.replace(/:/g, '~'); // colon v2.1 rid → online~en~US~x
+  return 'online~en~US~' + s;                             // bare offerId → default online feed
+};
 const moneyToNum = m => (m && m.amountMicros != null) ? Number(m.amountMicros) / 1e6 : (m && m.value != null ? Number(m.value) : null);
 
 // Map v1 productStatus.destinationStatuses (country arrays) → a v2.1-style status string for a country.
@@ -36,7 +51,17 @@ async function getProduct(rid, { country = 'US' } = {}) {
   const name = `accounts/${MERCHANT}/products/${toV1Name(rid)}`;
   const r = await fetch(`${BASE}/products/v1/${name}`, { headers: await H() });
   const j = await r.json();
-  if (!r.ok) { const e = new Error(`v1 products.get HTTP ${r.status} ${JSON.stringify(j.error || j).slice(0, 160)}`); e.status = r.status; throw e; }
+  if (!r.ok) {
+    const e = new Error(`v1 products.get HTTP ${r.status} ${JSON.stringify(j.error || j).slice(0, 160)}`);
+    e.status = r.status;
+    // NOT_MEASURED discipline (TK-11846 / CLAUDE.md TK-11431 amendment 1): ONLY a clean 404 is a
+    // genuine "product absent". A 400 (malformed name — the legacy-feed bug), 401/403 (auth), 429,
+    // or 5xx means we never actually measured this product's state — a caller MUST NOT read it as
+    // "no override present" / "not on Google". Callers that gate on presence should treat
+    // e.notMeasured === true as UNKNOWN, not as an absence.
+    e.notMeasured = r.status !== 404;
+    throw e;
+  }
   const attr = j.productAttributes || {};
   const ps = j.productStatus || {};
   const price = moneyToNum(attr.price);

← 192552c auto-data-snapshot: 2026-09-16T15:11:20 (1 data files) — dat  ·  back to Gmc Titlefix  ·  auto-data-snapshot: 2026-09-16T15:43:40 (2 data files) — dat 8e63c2e →