← back to Imageless Rescue

scripts/remediate-active-violators.js

108 lines

#!/usr/bin/env node
'use strict';
/**
 * SAFE remediation (DTD verdict A, 3/3, 2026-06-20) of the 2 confirmed LIVE
 * ACTIVE activation-gate violators. ADDITIVE only — attaches the exact-match
 * vendor image + width metafield + a DETERMINISTIC spec-driven description
 * (NO metered LLM). Does NOT flip status, does NOT remove data, does NOT touch
 * dw_unified. After this, validateBeforeActivate() passes for both → they stay
 * ACTIVE and are gate-compliant.
 *
 * DRY-RUN by default; pass --commit to write to Shopify.
 */
const https = require('https');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { validateBeforeActivate } = require(path.join(os.homedir(),
  'Projects/Designer-Wallcoverings/shopify/scripts/lib/validate-before-activate.js'));

const COMMIT = process.argv.includes('--commit');
const env = fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/g, '').trim();
const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
const sleep = ms => new Promise(r => setTimeout(r, ms));
const esc = s => String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');

function gql(q, v) {
  return new Promise(res => {
    const d = JSON.stringify({ query: q, variables: v || {} });
    const r = https.request({ host: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST',
      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(d) } },
      x => { let b=''; x.on('data',c=>b+=c); x.on('end',()=>{ try{res(JSON.parse(b))}catch{res({_raw:b.slice(0,300)})} }); });
    r.on('error', e => res({ _err: e.message })); r.write(d); r.end();
  });
}

// Deterministic spec-driven description (same shape as cadence-import.js buildInput).
function buildDesc(pattern, color, vendor, soldBy, specs) {
  const li = [];
  if (specs.material) li.push(`<li><strong>Material:</strong> ${esc(specs.material)}</li>`);
  if (specs.width)    li.push(`<li><strong>Width:</strong> ${esc(specs.width)}</li>`);
  if (specs.length)   li.push(`<li><strong>Roll Length:</strong> ${esc(specs.length)}</li>`);
  if (specs.repeat)   li.push(`<li><strong>Repeat:</strong> ${esc(specs.repeat)}</li>`);
  li.push(`<li><strong>Sold By:</strong> ${esc(soldBy)}</li>`);
  const intro = `${pattern}${color?' in '+color:''} is a designer wallcovering by ${vendor}. Priced per ${soldBy.toLowerCase()}.`;
  return `<p>${esc(intro)}</p><ul>${li.join('')}</ul>`;
}

// The 2 violators with their EXACT-match vendor data (verified against dw_unified).
const TARGETS = [
  {
    id: 'gid://shopify/Product/6604512165939',
    vendor: 'China Seas', pattern: 'Petite Zig Zag', color: 'White on White',
    soldBy: 'Single Roll',
    image: 'https://quadrillefabrics.s3.us-east-2.amazonaws.com/Wallpaper_Images/Petite-Zig-Zag-White-on-White-Vinyl-AP303-01PV.jpg',
    specs: { width: '27 in (68.58 cm)', material: '', length: '', repeat: '6 in (15.24 cm)' },
    catalog: 'china_seas_catalog DWCH-510687 / mfr AP303-01PV',
  },
  {
    id: 'gid://shopify/Product/7863634526259',
    vendor: 'Tres Tintas', pattern: 'Pipelines Brick', color: 'Burnt Orange',
    soldBy: 'Single Roll',
    image: 'https://trestintas.com/wp-content/uploads/2025/12/pipelines-brick-repeated-patterns-coordonne.jpg',
    specs: { width: '25.6 in (65 cm)', material: 'Bamboo', length: '', repeat: '' },
    catalog: 'tres_tintas_catalog DWTS-901473 / mfr UT5401-5',
  },
];

const M_UPDATE = `mutation($input:ProductInput!){productUpdate(input:$input){product{id status} userErrors{field message}}}`;
const M_FILES  = `mutation($id:ID!,$media:[CreateMediaInput!]!){productCreateMedia(productId:$id,media:$media){media{... on MediaImage{id status}} mediaUserErrors{field message code}}}`;

(async () => {
  if (!TOKEN) { console.error('no token'); process.exit(1); }
  for (const t of TARGETS) {
    const desc = buildDesc(t.pattern, t.color, t.vendor, t.soldBy, t.specs);
    const metafields = [
      { namespace:'global', key:'width', type:'single_line_text_field', value:t.specs.width },
      { namespace:'custom', key:'width', type:'single_line_text_field', value:t.specs.width },
    ];
    if (t.specs.material) {
      metafields.push({ namespace:'global', key:'material', type:'single_line_text_field', value:t.specs.material });
    }
    if (t.specs.repeat) metafields.push({ namespace:'global', key:'repeat', type:'single_line_text_field', value:t.specs.repeat });
    metafields.push({ namespace:'global', key:'unit_of_measure', type:'single_line_text_field', value:`Priced Per ${t.soldBy}` });

    console.log(`\n=== ${t.vendor} ${t.pattern} ${t.color}  (${t.catalog}) ===`);
    console.log(`  image: ${t.image}`);
    console.log(`  width: ${t.specs.width}  desc-len: ${desc.replace(/<[^>]+>/g,' ').trim().length}`);

    if (!COMMIT) { console.log('  DRY-RUN — would attach image + width/material/repeat/uom metafields + description'); continue; }

    // 1) description + metafields (additive)
    const up = await gql(M_UPDATE, { input: { id: t.id, descriptionHtml: desc, metafields } });
    const ue = up.data?.productUpdate?.userErrors || [];
    if (ue.length) { console.log(`  ✗ update error: ${JSON.stringify(ue).slice(0,160)}`); continue; }
    console.log('  ✓ description + spec metafields attached');
    await sleep(800);

    // 2) image (Shopify fetches the external URL async)
    const im = await gql(M_FILES, { id: t.id, media: [{ originalSource: t.image, mediaContentType:'IMAGE', alt: `${t.pattern} ${t.color} | ${t.vendor}` }] });
    const ie = im.data?.productCreateMedia?.mediaUserErrors || [];
    if (ie.length) { console.log(`  ✗ image error: ${JSON.stringify(ie).slice(0,160)}`); continue; }
    console.log('  ✓ exact-match vendor image attached (async fetch)');
    await sleep(1200);
  }
  console.log(COMMIT ? '\nDONE (live). Re-run audit/verify to confirm gate-compliance.' : '\nDRY-RUN complete. Re-run with --commit.');
})();