← back to Shopify Sample Shipping

migrate-samples.mjs

49 lines

import {query} from './query.mjs';
import fs from 'node:fs';
const SAMPLE_PROFILE='gid://shopify/DeliveryProfile/96764067891';
const GENERAL='gid://shopify/DeliveryProfile/29033627699';
const ROLL='gid://shopify/ProductVariant/40348093055027';
const APPLY=process.argv.includes('--apply');
const mut=`mutation($id:ID!,$profile:DeliveryProfileInput!){deliveryProfileUpdate(id:$id,profile:$profile){userErrors{field message}}}`;
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const chunk=(a,n)=>{const o=[];for(let i=0;i<a.length;i+=n)o.push(a.slice(i,i+n));return o;};
async function q(gql,vars){for(let a=0;a<6;a++){try{return await query(gql,vars);}catch(e){if(/THROTTLED|throttl/i.test(e.message)){await sleep(2000*(a+1));continue;}throw e;}}throw new Error('throttled out');}
const rq=`query($items:[DraftOrderLineItemInput!]!){draftOrderAvailableDeliveryOptions(input:{lineItems:$items,shippingAddress:{address1:"15442 Ventura Blvd",city:"Sherman Oaks",provinceCode:"CA",countryCode:US,zip:"91403"}}){availableShippingRates{title price{amount}}}}`;
const rates=async items=>{try{return (await q(rq,{items})).draftOrderAvailableDeliveryOptions.availableShippingRates;}catch{return [];}};
const free=r=>r.some(x=>parseFloat(x.price.amount)===0);

const ids=JSON.parse(fs.readFileSync(new URL('./verification/all-sample-variants.json',import.meta.url)));
console.log('sample variants to migrate:',ids.length,'| APPLY=',APPLY);
if(ids.length<1000){console.log('ABORT: list suspiciously small (<1000) — refusing to run.');process.exit(1);}

// snapshot General free method (for rollback re-create)
const g=(await q(`query($id:ID!){deliveryProfile(id:$id){profileLocationGroups{locationGroup{id}locationGroupZones(first:20){nodes{zone{id name}methodDefinitions(first:30){nodes{id name rateProvider{__typename ... on DeliveryRateDefinition{price{amount currencyCode}}} methodConditions{id operator conditionCriteria{__typename ... on MoneyV2{amount}}}}}}}}}}`,{id:GENERAL})).deliveryProfile;
let fm,flg,fzone;for(const gr of g.profileLocationGroups)for(const z of gr.locationGroupZones.nodes){if(z.zone.name!=='Domestic')continue;for(const md of z.methodDefinitions.nodes)if(md.name==='Free Shipping (No Tracking)'){fm=md;flg=gr.locationGroup.id;fzone=z.zone.id;}}
fs.writeFileSync(new URL('./verification/migration-general-band-snapshot.json',import.meta.url),JSON.stringify({cap:'45',method:fm,lg:flg,zone:fzone},null,2));
console.log('snapshot: General free method',fm?.id,'price',fm?.rateProvider?.price?.amount);
if(!APPLY){console.log('DRY RUN — pass --apply to execute.');process.exit(0);}

// 1) assign all in chunks of 100
const chunks=chunk(ids,100); let done=0;
for(const c of chunks){const r=await q(mut,{id:SAMPLE_PROFILE,profile:{variantsToAssociate:c}});if(r.deliveryProfileUpdate.userErrors.length)console.log('assoc err',JSON.stringify(r.deliveryProfileUpdate.userErrors));done+=c.length;if(done%1000<100)console.log(`  assigned ${done}/${ids.length}`);await sleep(300);}
console.log('ASSIGN COMPLETE:',done,'variants. waiting for propagation...');
await sleep(25000);

// 2) verify samples free (sample profile) BEFORE removing band
const s6=ids.slice(0,3).map(v=>({variantId:v,quantity:2}));
console.log('verify 6 samples free (pre-band-removal):',free(await rates(s6))?'FREE ✅':'NO ❌ — ABORT before removing band');
if(!free(await rates(s6))){console.log('SAFETY ABORT: samples not free on sample profile; NOT removing General band.');process.exit(1);}

// 3) remove General free method -> real products never free
await q(mut,{id:GENERAL,profile:{methodDefinitionsToDelete:[fm.id]}});
console.log('REMOVED General Free Shipping (No Tracking) method.');
await sleep(20000);

// 4) verify: real product NOT free, samples STILL free
const rReal=await rates([{variantId:ROLL,quantity:1}]);
const rSample=await rates(s6);
console.log('\n=== POST-MIGRATION VERIFY ===');
console.log('  real roll: '+(free(rReal)?'FREE ❌ (should not be!)':'no free ✅ '+rReal.map(r=>r.title+' $'+r.price.amount).slice(0,2).join(', ')));
console.log('  6 samples: '+(free(rSample)?'FREE ✅':'no free ❌'));
console.log('\nMIGRATION DONE.');