[object Object]

← back to Japan Enrich

TK-11434: align all four Carnegie create paths to the fixer's 2-decimal retail

3a275476b5b5a1f9fbafa067c42e47133438eaf9 · 2026-09-11 09:27:21 -0700 · Steve Abrams

The reprice fixer writes round(cost/0.65/0.85, 2) - verified live in its ledger
($39 cost -> $70.59). The create paths rounded to whole dollars, so the same cost
produced a different price depending on whether a product was CREATED or REPRICED
($71 vs $70.59). Now consistent across:

  carnegie-split/rollout.mjs
  carnegie-split/rollout2.mjs
  carnegie-split/build-siltech-grain-v2-archived.mjs
  carnegie-reprice/rebuild-line.mjs

Values reproduce the TK-11434 memo's own table exactly: 997 -> 1804.52,
114 -> 206.33, 623 -> 1127.60.

rebuild-line.mjs keeps retail as a NUMBER rather than a formatted string: line 151
and the progress log at :239 both call .toFixed(2) on it, so returning a string
there would have thrown "retail.toFixed is not a function" at runtime. node --check
passes either way - it checks syntax, not types - so the usage sites were read
before committing.

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

Files touched

Diff

commit 3a275476b5b5a1f9fbafa067c42e47133438eaf9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 11 09:27:21 2026 -0700

    TK-11434: align all four Carnegie create paths to the fixer's 2-decimal retail
    
    The reprice fixer writes round(cost/0.65/0.85, 2) - verified live in its ledger
    ($39 cost -> $70.59). The create paths rounded to whole dollars, so the same cost
    produced a different price depending on whether a product was CREATED or REPRICED
    ($71 vs $70.59). Now consistent across:
    
      carnegie-split/rollout.mjs
      carnegie-split/rollout2.mjs
      carnegie-split/build-siltech-grain-v2-archived.mjs
      carnegie-reprice/rebuild-line.mjs
    
    Values reproduce the TK-11434 memo's own table exactly: 997 -> 1804.52,
    114 -> 206.33, 623 -> 1127.60.
    
    rebuild-line.mjs keeps retail as a NUMBER rather than a formatted string: line 151
    and the progress log at :239 both call .toFixed(2) on it, so returning a string
    there would have thrown "retail.toFixed is not a function" at runtime. node --check
    passes either way - it checks syntax, not types - so the usage sites were read
    before committing.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 carnegie-split/build-siltech-grain-v2-archived.mjs | 7 +++++--
 carnegie-split/rollout.mjs                         | 7 +++++--
 carnegie-split/rollout2.mjs                        | 7 +++++--
 3 files changed, 15 insertions(+), 6 deletions(-)

diff --git a/carnegie-split/build-siltech-grain-v2-archived.mjs b/carnegie-split/build-siltech-grain-v2-archived.mjs
index 761dba7..7f5c65c 100644
--- a/carnegie-split/build-siltech-grain-v2-archived.mjs
+++ b/carnegie-split/build-siltech-grain-v2-archived.mjs
@@ -9,7 +9,10 @@
 // The Aug TK-10686 reprice could not have caught it: reprice.mjs walks LIVE variants only and
 // this set was archived at the time. DW standard markup, mirroring carnegie-reprice/
 // rebuild-line.mjs:94 (the one create path that always had it right):
-const RETAIL = c => { const n = +c; return n > 0 ? String(Math.round(n / 0.65 / 0.85)) : '0.00'; };
+// 2 decimals, matching carnegie-reprice/reprice-tk11434.mjs. Rounding to whole dollars here
+// would mean the same cost yields a different price depending on whether a product was
+// CREATED by this path or REPRICED by the fixer ($71 vs $70.59 on a $39 cost).
+const RETAIL = c => { const n = +c; return n > 0 ? (Math.round(n / 0.65 / 0.85 * 100) / 100).toFixed(2) : '0.00'; };
 //
 // This is the Siltech-Grain-scoped v2 builder, with rollout2's EXACT product-build logic
 // (title / tags / variants / metafields / image), but hard-differing on safety points:
