← back to Shopify Sample Shipping

remove-band.mjs

59 lines

#!/usr/bin/env node
// TK-11333 — remove the blunt "Free Shipping (No Tracking)" $0–$45 band so base shipping =
// honest ups/fedex carrier rates (fixes GAP2: free untracked shipping leaking to ANY product
// <= $45). Free SAMPLE shipping now comes from the segment/all-customer CODES instead.
//
// MECHANISM: the delivery API has NO methodDefinitionsToDelete, but a method has an `active`
// flag — so we DEACTIVATE the free method (exact-GID, trivially reversible) rather than delete.
// LAST STEP — run only AFTER grandfather + proto-autoapply PASS + codes exist.
//
// GATED (customer-facing shipping change). DRY-RUN BY DEFAULT; --apply to write.
// Snapshots to verification/remove-band-snapshot.json; undo = restore-band.mjs (reactivates
// + restores the exact prior conditions from that snapshot).
import { query } from './query.mjs';
import fs from 'node:fs';

const APPLY = process.argv.includes('--apply');
const PROFILE = 'gid://shopify/DeliveryProfile/29033627699';
const METHOD_NAME = 'Free Shipping (No Tracking)';
const SNAP = new URL('./verification/remove-band-snapshot.json', import.meta.url);
const LOGX = process.env.HOME + '/.claude/yolo-queue/executed-reversible/log-exec.mjs';

const rq = `query($id:ID!){deliveryProfile(id:$id){profileLocationGroups{locationGroup{id} locationGroupZones(first:20){nodes{zone{id name} methodDefinitions(first:40){nodes{id name active rateProvider{__typename ... on DeliveryRateDefinition{price{amount currencyCode}}} methodConditions{id operator conditionCriteria{__typename ... on MoneyV2{amount currencyCode}}}}}}}}}}`;
const d = await query(rq, { id: PROFILE });
let m = null, lg = null, zone = null;
for (const g of d.deliveryProfile.profileLocationGroups) for (const z of g.locationGroupZones.nodes) {
  if (z.zone.name !== 'Domestic') continue;
  for (const md of z.methodDefinitions.nodes) if (md.name === METHOD_NAME) { m = md; lg = g.locationGroup.id; zone = z.zone.id; }
}
if (!m) {
  // The $0 free band is already absent from the General profile's Domestic zone (verified
  // 2026-09-09: Domestic = ups_shipping + fedex only). Removal is already done — idempotent success.
  console.log('\nBAND ALREADY ABSENT — no "' + METHOD_NAME + '" method in the Domestic zone.');
  console.log('GAP2 is already closed on the General profile: base domestic shipping = carrier (ups/fedex).');
  console.log('Nothing to remove. (Free sample shipping must come from the CODES.)');
  process.exit(0);
}

const snapshot = { at: new Date().toISOString(), profile: PROFILE, lg, zone, method: m };
console.log('=== remove-band (' + (APPLY ? 'APPLY' : 'DRY-RUN') + ') ===');
console.log('method:', m.name, '| active:', m.active, '| price:', m.rateProvider?.price?.amount);
console.log('conditions:', m.methodConditions.map(c => `${c.operator} $${c.conditionCriteria?.amount}`).join(' & ') || '(none)');

if (!m.active) { console.log('\nAlready INACTIVE — nothing to do (idempotent).'); fs.writeFileSync(SNAP, JSON.stringify(snapshot, null, 2) + '\n'); process.exit(0); }
if (!APPLY) { console.log('\n-- WOULD set method active:false (deactivate the $0-band). Re-run with --apply.'); fs.writeFileSync(SNAP, JSON.stringify(snapshot, null, 2) + '\n'); console.log('snapshot -> verification/remove-band-snapshot.json'); process.exit(0); }

fs.writeFileSync(SNAP, JSON.stringify(snapshot, null, 2) + '\n');
const mut = `mutation($id:ID!,$profile:DeliveryProfileInput!){deliveryProfileUpdate(id:$id,profile:$profile){userErrors{field message}}}`;
const r = (await query(mut, { id: PROFILE, profile: { locationGroupsToUpdate: [{ id: lg, zonesToUpdate: [{ id: zone, methodDefinitionsToUpdate: [{ id: m.id, active: false }] }] }] } })).deliveryProfileUpdate;
if (r.userErrors?.length) { console.error('ERR', JSON.stringify(r.userErrors)); process.exit(1); }
console.log('\nband DEACTIVATED. Base shipping is now carrier rates; free sample shipping via the CODES only.');
console.log('snapshot -> verification/remove-band-snapshot.json  (undo: node restore-band.mjs --apply)');
try {
  const { execSync } = await import('node:child_process');
  execSync(`node ${LOGX} --agent vp-dw-commerce --ticket TK-11333 ` +
    `--action ${JSON.stringify('deactivated "' + METHOD_NAME + '" $0-band (GAP2 fix)')} --blast 1 ` +
    `--undo ${JSON.stringify('cd ~/Projects/shopify-sample-shipping && node restore-band.mjs --apply')} ` +
    `--verify ${JSON.stringify('node live-rate-state.mjs')}`, { stdio: 'inherit' });
} catch (e) { console.log('(ledger note skipped:', e.message, ')'); }