← 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, ')'); }