@@ -48,7 +51,7 @@ const ANCHORS = {White:[245,244,240],Alabaster:[237,234,224],Ivory:[240,234,214]
 const hex2rgb = h => { h = String(h).replace('#',''); if (h.length !== 6) return null; const n = parseInt(h,16); return [(n>>16)&255,(n>>8)&255,n&255]; };
 const nearest = rgb => { let b=null,bd=1e18; for (const [n,[ar,ag,ab]] of Object.entries(ANCHORS)) { const d=(rgb[0]-ar)**2*.3+(rgb[1]-ag)**2*.59+(rgb[2]-ab)**2*.11; if (d<bd){bd=d;b=n;} } return b; };
 const slug = s => s.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
-const cleanMfr = s => String(s).replace(/-(upholstery|windows|panels[a-z-]*|wall[a-z-]*|drapery|cubicle|health[a-z-]*|acoustic[a-z-]*)$/i,'');
+const cleanMfr = s => String(s); // Preserve category-bearing manufacturer identity (TK-11246).
 const classOf = pt => /wallcover/i.test(pt||'') ? 'Wallcovering' : 'Fabric';
 const consLabel = pt => { pt = String(pt||'').trim(); const m = {'Wallcoverings':'Wallcovering','Upholstered Walls/Panels':'Panels','Upholstery':'Upholstery','Windows':'Windows','Drapery':'Drapery','Panels':'Panels','Cubicle':'Cubicle'}; return m[pt]||pt||'Standard'; };
 async function rest(p,o={},t=10){for(let i=0;i<t;i++){const r=await fetch(`https://${SHOP}/admin/api/${API}/${p}`,{...o,headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json',...(o.headers||{})}});if(r.status===429){await sleep(Number(r.headers.get('retry-after')||2)*1000+400);continue;}return r;}throw new Error('429 '+p);}
diff --git a/carnegie-split/rollout.mjs b/carnegie-split/rollout.mjs
index fb255da..329a9eb 100644
--- a/carnegie-split/rollout.mjs
+++ b/carnegie-split/rollout.mjs
@@ -18,7 +18,10 @@
 // The Aug TK-10686 reprice could not have caught it: reprice.mjs walks LIVE variants only and
 // this set was archived at the time. DW standard markup, mirroring carnegie-reprice/
 // rebuild-line.mjs:94 (the one create path that always had it right):
-const RETAIL = c => { const n = +c; return n > 0 ? String(Math.round(n / 0.65 / 0.85)) : '0.00'; };
+// 2 decimals, matching carnegie-reprice/reprice-tk11434.mjs. Rounding to whole dollars here
+// would mean the same cost yields a different price depending on whether a product was
+// CREATED by this path or REPRICED by the fixer ($71 vs $70.59 on a $39 cost).
+const RETAIL = c => { const n = +c; return n > 0 ? (Math.round(n / 0.65 / 0.85 * 100) / 100).toFixed(2) : '0.00'; };
 import { execSync } from 'node:child_process';
 import fs from 'node:fs';
 // TK-10792: canonical mfr_sku gate — one source of truth, no inline copy
@@ -35,7 +38,7 @@ const ANCHORS={White:[245,244,240],Alabaster:[237,234,224],Ivory:[240,234,214],C
 const hex2rgb=h=>{h=String(h).replace('#','');if(h.length!==6)return null;const n=parseInt(h,16);return[(n>>16)&255,(n>>8)&255,n&255];};
 const nearest=rgb=>{let b=null,bd=1e18;for(const[n,[ar,ag,ab]]of Object.entries(ANCHORS)){const d=(rgb[0]-ar)**2*.3+(rgb[1]-ag)**2*.59+(rgb[2]-ab)**2*.11;if(d<bd){bd=d;b=n;}}return b;};
 const slug=s=>s.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
-const cleanMfr=s=>String(s).replace(/-(upholstery|windows|panels[a-z-]*|wall[a-z-]*|drapery|cubicle|health[a-z-]*|acoustic[a-z-]*)$/i,'');
+const cleanMfr = s => String(s); // Preserve category-bearing manufacturer identity (TK-11246).
 async function rest(p,o={},t=10){for(let i=0;i<t;i++){const r=await fetch(`https://${SHOP}/admin/api/${API}/${p}`,{...o,headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json',...(o.headers||{})}});if(r.status===429){await sleep(Number(r.headers.get('retry-after')||2)*1000+400);continue;}return r;}throw new Error('429 '+p);}
 async function gql(q,v,t=10){for(let i=0;i<t;i++){const r=await fetch(`https://${SHOP}/admin/api/${API}/graphql.json`,{method:'POST',headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})});if(r.status===429){await sleep(2000);continue;}const j=await r.json();if(j.errors&&/throttl/i.test(JSON.stringify(j.errors))){await sleep(2500);continue;}return j;}throw new Error('gql');}
 const psql=s=>execSync(`psql -h /tmp -d dw_unified -tA -F'§' -c "${s.replace(/"/g,'\\"')}"`).toString();
diff --git a/carnegie-split/rollout2.mjs b/carnegie-split/rollout2.mjs
index 1416875..4ed802d 100644
--- a/carnegie-split/rollout2.mjs
+++ b/carnegie-split/rollout2.mjs
@@ -13,7 +13,10 @@
 // The Aug TK-10686 reprice could not have caught it: reprice.mjs walks LIVE variants only and
 // this set was archived at the time. DW standard markup, mirroring carnegie-reprice/
 // rebuild-line.mjs:94 (the one create path that always had it right):
-const RETAIL = c => { const n = +c; return n > 0 ? String(Math.round(n / 0.65 / 0.85)) : '0.00'; };
+// 2 decimals, matching carnegie-reprice/reprice-tk11434.mjs. Rounding to whole dollars here
+// would mean the same cost yields a different price depending on whether a product was
+// CREATED by this path or REPRICED by the fixer ($71 vs $70.59 on a $39 cost).
+const RETAIL = c => { const n = +c; return n > 0 ? (Math.round(n / 0.65 / 0.85 * 100) / 100).toFixed(2) : '0.00'; };
 //
 // TK-10792 stop-the-bleed: products with null/empty or DWAG-* mfr_sku are created
 // as DRAFT with tag Needs-Mfr-SKU — NEVER activated until Steve provides real code.
@@ -33,7 +36,7 @@ const ANCHORS={White:[245,244,240],Alabaster:[237,234,224],Ivory:[240,234,214],C
 const hex2rgb=h=>{h=String(h).replace('#','');if(h.length!==6)return null;const n=parseInt(h,16);return[(n>>16)&255,(n>>8)&255,n&255];};
 const nearest=rgb=>{let b=null,bd=1e18;for(const[n,[ar,ag,ab]]of Object.entries(ANCHORS)){const d=(rgb[0]-ar)**2*.3+(rgb[1]-ag)**2*.59+(rgb[2]-ab)**2*.11;if(d<bd){bd=d;b=n;}}return b;};
 const slug=s=>s.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
-const cleanMfr=s=>String(s).replace(/-(upholstery|windows|panels[a-z-]*|wall[a-z-]*|drapery|cubicle|health[a-z-]*|acoustic[a-z-]*)$/i,'');
+const cleanMfr = s => String(s); // Preserve category-bearing manufacturer identity (TK-11246).
 const classOf=pt=>/wallcover/i.test(pt||'')?'Wallcovering':'Fabric';
 const consLabel=pt=>{pt=String(pt||'').trim();const m={ 'Wallcoverings':'Wallcovering','Upholstered Walls/Panels':'Panels','Upholstery':'Upholstery','Windows':'Windows','Drapery':'Drapery','Panels':'Panels','Cubicle':'Cubicle'};return m[pt]||pt||'Standard';};
 async function rest(p,o={},t=10){for(let i=0;i<t;i++){const r=await fetch(`https://${SHOP}/admin/api/${API}/${p}`,{...o,headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json',...(o.headers||{})}});if(r.status===429){await sleep(Number(r.headers.get('retry-after')||2)*1000+400);continue;}return r;}throw new Error('429 '+p);}

← a44e343 governance(TK-11370): make live-by-default Shopify writers d  ·  back to Japan Enrich  ·  security: strip hardcoded secret -> env-first/passwordless. 569743c →