← back to Carnegie Reprice
build-one-proof.mjs
74 lines
// build-one-proof — create ONE perfect Carnegie product off to the side (unlinked, deletable)
// from the now-complete carnegie_catalog, to prove what the rebuild looks like.
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
// TK-10820: canonical born-active guards — a proof product is still a born-active
// create path, so it must obey the same body_html + mfr_sku gate as every rollout path.
import { mfrSkuValid, bodyHtmlValid, SKIP_TAG as NEEDS_MFR_TAG, NEEDS_DESC_TAG } from './carnegie-mfr-gate.mjs';
const ENV = `${process.env.HOME}/Projects/secrets-manager/.env`;
const env = k => { const m = fs.readFileSync(ENV,'utf8').split('\n').find(l=>l.startsWith(k+'=')); return m? m.slice(k.length+1).trim().replace(/^["']|["']$/g,''):''; };
const TOKEN = env('SHOPIFY_ADMIN_TOKEN'); let SHOP = env('SHOPIFY_STORE_DOMAIN')||env('SHOPIFY_STORE'); if(SHOP&&!SHOP.includes('.'))SHOP+='.myshopify.com';
const API = `https://${SHOP}/admin/api/2024-10`, H = { 'X-Shopify-Access-Token':TOKEN, 'Content-Type':'application/json' };
const PSQL = ['/opt/homebrew/opt/postgresql@14/bin/psql','/usr/local/opt/postgresql@14/bin/psql','psql'].find(p=>{try{execFileSync(p,['--version'],{stdio:'ignore'});return 1}catch{return 0}})||'psql';
const q1 = sql => execFileSync(PSQL,['postgresql:///dw_unified?host=/tmp','-At','-c',sql],{encoding:'utf8'}).trim();
const sleep = ms => new Promise(r=>setTimeout(r,ms));
async function shop(path,opts={}){ const r=await fetch(`${API}${path}`,{headers:H,...opts}); if(!r.ok) throw new Error(`HTTP ${r.status} ${path} :: ${(await r.text()).slice(0,300)}`); return r.json(); }
const SKU='DWAG-379296';
const row = JSON.parse(q1(`select row_to_json(t) from (select * from carnegie_catalog where dw_sku='${SKU}') t`));
const specs = row.specs||{};
const cost=+row.price, retail=Math.round(cost/0.65/0.85);
const colorName = (row.color_tags&&row.color_tags[0])||row.color_bucket||('Color '+row.color_number);
const big = u => u? u.replace(/height=\d*&width=\d*/,'height=1400&width=1400').replace(/height=&width=/,'height=1400&width=1400') : null;
const imgs = (row.all_images||[]).filter(u=>/_puf|_pud|siltech/i.test(u)).slice(0,3).map(big);
// --- create product (unlinked proof) ---
// TK-10820: gate born-active on real mfr_sku AND non-blank body_html (a whitespace/entity
// shell must never ship ACTIVE). A proof that fails either stays DRAFT + the matching tag.
const proofBody = row.description_text;
const hasMfr = mfrSkuValid(row.mfr_sku);
const bodyValid = bodyHtmlValid(proofBody);
const finalStatus = (imgs.length && hasMfr && bodyValid) ? 'active' : 'draft';
if(!hasMfr) console.warn(`[MFR-GATE] ${SKU} mfr=${JSON.stringify(row.mfr_sku)} → DRAFT+${NEEDS_MFR_TAG}`);
if(!bodyValid) console.warn(`[DESC-GATE] ${SKU} blank body_html → DRAFT+${NEEDS_DESC_TAG}`);
const title = `Carnegie Siltech Grain — ${colorName}`;
const payload = { product: {
title, body_html: proofBody, vendor:'Carnegie', product_type:'Upholstery',
handle: 'carnegie-siltech-grain-proof-ivory', status: finalStatus, published_scope:'web',
tags: [colorName, row.color_bucket, 'Carnegie','Fabric','Upholstery','Carnegie Textiles','PROOF-DELETE-ME',
...(!hasMfr?[NEEDS_MFR_TAG]:[]), ...(!bodyValid?[NEEDS_DESC_TAG]:[])].filter(Boolean).join(', '),
options:[{name:'Format'}],
images: imgs.map((src,i)=>({src, position:i+1})),
variants:[
{ option1:'Per Yard', price: retail.toFixed(2), sku: SKU, requires_shipping:true, taxable:true, inventory_management:null },
{ option1:'Memo Sample', price: '4.25', sku: `${SKU}-Sample`, requires_shipping:true, taxable:true, inventory_management:null },
],
}};
const { product } = await shop('/products.json',{method:'POST',body:JSON.stringify(payload)});
console.log('created product id', product.id, 'handle', product.handle);
// --- metafields: identity + FULL specs across the namespaces the theme may read ---
const spec = {
Width: row.width, Content: row.content, Contents: row.content, Durability: row.durability_wyzenbeek,
'Cleaning Code': row.cleaning_code, Cleaning: row.cleaning_code, Finish: row.finish, Backing: row.backing,
Flammability: row.flammability, Origin: row.origin, 'Country of Origin': row.origin,
Type: specs.Type, Warranty: specs.Warranty, Weight: specs['Weight per Linear Yard'],
Use:'Upholstery', Repeat: (row.repeat_v||row.repeat_h||'')||undefined,
};
const ident = { dw_sku:SKU, manufacturer_sku:row.mfr_sku, brand:'Carnegie', vendor:'Carnegie',
pattern_name:row.pattern_name, color:colorName, color_number:String(row.color_number),
product_class:'Fabric', collection_name:'Carnegie Textiles' };
const mfs=[];
for(const ns of ['global','custom','specifications','specs','dwc'])
for(const [k,v] of Object.entries(spec)) if(v) mfs.push({namespace:ns,key:k,type:'single_line_text_field',value:String(v)});
for(const ns of ['global','custom','dwc'])
for(const [k,v] of Object.entries(ident)) if(v) mfs.push({namespace:ns,key:k,type:'single_line_text_field',value:String(v)});
mfs.push({namespace:'custom',key:'color_hex',type:'single_line_text_field',value:row.primary_hex||''});
let ok=0,err=0;
for(const m of mfs){ try{ await shop(`/products/${product.id}/metafields.json`,{method:'POST',body:JSON.stringify({metafield:m})}); ok++; }catch(e){ err++; } await sleep(120); }
console.log(`metafields: ${ok} set, ${err} err`);
fs.writeFileSync(`${new URL('.',import.meta.url).pathname}proof-one.json`, JSON.stringify({product_id:product.id, handle:product.handle, sku:SKU, colorName, retail, url:`https://www.designerwallcoverings.com/products/${product.handle}`},null,2));
console.log('URL: https://www.designerwallcoverings.com/products/'+product.handle);
console.log('title:', title, '| color:', colorName, '| retail $'+retail, '| images:', imgs.length, '| specs on page:', Object.values(spec).filter(Boolean).length);