← back to Shopify Sample Shipping

create-trade-segment.mjs

108 lines

#!/usr/bin/env node
// TK-11333 — create the `DW Trade / Designers` customer segment(s), and delete the broken
// `interior-designer-res` segment (which queries a literal that matches 0 customers).
//
// Shopify caps a customer-segment query at 10 filters. Our tag set is 12 (19 with --with-confirm),
// so we split into ≤10-filter segments ("DW Trade / Designers", "DW Trade / Designers (2)", …).
// The free-ship discount code scopes to ALL of them (multi-segment) — see create-freeship-codes.mjs.
// `sample-freeship` (the grandfather vehicle for the 2,663) is ALWAYS included.
//
// GATED (customer-facing config write). DRY-RUN BY DEFAULT — prints the exact segmentCreate
// queries it WOULD run. Pass --apply to write. Idempotent: reuses same-named segments, no dupes.
// Records created ids to verification/trade-segment-created.json for the paired undo.
//
// Usage:
//   node create-trade-segment.mjs                 # dry-run
//   node create-trade-segment.mjs --with-confirm  # dry-run incl. the §4 "confirm-these" tags
//   node create-trade-segment.mjs --apply         # WRITE (Steve-gated)
import { query } from './query.mjs';
import fs from 'node:fs';

const APPLY = process.argv.includes('--apply');
const WITH_CONFIRM = process.argv.includes('--with-confirm');
const SEG_NAME = 'DW Trade / Designers';
const BROKEN_NAME = 'interior-designer-res';
const OUT = new URL('./verification/trade-segment-created.json', import.meta.url);
const LOGX = process.env.HOME + '/.claude/yolo-queue/executed-reversible/log-exec.mjs';

// Real trade tags (memo §4 core) + the dedicated grandfather vehicle + the theme entitlement tag.
const CORE = ['trade', 'trade_approved', 'Interior Designer - Residential', 'Interior Designer - Commercial',
  'interior design', 'interior designer', 'interior', 'Contractor', 'Commercial Property Owner',
  'Architect', 'Wallcovering Installer', 'sample-freeship'];
const CONFIRM = ['Photography Studio', 'Graphic Designer', 'Illustrator', 'Visual Merchandiser',
  'Production Company', 'Manufacturer', 'Developer'];
const tags = WITH_CONFIRM ? [...CORE, ...CONFIRM] : CORE;

// --- chunk into ≤10-filter segments; guarantee sample-freeship lands in a chunk (it's in CORE) ---
const CHUNK = 10;
const chunks = [];
for (let i = 0; i < tags.length; i += CHUNK) chunks.push(tags.slice(i, i + CHUNK));
const chunkQuery = c => c.map(t => `customer_tags CONTAINS '${t.replace(/'/g, "\\'")}'`).join(' OR ');
const segNameFor = i => i === 0 ? SEG_NAME : `${SEG_NAME} (${i + 1})`;

async function existingSegments() {
  return (await query(`{segments(first:200){nodes{id name query}}}`)).segments.nodes;
}

console.log('=== create-trade-segment  (' + (APPLY ? 'APPLY' : 'DRY-RUN') + ') ===');
console.log('base name    :', SEG_NAME);
console.log('tag set      :', WITH_CONFIRM ? 'CORE + confirm-these' : 'CORE only', `(${tags.length} tags → ${chunks.length} segment(s), ≤10 filters each)`);
chunks.forEach((c, i) => console.log(`  [${segNameFor(i)}]  (${c.length}) ${chunkQuery(c)}`));

const segs = await existingSegments();
const broken = segs.find(s => s.name === BROKEN_NAME);
chunks.forEach((c, i) => { const ex = segs.find(s => s.name === segNameFor(i)); console.log(`existing "${segNameFor(i)}"?`, ex ? `YES ${ex.id}` : 'no'); });
console.log('broken interior-designer-res?', broken ? `YES ${broken.id} (query: ${broken.query})` : 'no');

if (!APPLY) {
  chunks.forEach((c, i) => console.log(`-- WOULD segmentCreate("${segNameFor(i)}", <${c.length} filters>)`));
  if (broken) console.log('-- WOULD segmentDelete(' + broken.id + ')  [snapshot recorded first]');
  console.log('\nDry-run only. Re-run with --apply to write.');
  process.exit(0);
}

const result = { at: new Date().toISOString(), segName: SEG_NAME, tags, segments: [] };

// 1) create (or reuse) each chunk segment
const segmentIds = [];
for (let i = 0; i < chunks.length; i++) {
  const nm = segNameFor(i);
  const q = chunkQuery(chunks[i]);
  const ex = segs.find(s => s.name === nm);
  let segId = ex?.id;
  if (!segId) {
    const r = (await query(`mutation($name:String!,$q:String!){segmentCreate(name:$name,query:$q){segment{id name query} userErrors{field message}}}`,
      { name: nm, q })).segmentCreate;
    if (r.userErrors?.length) { console.error('segmentCreate ERR', nm, JSON.stringify(r.userErrors)); process.exit(1); }
    segId = r.segment.id;
    console.log('created segment', nm, segId);
  } else {
    console.log('re-using existing segment', nm, segId, '(no duplicate created)');
  }
  segmentIds.push(segId);
  result.segments.push({ name: nm, id: segId, query: q });
}
result.segmentIds = segmentIds;
result.segmentId = segmentIds[0]; // back-compat

// 2) snapshot + delete the broken segment
if (broken) {
  result.brokenSnapshot = { id: broken.id, name: broken.name, query: broken.query };
  const del = (await query(`mutation($id:ID!){segmentDelete(id:$id){deletedSegmentId userErrors{field message}}}`, { id: broken.id })).segmentDelete;
  if (del.userErrors?.length) { console.error('segmentDelete ERR', JSON.stringify(del.userErrors)); }
  else console.log('deleted broken segment', del.deletedSegmentId);
}

fs.writeFileSync(OUT, JSON.stringify(result, null, 2) + '\n');
console.log('\nsegmentIds:', segmentIds.join(', '));
console.log('recorded -> verification/trade-segment-created.json');

// 3) ledger (reversible)
try {
  const { execSync } = await import('node:child_process');
  execSync(`node ${LOGX} --agent vp-dw-commerce --ticket TK-11333 ` +
    `--action ${JSON.stringify('created ' + segmentIds.length + ' segment(s) "' + SEG_NAME + '" + deleted broken interior-designer-res')} --blast ${segmentIds.length + (broken ? 1 : 0)} ` +
    `--undo ${JSON.stringify('cd ~/Projects/shopify-sample-shipping && node create-trade-segment-undo.mjs --apply')} ` +
    `--verify ${JSON.stringify('node list-designer-segments.mjs')}`, { stdio: 'inherit' });
} catch (e) { console.log('(ledger note skipped:', e.message, ')'); }