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