← back to Shopify Sample Shipping

find-defacto-designers.mjs

49 lines

// READ-ONLY: find de-facto designers = customers whose past SAMPLE orders were $0 (comped samples)
// or shipped free, but who lack a trade/designer tag -> they'd be "lost" (start getting charged) under the restrict-to-designers policy.
import {query} from './query.mjs';
import fs from 'node:fs';
const DESIGNER_TAG=/trade|design|contractor|architect|photograph|graphic|commercial (property|design)|installer|interior|studio|showroom|dealer|wholesale/i;
const isSample=li=>/sample/i.test(li.name||'')|| /sample/i.test(li.sku||'');
const num=x=>parseFloat(x?.shopMoney?.amount??'0');

let cursor=null,pages=0,scanned=0;
const MAXP=70; // ~7000 most-recent orders
const byCust=new Map(); // email -> {email, tags, tagged, zeroSampleOrders, freeShipSampleOrders, sampleOrders, names:Set}
while(pages<MAXP){
  const d=await query(`query($c:String){orders(first:100,after:$c,sortKey:CREATED_AT,reverse:true){pageInfo{hasNextPage endCursor} nodes{
    name createdAt totalShippingPriceSet{shopMoney{amount}}
    customer{id email tags}
    lineItems(first:30){nodes{name sku quantity discountedUnitPriceSet{shopMoney{amount}} originalUnitPriceSet{shopMoney{amount}}}}}}}`,{c:cursor});
  const os=d.orders; pages++;
  for(const o of os.nodes){
    scanned++;
    const samples=o.lineItems.nodes.filter(isSample);
    if(!samples.length) continue;
    const c=o.customer; const email=c?.email||'(guest)';
    const tags=c?.tags||[];
    const tagged=tags.some(t=>DESIGNER_TAG.test(t));
    const allSamplesZero=samples.every(li=>num(li.discountedUnitPriceSet)===0);
    const freeShip=num(o.totalShippingPriceSet)===0;
    if(!byCust.has(email)) byCust.set(email,{email,tags,tagged,zeroSampleOrders:0,freeShipSampleOrders:0,sampleOrders:0,names:new Set()});
    const r=byCust.get(email);
    r.sampleOrders++; r.names.add(o.name);
    if(allSamplesZero) r.zeroSampleOrders++;
    if(freeShip) r.freeShipSampleOrders++;
  }
  if(!os.pageInfo.hasNextPage){cursor=null;break;}
  cursor=os.pageInfo.endCursor;
}
const rows=[...byCust.values()];
const zeroSample=rows.filter(r=>r.zeroSampleOrders>0);
const atRiskZero=zeroSample.filter(r=>!r.tagged&&r.email!=='(guest)');           // comped samples, NOT tagged -> would be lost
const atRiskFreeShip=rows.filter(r=>r.freeShipSampleOrders>0&&!r.tagged&&r.email!=='(guest)'); // free-shipped samples, not tagged
console.log(`scanned ${scanned} recent orders${cursor?' (TRUNCATED at page cap)':''}`);
console.log(`customers with sample orders: ${rows.length}`);
console.log(`customers with $0-priced (comped) sample orders: ${zeroSample.length}  | of them UNtagged (AT-RISK): ${atRiskZero.length}`);
console.log(`customers with free-shipped sample orders & UNtagged: ${atRiskFreeShip.length}`);
console.log('\n=== AT-RISK: comped ($0) samples but NO designer tag (must not lose) ===');
for(const r of atRiskZero.sort((a,b)=>b.zeroSampleOrders-a.zeroSampleOrders).slice(0,60))
  console.log(`  ${r.email.padEnd(38)} zero-sample orders:${r.zeroSampleOrders} tags:[${r.tags.join(', ')}] e.g. ${[...r.names].slice(0,3).join(',')}`);
fs.writeFileSync(new URL('./verification/defacto-designers.json',import.meta.url),JSON.stringify({scanned,truncated:!!cursor,counts:{sampleCustomers:rows.length,zeroSampleCustomers:zeroSample.length,atRiskZero:atRiskZero.length,atRiskFreeShip:atRiskFreeShip.length},atRiskZero,atRiskFreeShip},null,2)+'\n');
console.log('\nfull data -> verification/defacto-designers.json');