← back to Shopify Sample Shipping

profile.mjs

96 lines

import fs from 'node:fs';
import assert from 'node:assert/strict';
import {query} from './query.mjs';
const dir=new URL('./verification/',import.meta.url);
const read=name=>JSON.parse(fs.readFileSync(new URL(name,dir)));
const save=(name,data)=>fs.writeFileSync(new URL(name,dir),JSON.stringify(data,null,2)+'\n');
const original='gid://shopify/DeliveryProfile/29033627699';
const name='Samples — Free Shipping (No Tracking)';
const pilots=['44090076954675','44086096756787','44044973408307'].map(id=>'gid://shopify/ProductVariant/'+id);
const mutation='mutation($id:ID!,$profile:DeliveryProfileInput!){deliveryProfileUpdate(id:$id,profile:$profile){profile{id name} userErrors{field message}}}';
function method(m,domestic){
 const out={name:m.name,description:m.description,active:m.active};
 const r=m.rateProvider;
 if(r.__typename==='DeliveryRateDefinition')out.rateDefinition={price:r.price};
 else out.participant={carrierServiceId:r.carrierService.id,fixedFee:r.fixedFee,percentageOfRateFee:r.percentageOfRateFee,participantServices:r.participantServices};
 if(!(domestic&&m.name==='Free Shipping (No Tracking)'))for(const c of m.methodConditions){
   const k=c.field==='TOTAL_WEIGHT'?'weightConditionsToCreate':'priceConditionsToCreate';
   const {__typename,...criteria}=c.conditionCriteria;
   (out[k]??=[]).push({operator:c.operator,criteria});
 }
 return out;
}
function build(){
 const p=read('general-detail.json').deliveryProfile;
 assert.equal(p.id,original);
 return {name,locationGroupsToCreate:p.profileLocationGroups.map(g=>({
   locationsToAdd:g.locationGroup.locations.nodes.map(l=>l.id),
   zonesToCreate:g.locationGroupZones.nodes.map(z=>({
     name:z.zone.name,countries:z.zone.countries.map(c=>({...(c.code.restOfWorld?{restOfWorld:true}:{code:c.code.countryCode}),provinces:c.provinces.map(p=>({code:p.code}))})),
     methodDefinitionsToCreate:z.methodDefinitions.nodes.map(m=>method(m,z.zone.name==='Domestic'))
   }))
 }))};
}
async function validateIds(ids,allowed){
 const j=await query('query($ids:[ID!]!){nodes(ids:$ids){... on ProductVariant{id title product{status} deliveryProfile{id}}}}',{ids});
 assert.equal(j.nodes.length,ids.length);
 for(const v of j.nodes){assert.ok(v&&ids.includes(v.id));assert.match(v.title,/\bsample\b/i);assert.equal(v.product.status,'ACTIVE');assert.ok(allowed.includes(v.deliveryProfile.id));}
 return j.nodes;
}
async function assign(id,ids){
 const j=await query(mutation,{id,profile:{variantsToAssociate:ids}});
 if(j.deliveryProfileUpdate.userErrors.length)throw Error(JSON.stringify(j));
 return j;
}
const mode=process.argv[2];
if(mode==='prepare'){
 const input=build();save('profile-create-input.json',input);
 const free=input.locationGroupsToCreate.flatMap(g=>g.zonesToCreate).find(z=>z.name==='Domestic').methodDefinitionsToCreate.find(m=>m.name==='Free Shipping (No Tracking)');
 assert.equal(Number(free.rateDefinition.price.amount),0);assert.equal(free.weightConditionsToCreate,undefined);
 console.log(JSON.stringify({name:input.name,groups:input.locationGroupsToCreate.length,zones:input.locationGroupsToCreate[0].zonesToCreate.map(z=>({name:z.name,countries:z.countries.length,methods:z.methodDefinitionsToCreate.map(m=>m.name)})),pilotVariants:pilots}));
}else if(mode==='create'){
 if(fs.existsSync(new URL('profile-created.json',dir)))throw Error('Profile already created; use saved ID');
 const existing=await query('{deliveryProfiles(first:100){nodes{id name}}}');
 assert.ok(!existing.deliveryProfiles.nodes.some(p=>p.name===name),'Existing sample profile needs reconciliation');
 const input=read('profile-create-input.json');assert.deepEqual(input,build());
 const j=await query('mutation($profile:DeliveryProfileInput!){deliveryProfileCreate(profile:$profile){profile{id name} userErrors{field message}}}',{profile:input});
 if(j.deliveryProfileCreate.userErrors.length)throw Error(JSON.stringify(j));
 save('profile-created.json',j);console.log(JSON.stringify(j));
}else if(mode==='ensure-weight-floor'){
 const p=read('sample-detail.json').deliveryProfile;
 const g=p.profileLocationGroups[0];const z=g.locationGroupZones.nodes.find(z=>z.zone.name==='Domestic');
 const m=z.methodDefinitions.nodes.find(m=>m.name==='Free Shipping (No Tracking)');
 assert.deepEqual(m.methodConditions,[]);
 const profile={locationGroupsToUpdate:[{id:g.locationGroup.id,zonesToUpdate:[{id:z.zone.id,methodDefinitionsToUpdate:[{id:m.id,weightConditionsToCreate:[{operator:'GREATER_THAN_OR_EQUAL_TO',criteria:{value:0,unit:'POUNDS'}}]}]}]}]};
 const j=await query(mutation,{id:p.id,profile});if(j.deliveryProfileUpdate.userErrors.length)throw Error(JSON.stringify(j));save('weight-floor-result.json',j);console.log(JSON.stringify(j));
}else if(mode==='normalize-us-zone'){
 const p=read('sample-detail.json').deliveryProfile;const g=p.profileLocationGroups[0];const z=g.locationGroupZones.nodes.find(z=>z.zone.name==='Domestic');
 const profile={locationGroupsToUpdate:[{id:g.locationGroup.id,zonesToUpdate:[{id:z.zone.id,countries:[{code:'US',includeAllProvinces:true}]}]}]};
 const j=await query(mutation,{id:p.id,profile});if(j.deliveryProfileUpdate.userErrors.length)throw Error(JSON.stringify(j));save('normalize-us-result.json',j);console.log(JSON.stringify(j));
}else if(mode==='pilot'){
 const id=read('profile-created.json').deliveryProfileCreate.profile.id;
 const before=await validateIds(pilots,[original]);save('pilot-before.json',before);
 save('pilot-result.json',await assign(id,pilots));
 const after=await validateIds(pilots,[id]);save('pilot-after.json',after);
 console.log(JSON.stringify({profile:id,pilots:after.map(v=>v.id),rollback:{profile:original,variants:pilots}}));
}else if(mode==='associate-all'){
 assert.equal(read('e2e-proof.json').status,'PASS','Broad rollout blocked: pilot E2E has not passed');
 const id=read('profile-created.json').deliveryProfileCreate.profile.id;
 const variants=read('sample-variants.json');
 assert.ok(variants.length>0);
 for(const v of variants){assert.match(v.title,/\bsample\b/i);assert.equal(v.product.status,'ACTIVE');assert.equal(v.deliveryProfile.id,original);}
 const journal=new URL('association-journal.jsonl',dir);
 const done=new Set(fs.existsSync(journal)?fs.readFileSync(journal,'utf8').trim().split('\n').filter(Boolean).flatMap(s=>JSON.parse(s).ids):[]);
 const remaining=variants.filter(v=>!done.has(v.id));
 for(let i=0;i<remaining.length;i+=250){
   const ids=remaining.slice(i,i+250).map(v=>v.id);
   await assign(id,ids);
   fs.appendFileSync(journal,JSON.stringify({at:new Date().toISOString(),profile:id,original,ids})+'\n');
   console.log(JSON.stringify({assigned:Math.min(i+250,remaining.length)+done.size,total:variants.length,persistedVerification:'pending full bulk readback'}));
   await new Promise(r=>setTimeout(r,800));
 }
}else if(mode==='rollback-pilot'){
 const id=read('profile-created.json').deliveryProfileCreate.profile.id;
 await validateIds(pilots,[id]);await assign(original,pilots);await validateIds(pilots,[original]);console.log('Pilot rollback verified');
}else throw Error('Use prepare, create, pilot, associate-all, or rollback-pilot');