← back to Flock Fix Viewer

update-flock.mjs

71 lines

// Remove the wrong `quotes` tag from ALL priced Phillipe Romano flock products.
// Reversible (re-add tag) + ledgered. Idempotent. Excludes Maharam / non-PR / sample-only(no roll price).
import fs from 'fs';
import os from 'os';
import path from 'path';

const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = fs.readFileSync(new URL('./.token', import.meta.url), 'utf8').trim();
const LEDGER = path.join(os.homedir(), '.claude/yolo-queue/executed-reversible/ledger.jsonl');
const DRY = !process.argv.includes('--apply');

async function gql(query, variables = {}) {
  const r = await fetch(`https://${SHOP}/admin/api/2024-10/graphql.json`, {
    method: 'POST',
    headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables })
  });
  return r.json();
}
const sleep = ms => new Promise(r => setTimeout(r, ms));

const Q = `query($c:String){
  products(first:100, query:"vendor:'Phillipe Romano' status:active (tag:'Flock Velvet' OR title:flock OR handle:flock)", after:$c){
    pageInfo{hasNextPage endCursor}
    edges{node{ id handle title tags variants(first:8){edges{node{title price}}} }}
  }
}`;

const UPD = `mutation($id:ID!,$tags:[String!]!){ productUpdate(input:{id:$id, tags:$tags}){ product{id tags} userErrors{field message} } }`;

function isQuoteTag(t){ return String(t).replace(/["{}]/g,'').trim().toLowerCase() === 'quotes'; }

(async () => {
  let cur = null, all = [];
  do {
    const r = await gql(Q, { c: cur });
    const p = r?.data?.products; if (!p) { console.error('query err', JSON.stringify(r)); break; }
    all = all.concat(p.edges);
    cur = p.pageInfo.hasNextPage ? p.pageInfo.endCursor : null;
  } while (cur);

  const targets = all.map(e => e.node).filter(n => {
    const hasQuotes = (n.tags || []).some(isQuoteTag);
    const roll = (n.variants.edges || []).map(v => v.node).filter(v => !/sample/i.test(v.title || ''));
    const price = roll.reduce((m, v) => Math.max(m, parseFloat(v.price) || 0), 0);
    return hasQuotes && price > 10;
  });

  console.log(`Scanned ${all.length} PR flock products · ${targets.length} need quotes-tag removal (priced + quote-tagged)`);
  if (DRY) { targets.forEach(n => console.log('  would fix', n.handle)); return; }

  let done = 0, failed = 0;
  for (const n of targets) {
    const newTags = (n.tags || []).filter(t => !isQuoteTag(t));
    const r = await gql(UPD, { id: n.id, tags: newTags });
    const errs = r?.data?.productUpdate?.userErrors;
    if (errs && errs.length) { failed++; console.log('  FAIL', n.handle, JSON.stringify(errs)); continue; }
    done++;
    const sku = ((n.variants.edges[0]||{}).node||{}).title || n.handle;
    fs.appendFileSync(LEDGER, JSON.stringify({
      ts: new Date().toISOString(), agent: 'main:flock-update', ticket: 'flock-quotes',
      action: `remove quotes tag from ${n.handle}`, blast_radius: 1,
      undo_cmd: `productUpdate ${n.id} tags += quotes`,
      verify: `GET /products/${n.handle}.json → tags no longer include quotes; price + add-to-cart visible`
    }) + '\n');
    if (done % 5 === 0) console.log(`  …${done}/${targets.length}`);
    await sleep(350); // gentle on rate limit
  }
  console.log(`DONE · removed quotes from ${done} · failed ${failed}`);
})();