← back to Shopify Sample Shipping

grandfather-apply.mjs

96 lines

#!/usr/bin/env node
// TK-11333 — grandfather TIER-1 designers (2,663) so they KEEP free sample shipping when
// retail-charging goes live ("lose no designers"). Adds the dedicated `sample-freeship` tag
// (NOT `trade` — see trade-grant-check.mjs verdict) to each TIER-1 customer.
//
// GATED (identity/customer write, blast radius 2,663 > 500 → hard-gated regardless of
// reversibility). DRY-RUN BY DEFAULT. Resumable + idempotent + fully reversible.
//
//   node grandfather-apply.mjs                 # dry-run: plan + validate a few lookups
//   node grandfather-apply.mjs --limit 20      # dry-run of a 20-customer pilot slice
//   node grandfather-apply.mjs --apply         # WRITE all pending (Steve-gated)
//   node grandfather-apply.mjs --apply --limit 20   # WRITE only the first 20 pending (pilot)
//   node grandfather-apply.mjs --apply --tier2 # also include TIER-2 (765) — default is TIER-1 only
//
// Resume: re-running --apply skips customers already tagged (progress in
// verification/grandfather-progress.json). Undo: grandfather-undo.mjs reads the exact
// applied list (verification/grandfather-applied.json) and tagsRemove.
import { query } from './query.mjs';
import fs from 'node:fs';

const APPLY = process.argv.includes('--apply');
const WITH_TIER2 = process.argv.includes('--tier2');
const li = process.argv.indexOf('--limit');
const LIMIT = li >= 0 ? Number(process.argv[li + 1]) : Infinity;
const TAG = 'sample-freeship';

const LIST = JSON.parse(fs.readFileSync(new URL('./verification/grandfather-list.json', import.meta.url), 'utf8'));
const PROG = new URL('./verification/grandfather-progress.json', import.meta.url);
const APPLIED = new URL('./verification/grandfather-applied.json', import.meta.url);
const LOGX = process.env.HOME + '/.claude/yolo-queue/executed-reversible/log-exec.mjs';

const targets = [...LIST.tier1, ...(WITH_TIER2 ? LIST.tier2 : [])];
const prog = fs.existsSync(PROG) ? JSON.parse(fs.readFileSync(PROG, 'utf8')) : { doneEmails: [], missing: [], ambiguous: [] };
const done = new Set(prog.doneEmails.map(e => e.toLowerCase()));
const applied = fs.existsSync(APPLIED) ? JSON.parse(fs.readFileSync(APPLIED, 'utf8')) : [];
const appliedSet = new Set(applied.map(a => a.email.toLowerCase()));

const sleep = ms => new Promise(r => setTimeout(r, ms));
async function q(gql, vars) { for (let a = 0; a < 6; a++) { try { return await query(gql, vars); } catch (e) { if (a === 5) throw e; await sleep(500 * (a + 1)); } } }
async function findCustomer(email) {
  const r = await q(`query($q:String!){customers(first:3,query:$q){edges{node{id email tags}}}}`, { q: `email:${email}` });
  const exact = r.customers.edges.filter(e => (e.node.email || '').toLowerCase() === email.toLowerCase());
  if (exact.length === 1) return exact[0].node;
  if (exact.length > 1) return { ambiguous: true };
  return null;
}

const pending = targets.filter(t => !done.has(t.email.toLowerCase())).slice(0, LIMIT);
console.log('=== grandfather-apply (' + (APPLY ? 'APPLY' : 'DRY-RUN') + ') ===');
console.log('tag           :', TAG);
console.log('tier          :', WITH_TIER2 ? 'TIER-1 + TIER-2' : 'TIER-1 only');
console.log('total targets :', targets.length, '| already done:', done.size, '| pending this run:', pending.length, LIMIT !== Infinity ? `(limit ${LIMIT})` : '');

if (!APPLY) {
  console.log('\n-- validating first 5 email lookups (read-only) --');
  for (const t of pending.slice(0, 5)) {
    const c = await findCustomer(t.email);
    console.log(`  ${t.email} -> ${c?.ambiguous ? 'AMBIGUOUS' : c ? c.id + (c.tags?.includes(TAG) ? ' [already tagged]' : '') : 'NOT FOUND'}`);
  }
  console.log(`\nWOULD tagsAdd '${TAG}' to up to ${pending.length} customers. Re-run with --apply.`);
  process.exit(0);
}

let tagged = 0, already = 0, missing = 0, ambiguous = 0, i = 0;
for (const t of pending) {
  i++;
  const email = t.email;
  try {
    const c = await findCustomer(email);
    if (!c) { prog.missing.push(email); missing++; done.add(email.toLowerCase()); prog.doneEmails.push(email); }
    else if (c.ambiguous) { prog.ambiguous.push(email); ambiguous++; done.add(email.toLowerCase()); prog.doneEmails.push(email); }
    else if (c.tags?.includes(TAG)) { already++; done.add(email.toLowerCase()); prog.doneEmails.push(email); if (!appliedSet.has(email.toLowerCase())) { applied.push({ email, customerId: c.id, preexisting: true }); appliedSet.add(email.toLowerCase()); } }
    else {
      const r = (await q(`mutation($id:ID!,$tags:[String!]!){tagsAdd(id:$id,tags:$tags){userErrors{field message}}}`, { id: c.id, tags: [TAG] })).tagsAdd;
      if (r.userErrors?.length) { console.error('  tagsAdd ERR', email, JSON.stringify(r.userErrors)); }
      else { tagged++; applied.push({ email, customerId: c.id }); appliedSet.add(email.toLowerCase()); done.add(email.toLowerCase()); prog.doneEmails.push(email); }
    }
  } catch (e) { console.error('  ERR', email, e.message.slice(0, 100)); }
  if (i % 50 === 0) {
    fs.writeFileSync(PROG, JSON.stringify(prog, null, 2)); fs.writeFileSync(APPLIED, JSON.stringify(applied, null, 2));
    console.log(`  ...${i}/${pending.length}  tagged=${tagged} already=${already} missing=${missing} ambiguous=${ambiguous}`);
  }
  await sleep(120);
}
fs.writeFileSync(PROG, JSON.stringify(prog, null, 2)); fs.writeFileSync(APPLIED, JSON.stringify(applied, null, 2));
console.log(`\nDONE this run: tagged=${tagged} already=${already} missing=${missing} ambiguous=${ambiguous}`);
console.log('applied list -> verification/grandfather-applied.json (', applied.length, 'total tagged )');

try {
  const { execSync } = await import('node:child_process');
  execSync(`node ${LOGX} --agent vp-dw-commerce --ticket TK-11333 ` +
    `--action ${JSON.stringify(`grandfather tagged '${TAG}' on ${tagged} customers this run (${applied.length} total)`)} --blast ${applied.length} ` +
    `--undo ${JSON.stringify('cd ~/Projects/shopify-sample-shipping && node grandfather-undo.mjs --apply')} ` +
    `--verify ${JSON.stringify('node -e "const a=require(\\"./verification/grandfather-applied.json\\");console.log(a.length,\\"tagged\\")"')}`, { stdio: 'inherit' });
} catch (e) { console.log('(ledger note skipped:', e.message, ')'); }