← back to Shopify Sample Shipping
create-freeship-codes.mjs
86 lines
#!/usr/bin/env node
// TK-11333 — create the two free-shipping CODE discounts (Option A):
// • TRADESHIP — DiscountCodeFreeShipping scoped to the DW Trade / Designers SEGMENT
// (server-enforced: retail literally cannot claim it even by pasting the code).
// • SAMPLESHIP — DiscountCodeFreeShipping, ALL customers, maximumShippingPrice guarded.
// Both carry maximumShippingPrice '30' so a freight roll in a mixed cart never rides free.
// Customer selection is set via `context` (2024-10+ moved it off customerSelection to
// context.customerSegments / context.all=ALL — verified by introspection 2026-09-09).
//
// GATED (customer-facing config write). DRY-RUN BY DEFAULT — prints the exact mutation+vars.
// Idempotent: skips a code whose `code` already exists. Records created ids to
// verification/freeship-codes-created.json for create-freeship-codes-undo.mjs.
//
// node create-freeship-codes.mjs # dry-run (prints both mutations)
// node create-freeship-codes.mjs --apply # WRITE (Steve-gated) — segment must exist first
import { query } from './query.mjs';
import fs from 'node:fs';
const APPLY = process.argv.includes('--apply');
const MAX_SHIP = '30';
const SEG_REC = new URL('./verification/trade-segment-created.json', import.meta.url);
const OUT = new URL('./verification/freeship-codes-created.json', import.meta.url);
const LOGX = process.env.HOME + '/.claude/yolo-queue/executed-reversible/log-exec.mjs';
let segIds = [];
if (fs.existsSync(SEG_REC)) {
const rec = JSON.parse(fs.readFileSync(SEG_REC, 'utf8'));
segIds = rec.segmentIds || (rec.segmentId ? [rec.segmentId] : []);
}
if (!segIds.length) {
if (APPLY) { console.error('MISSING segmentIds — run create-trade-segment.mjs --apply FIRST'); process.exit(1); }
segIds = ['gid://shopify/Segment/PENDING']; // dry-run placeholder so the mutation shape prints
console.log('(dry-run: no segment created yet — using placeholder; create the segment before --apply)');
}
const now = new Date().toISOString();
const combines = { orderDiscounts: true, productDiscounts: true, shippingDiscounts: false };
const CODES = [
{ code: 'TRADESHIP', title: 'DW Trade Sample Free Shipping', context: { customerSegments: { add: segIds } } },
{ code: 'SAMPLESHIP', title: 'DW Retail Sample Free Shipping', context: { all: 'ALL' } },
];
const MUT = `mutation($fs:DiscountCodeFreeShippingInput!){discountCodeFreeShippingCreate(freeShippingCodeDiscount:$fs){codeDiscountNode{id codeDiscount{__typename ... on DiscountCodeFreeShipping{title status}}} userErrors{field message}}}`;
function buildInput(c) {
// NOTE: appliesOnOneTimePurchase/appliesOnSubscription omitted — this shop has no subscriptions,
// and Shopify rejects those fields unless subscriptions are enabled.
return { title: c.title, code: c.code, startsAt: now,
destination: { all: true }, maximumShippingPrice: MAX_SHIP, combinesWith: combines, context: c.context };
}
async function codeExists(code) {
const r = await query(`{codeDiscountNodes(first:5,query:${JSON.stringify('code:' + code)}){nodes{id codeDiscount{__typename ... on DiscountCodeFreeShipping{title codes(first:5){nodes{code}}}}}}}`);
const hit = r.codeDiscountNodes.nodes.find(n => n.codeDiscount?.codes?.nodes?.some(x => x.code === code));
return hit?.id || null;
}
console.log('=== create-freeship-codes (' + (APPLY ? 'APPLY' : 'DRY-RUN') + ') ===');
console.log('segments (designer code):', segIds.join(', '));
console.log('maximumShippingPrice : $' + MAX_SHIP);
const result = { at: now, segIds, maxShip: MAX_SHIP, created: [] };
for (const c of CODES) {
const input = buildInput(c);
console.log(`\n• ${c.code} — ${c.title}`);
console.log(' vars:', JSON.stringify({ fs: input }));
const existing = await codeExists(c.code);
if (existing) { console.log(' ALREADY EXISTS ->', existing, '(skip)'); result.created.push({ code: c.code, id: existing, preexisting: true }); continue; }
if (!APPLY) { console.log(' WOULD create.'); continue; }
const r = (await query(MUT, { fs: input })).discountCodeFreeShippingCreate;
if (r.userErrors?.length) { console.error(' ERR', JSON.stringify(r.userErrors)); continue; }
console.log(' created ->', r.codeDiscountNode.id, r.codeDiscountNode.codeDiscount?.status);
result.created.push({ code: c.code, id: r.codeDiscountNode.id });
}
if (!APPLY) { console.log('\nDry-run only. Re-run with --apply.'); process.exit(0); }
fs.writeFileSync(OUT, JSON.stringify(result, null, 2) + '\n');
console.log('\nrecorded -> verification/freeship-codes-created.json');
try {
const { execSync } = await import('node:child_process');
const ids = result.created.filter(c => !c.preexisting).map(c => c.code).join(',');
execSync(`node ${LOGX} --agent vp-dw-commerce --ticket TK-11333 ` +
`--action ${JSON.stringify('created free-ship codes ' + (ids || '(none new)'))} --blast ${result.created.length} ` +
`--undo ${JSON.stringify('cd ~/Projects/shopify-sample-shipping && node create-freeship-codes-undo.mjs --apply')} ` +
`--verify ${JSON.stringify('node disc-feasible.mjs')}`, { stdio: 'inherit' });
} catch (e) { console.log('(ledger note skipped:', e.message, ')'); }