← back to Hollywood Mfr Trim
remediate.mjs
139 lines
#!/usr/bin/env node
// HWC Group-A/B remediation — writes the correct HWC canonical code into the
// dw_sku metafields (custom/dwc/global) on the LIVE store. Dry-run unless --apply.
// node remediate.mjs --part archived (221 archived: copy existing HWC col -> metafields)
// node remediate.mjs --part mint75 (75 clean verified: mint HWC-61245+ -> metafields)
// Worklist is pulled fresh from the local dw_unified mirror (psql, /tmp socket).
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
const APPLY = process.argv.includes('--apply');
const PART = (process.argv[process.argv.indexOf('--part')+1]) || '';
const SHOP='designer-laboratory-sandbox.myshopify.com', API='2024-10';
const TOKEN=(fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env','utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1]?.trim();
if(!TOKEN){console.error('No SHOPIFY_ADMIN_TOKEN');process.exit(1);}
const psql = sql => JSON.parse(execFileSync('psql',['-h','/tmp','-d','dw_unified','-tAc',sql],{encoding:'utf8'}).trim()||'[]');
// Base Group-B / archived predicate (Hollywood line, fabricated dw_sku metafield)
const FAB = `(metafields->'custom'->'dw_sku'->>'value' ~ '^DWH[DW]' OR metafields->'dwc'->'dw_sku'->>'value' ~ '^DWH[DW]' OR metafields->'global'->'dw_sku'->>'value' ~ '^DWH[DW]')`;
let SQL;
if(PART==='archived'){
// 221 ARCHIVED: correct HWC already in dw_sku column -> copy into each fabricated dw_sku metafield
SQL = `WITH a AS (
SELECT DISTINCT regexp_replace(shopify_id,'\\D','','g') AS pid, dw_sku AS code,
(metafields->'custom'->'dw_sku'->>'value' ~ '^DWH[DW]') c,
(metafields->'dwc'->'dw_sku'->>'value' ~ '^DWH[DW]') d,
(metafields->'global'->'dw_sku'->>'value' ~ '^DWH[DW]') g
FROM shopify_products
WHERE (vendor='Hollywood Wallcoverings' OR dw_sku LIKE 'HWC-%')
AND status='ARCHIVED' AND dw_sku ~ '^HWC-\\d+$' AND ${FAB}
), w AS (
SELECT pid,'custom' ns,code FROM a WHERE c
UNION ALL SELECT pid,'dwc',code FROM a WHERE d
UNION ALL SELECT pid,'global',code FROM a WHERE g
)
SELECT COALESCE(json_agg(json_build_object('ownerId','gid://shopify/Product/'||pid,'namespace',ns,'key','dw_sku','type','single_line_text_field','value',code) ORDER BY pid,ns),'[]') FROM w;`;
} else if(PART==='mint75'){
// 75 CLEAN: verified against momentum_colorways (pattern_sku/lcode) AND mfr_sku unique in catalog.
// Deterministic assignment HWC-(61244 + rank) ordered by pid. Written to the fabricated dw_sku metafields.
SQL = `WITH b AS (
SELECT DISTINCT regexp_replace(shopify_id,'\\D','','g') AS pid, TRIM(mfr_sku) AS mfr,
(metafields->'custom'->'dw_sku'->>'value' ~ '^DWH[DW]') c,
(metafields->'dwc'->'dw_sku'->>'value' ~ '^DWH[DW]') d,
(metafields->'global'->'dw_sku'->>'value' ~ '^DWH[DW]') g
FROM shopify_products sp
WHERE (vendor='Hollywood Wallcoverings' OR dw_sku LIKE 'HWC-%')
AND status='ACTIVE' AND dw_sku IS NULL AND ${FAB}
AND mfr_sku IS NOT NULL AND TRIM(mfr_sku)<>''
AND EXISTS (SELECT 1 FROM momentum_colorways m WHERE TRIM(mfr_sku) IN (TRIM(m.pattern_sku),TRIM(m.lcode)))
AND NOT EXISTS (SELECT 1 FROM shopify_products o WHERE TRIM(o.mfr_sku)=TRIM(sp.mfr_sku) AND o.shopify_id<>sp.shopify_id)
), r AS (SELECT pid,c,d,g,'HWC-'||(61244 + row_number() OVER (ORDER BY pid))::text AS code FROM b),
w AS (
SELECT pid,'custom' ns,code FROM r WHERE c
UNION ALL SELECT pid,'dwc',code FROM r WHERE d
UNION ALL SELECT pid,'global',code FROM r WHERE g
)
SELECT COALESCE(json_agg(json_build_object('ownerId','gid://shopify/Product/'||pid,'namespace',ns,'key','dw_sku','type','single_line_text_field','value',code) ORDER BY pid,ns),'[]') FROM w;`;
} else if(PART==='recover24'){
// 24 no-mfr (Goat Rock=Seto, Hearst Castle=Layout): recover per-color Momentum sku into manufacturer_sku
// AND mint HWC-61320+ into dw_sku. mint75 consumed HWC-61245..61319, so this continues at 61320.
SQL = `WITH b AS (
SELECT DISTINCT regexp_replace(shopify_id,'\\D','','g') AS pid, title,
regexp_replace(title,'^(Goat Rock|Hearst Castle) - ([A-Za-z]+).*','\\2') AS color,
CASE WHEN title LIKE 'Goat Rock%' THEN 'Seto' WHEN title LIKE 'Hearst Castle%' THEN 'Layout' END AS mpat,
(metafields->'custom'->'dw_sku'->>'value' ~ '^DWH[DW]') c,
(metafields->'dwc'->'dw_sku'->>'value' ~ '^DWH[DW]') d,
(metafields->'global'->'dw_sku'->>'value' ~ '^DWH[DW]') g
FROM shopify_products
WHERE (vendor='Hollywood Wallcoverings' OR dw_sku LIKE 'HWC-%')
AND status='ACTIVE' AND dw_sku IS NULL AND ${FAB}
AND (mfr_sku IS NULL OR TRIM(mfr_sku)='')
), r AS (
SELECT b.pid, b.c, b.d, b.g, m.momentum_sku AS msku,
'HWC-'||(61319 + row_number() OVER (ORDER BY b.pid))::text AS hwc
FROM b JOIN LATERAL (
SELECT momentum_sku FROM momentum_colorways m
WHERE m.pattern_name=b.mpat AND TRIM(m.color_name)=b.color LIMIT 1
) m ON true
), w AS (
-- mint HWC into all 3 line-standard dw_sku namespaces (matches mint75; clears the fabricated code + populates the mirror)
SELECT pid,'custom' ns,'dw_sku' k,hwc v FROM r
UNION ALL SELECT pid,'dwc','dw_sku',hwc FROM r
UNION ALL SELECT pid,'global','dw_sku',hwc FROM r
-- write recovered Momentum sku into manufacturer_sku across the same 3 namespaces
UNION ALL SELECT pid,'custom','manufacturer_sku',msku FROM r
UNION ALL SELECT pid,'dwc','manufacturer_sku',msku FROM r
UNION ALL SELECT pid,'global','manufacturer_sku',msku FROM r
)
SELECT COALESCE(json_agg(json_build_object('ownerId','gid://shopify/Product/'||pid,'namespace',ns,'key',k,'type','single_line_text_field','value',v) ORDER BY pid,k,ns),'[]') FROM w;`;
} else if(PART==='mint14'){
// 14 collides: provenance-verified Momentum products whose mfr code collides on a shared pattern string.
// Their manufacturer_sku already holds the pattern code; just mint the unique HWC canonical into dw_sku.
// recover24 consumed HWC-61320..61343, so this continues at 61344.
SQL = `WITH b AS (
SELECT DISTINCT regexp_replace(shopify_id,'\\D','','g') AS pid, TRIM(mfr_sku) AS mfr
FROM shopify_products sp
WHERE (vendor='Hollywood Wallcoverings' OR dw_sku LIKE 'HWC-%')
AND status='ACTIVE' AND dw_sku IS NULL AND ${FAB}
AND mfr_sku IS NOT NULL AND TRIM(mfr_sku)<>''
AND EXISTS (SELECT 1 FROM momentum_colorways m WHERE TRIM(sp.mfr_sku) IN (TRIM(m.pattern_sku),TRIM(m.lcode)))
AND EXISTS (SELECT 1 FROM shopify_products o WHERE TRIM(o.mfr_sku)=TRIM(sp.mfr_sku) AND o.shopify_id<>sp.shopify_id)
), r AS (SELECT pid,'HWC-'||(61343 + row_number() OVER (ORDER BY pid))::text AS hwc FROM b),
w AS (
SELECT pid,'custom' ns,'dw_sku' k,hwc v FROM r
UNION ALL SELECT pid,'dwc','dw_sku',hwc FROM r
UNION ALL SELECT pid,'global','dw_sku',hwc FROM r
)
SELECT COALESCE(json_agg(json_build_object('ownerId','gid://shopify/Product/'||pid,'namespace',ns,'key',k,'type','single_line_text_field','value',v) ORDER BY pid,ns),'[]') FROM w;`;
} else { console.error('Pass --part archived | mint75 | recover24 | mint14'); process.exit(1); }
const payload = psql(SQL); // json_agg -> a single JSON array, parsed by the psql() helper
const prods = new Set(payload.map(p=>p.ownerId));
console.log(`Part: ${PART} | ${payload.length} dw_sku metafield writes across ${prods.size} products`);
console.log(APPLY ? '>>> APPLY MODE — writing to LIVE Shopify' : '>>> DRY RUN (no writes). Pass --apply to execute.');
if(!APPLY){
const byProd={}; for(const p of payload){ (byProd[p.ownerId] ||= []).push(`${p.namespace}.${p.key}=${p.value}`); }
for(const [gid,sets] of Object.entries(byProd)) console.log(` ${gid.split('/').pop()} ${sets.join(' ')}`);
console.log(`\nDry run complete. ${payload.length} writes staged across ${prods.size} products.`);
process.exit(0);
}
const MUT=`mutation($mf:[MetafieldsSetInput!]!){metafieldsSet(metafields:$mf){metafields{id} userErrors{field message}}}`;
let ok=0,errs=[];
for(let i=0;i<payload.length;i+=25){
const batch=payload.slice(i,i+25);
const r=await fetch(`https://${SHOP}/admin/api/${API}/graphql.json`,{method:'POST',headers:{'Content-Type':'application/json','X-Shopify-Access-Token':TOKEN},body:JSON.stringify({query:MUT,variables:{mf:batch}})});
const j=await r.json();
const ue=j?.data?.metafieldsSet?.userErrors||[]; const set=j?.data?.metafieldsSet?.metafields||[];
if(j.errors){errs.push(JSON.stringify(j.errors));console.error('GraphQL',j.errors);}
if(ue.length){errs.push(...ue.map(e=>JSON.stringify(e)));console.error('userErrors',ue);}
ok+=set.length; console.log(` batch ${i/25+1}: set ${set.length}/${batch.length}`);
await new Promise(r=>setTimeout(r,600));
}
console.log(`\nDone. ${ok}/${payload.length} dw_sku metafields set. Errors: ${errs.length}`);
if(errs.length) process.exit(1);