← back to Shopify Sample Shipping
pilot-bigger.mjs
44 lines
import {query} from './query.mjs';
import fs from 'node:fs';
const SAMPLE_PROFILE='gid://shopify/DeliveryProfile/96764067891';
const ROLL='gid://shopify/ProductVariant/40348093055027'; // real product (stays on General)
const mut=`mutation($id:ID!,$profile:DeliveryProfileInput!){deliveryProfileUpdate(id:$id,profile:$profile){profile{id} userErrors{field message}}}`;
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 query(rq,{items})).draftOrderAvailableDeliveryOptions.availableShippingRates;}catch(e){return [];}};
const free=r=>r.some(x=>parseFloat(x.price.amount)===0);
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;};
// STEP 1: set sample-profile free rate to $0-$45 (>=0 & <=45). Delete existing free conditions then add.
const pr=(await query(`query($id:ID!){deliveryProfile(id:$id){profileLocationGroups{locationGroup{id}locationGroupZones(first:20){nodes{zone{id name}methodDefinitions(first:30){nodes{id name methodConditions{id}}}}}}}}`,{id:SAMPLE_PROFILE})).deliveryProfile;
let fm,flg,fzone;for(const g of pr.profileLocationGroups)for(const z of g.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=g.locationGroup.id;fzone=z.zone.id;}}
if(fm.methodConditions.length) await query(mut,{id:SAMPLE_PROFILE,profile:{conditionsToDelete:fm.methodConditions.map(c=>c.id)}});
await query(mut,{id:SAMPLE_PROFILE,profile:{locationGroupsToUpdate:[{id:flg,zonesToUpdate:[{id:fzone,methodDefinitionsToUpdate:[{id:fm.id,priceConditionsToCreate:[{operator:'GREATER_THAN_OR_EQUAL_TO',criteria:{amount:'0',currencyCode:'USD'}},{operator:'LESS_THAN_OR_EQUAL_TO',criteria:{amount:'45',currencyCode:'USD'}}]}]}]}]}});
console.log('STEP1: sample-profile free rate set to $0-$45');
// STEP 2: gather ~300 distinct sample variant ids
let cursor=null, samples=[];
const pq=`query($c:String){products(first:100,after:$c,query:"status:active"){pageInfo{hasNextPage endCursor} nodes{variants(first:5){nodes{id title price}}}}}`;
while(samples.length<300){const d=await query(pq,{c:cursor});for(const p of d.products.nodes){const sv=p.variants.nodes.find(v=>/^sample$/i.test(v.title)&&parseFloat(v.price)>0);if(sv)samples.push(sv.id);if(samples.length>=300)break;}if(!d.products.pageInfo.hasNextPage)break;cursor=d.products.pageInfo.endCursor;}
console.log('STEP2: gathered',samples.length,'distinct sample variants');
fs.writeFileSync('verification/pilot-variants.json',JSON.stringify(samples)+'\n');
// STEP 3: assign (chunks of 100)
for(const c of chunk(samples,100)){const a=await query(mut,{id:SAMPLE_PROFILE,profile:{variantsToAssociate:c}});if(a.deliveryProfileUpdate.userErrors.length)console.log('assoc err',JSON.stringify(a.deliveryProfileUpdate.userErrors));}
console.log('STEP3: assigned',samples.length,'-> sample profile. waiting for propagation...');
await sleep(20000);
// STEP 4: verify
const s6=samples.slice(0,3).map(v=>({variantId:v,quantity:2})); // 6 samples
const s12=samples.slice(0,4).map((v,i)=>({variantId:v,quantity:i<4?3:0})).filter(x=>x.quantity); // 12 samples
const mixed=[{variantId:samples[0],quantity:1},{variantId:ROLL,quantity:1}]; // sample + real roll
const r6=await rates(s6), r12=await rates(s12), rmix=await rates(mixed);
console.log('\n=== PILOT VERIFICATION (300 samples on dedicated profile) ===');
console.log(` 6 pilot samples ($25.50): ${free(r6)?'FREE ✅':'no free ❌'}`);
console.log(` 12 pilot samples ($51): ${free(r12)?'FREE ❌(should be charged)':'charged ✅ (>10 samples)'}`);
console.log(` MIXED (1 sample + 1 real roll): ${free(rmix)?'FREE ❌ (real product would ride free!)':'no free ✅ (real product keeps paid shipping)'} [${rmix.map(r=>r.title+' $'+r.price.amount).join(' | ')}]`);
// STEP 5: ROLLBACK the pilot (dissociate all back to General) — pilot only
for(const c of chunk(samples,100)){await query(mut,{id:SAMPLE_PROFILE,profile:{variantsToDissociate:c}});}
console.log('\nSTEP5: rolled back — all',samples.length,'pilot variants dissociated to General (clean state).');