← back to Dw Yolo Loop

gmc-thibaut-dw-9-exclude.js

80 lines

#!/usr/bin/env node
// GMC Thibaut+DW 9-scoped feed-exclude — GMC-remediation (2026-08-25)
// SCOPED unpublish of EXACTLY the 9 TRUE $4.25 memo-sample leaks (7 Thibaut Chloe/Dickinson
// + 2 DW/Harlequin legacy) off the "Google & YouTube" channel. These advertise the $4.25 memo
// sample as the product price and have NO sourceable per-roll price locally → the reversible fix
// is to drop the wrong offer from Google. Sibling of gmc-fentucci-exclude.js (identical mechanism).
// Reversible: writes a restore-map of exactly which GIDs were on Google (re-publish to reverse).
//
//   node gmc-thibaut-dw-9-exclude.js                              # DRY-RUN: how many of the 9 are on Google
//   node gmc-thibaut-dw-9-exclude.js --apply --yes-i-am-steve     # exclude all on-Google of the 9
import fs from 'node:fs';

const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const VERSION = '2024-10';
const ENV = fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1];
if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
const URL = `https://${SHOP}/admin/api/${VERSION}/graphql.json`;
const GOOGLE_PUBLICATION_ID = 'gid://shopify/Publication/29646651457';
const WORKLIST = '/Users/macstudio3/.claude/yolo-queue/gmc-leak-worklist-2026-08-24.json';
const RESTORE = '/Users/macstudio3/.claude/yolo-queue/gmc-thibaut-dw-9-exclude-restore-map.json';

const args = process.argv.slice(2);
const APPLY = args.includes('--apply');
const CONFIRMED = args.includes('--yes-i-am-steve');

async function gql(query, variables) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(URL, {
      method: 'POST',
      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
      body: JSON.stringify({ query, variables }),
    });
    if (res.status === 429 || res.status >= 500) { await new Promise(r => setTimeout(r, 1000 * (attempt + 1))); continue; }
    const j = await res.json();
    if (j.errors) { await new Promise(r => setTimeout(r, 800 * (attempt + 1))); continue; }
    return j.data;
  }
  throw new Error('gql failed after retries');
}

// Self-select the 9 from the master worklist: everything that is NOT Fentucci Naturals.
const wl = JSON.parse(fs.readFileSync(WORKLIST, 'utf8'));
const nine = (wl.leaks || []).filter(l => (l.vendor || '') !== 'Fentucci Naturals');
const ids = nine.map(l => l.product_gid).filter(Boolean);
console.log(`Thibaut+DW 9-scoped GMC exclude — ${APPLY ? (CONFIRMED ? 'APPLY' : 'APPLY-BLOCKED(no --yes-i-am-steve)') : 'DRY-RUN'}`);
console.log(`Worklist: ${ids.length} GIDs (vendors: ${[...new Set(nine.map(l => l.vendor))].join(', ')})`);
if (ids.length !== 9) console.log(`  ⚠️ note: selected ${ids.length} (expected 9) — check worklist vendor field before --apply`);
nine.forEach(l => console.log(`   • ${l.vendor} ${l.dw_sku || ''} "${l.title}" google=${l.current_google_offer_price}`));

// 1. Which are actually ON Google right now.
const onGoogle = [];
for (const id of ids) {
  const d = await gql(
    `query($id:ID!){ product(id:$id){ id title status onGoogle: publishedOnPublication(publicationId:"${GOOGLE_PUBLICATION_ID}") } }`,
    { id });
  const p = d?.product;
  if (p && p.onGoogle) onGoogle.push({ id: p.id, title: p.title, status: p.status });
}
console.log(`\nOf ${ids.length}: ${onGoogle.length} currently ON Google (would be excluded), ${ids.length - onGoogle.length} already off.`);

if (!APPLY) { console.log(`\nDRY-RUN only — no writes. Re-run with --apply --yes-i-am-steve to exclude the ${onGoogle.length}.`); process.exit(0); }
if (!CONFIRMED) { console.error('\nREFUSING: --apply requires --yes-i-am-steve (customer-facing Shopify write).'); process.exit(2); }

// 2. Apply: restore-map first (reversible), then unpublish.
fs.writeFileSync(RESTORE, JSON.stringify({ generated_at: new Date().toISOString(), publication: GOOGLE_PUBLICATION_ID,
  note: 'Re-publish to reverse: publishablePublish(id, [{publicationId}]). Do this after sourcing the real Thibaut per-roll price.', excluded: onGoogle }, null, 2));
console.log(`\nRestore-map written (${onGoogle.length} GIDs) -> ${RESTORE}\nExcluding ${onGoogle.length}...`);
let ok = 0, err = 0;
for (const t of onGoogle) {
  try {
    const d = await gql(
      `mutation($id:ID!,$pubs:[PublicationInput!]!){ publishableUnpublish(id:$id, input:$pubs){ userErrors{ message } } }`,
      { id: t.id, pubs: [{ publicationId: GOOGLE_PUBLICATION_ID }] });
    const ue = d?.publishableUnpublish?.userErrors;
    if (ue && ue.length) { err++; console.log(`  ✗ ${t.id}: ${ue.map(e => e.message).join(';')}`); } else { ok++; console.log(`  ✓ ${t.title}`); }
  } catch (e) { err++; console.log(`  ✗ ${t.id}: ${e.message}`); }
}
console.log(`\nDONE — excluded ${ok}, errors ${err}. Reverse via ${RESTORE}.`);