← 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
M carnegie-split/build-siltech-grain-v2-archived.mjsM carnegie-split/rollout.mjsM carnegie-split/rollout2.mjs
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 →