← back to Shopify Order Bell
poller.js
46 lines
#!/usr/bin/env node
// Headless poller — same logic as the desktop app, but CLI-only.
// Useful for overnight / server use: `node poller.js` loops forever and `afplay`s on new orders.
const fs = require('fs'), path = require('path'), https = require('https');
const { exec } = require('child_process');
function loadEnv(){
for (const p of [path.join(__dirname,'.env'), path.join(process.env.HOME,'Projects/secrets-manager/.env')]){
try{ const t=fs.readFileSync(p,'utf8'); for(const l of t.split('\n')){ const m=l.match(/^([A-Z0-9_]+)=(.*)$/); if(m && process.env[m[1]]===undefined) process.env[m[1]]=m[2].replace(/^["']|["']$/g,''); }}catch{}
}
}
loadEnv();
const STORE = process.env.SHOPIFY_STORE || process.env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = process.env.SHOPIFY_ORDERS_TOKEN || process.env.SHOPIFY_FULL_ACCESS_TOKEN || '';
const POLL_MS = parseInt(process.env.POLL_SECONDS||'15',10)*1000;
const STATE = path.join(__dirname,'.last-order.json');
let lastId=null; let seeded=false;
try{ if(fs.existsSync(STATE)) lastId=JSON.parse(fs.readFileSync(STATE,'utf8')).lastId; seeded=lastId!=null; }catch{}
function fetchOrders(){
return new Promise((res,rej)=>{
if(!TOKEN) return rej(new Error('Missing SHOPIFY_ORDERS_TOKEN'));
const req=https.request({hostname:STORE,path:'/admin/api/2024-04/orders.json?limit=5&status=any&order=created_at%20desc',headers:{'X-Shopify-Access-Token':TOKEN}},r=>{
let d=''; r.on('data',c=>d+=c); r.on('end',()=> r.statusCode===200 ? res(JSON.parse(d)) : rej(new Error(`Shopify ${r.statusCode}: ${d.slice(0,300)}`)));
}); req.on('error',rej); req.setTimeout(10000,()=>req.destroy(new Error('timeout'))); req.end();
});
}
function ring(order){
const msg=`🔔 NEW ORDER #${order.name} ${order.total_price} ${order.currency} — ${order.email||''}`;
console.log(new Date().toISOString(), msg);
// macOS bell: Glass + spoken cue
exec('afplay /System/Library/Sounds/Glass.aiff 2>/dev/null; afplay /System/Library/Sounds/Ping.aiff 2>/dev/null; echo "\\a"');
try{ exec(`osascript -e 'display notification "${msg.replace(/"/g,'\\"').slice(0,120)}" with title "Shopify Order"'`);}catch{}
}
async function poll(){
try{
const {orders}=(await fetchOrders()); if(!orders.length){ console.log(new Date().toISOString(),'No orders'); return; }
const newest=orders[0];
if(!seeded){ lastId=newest.id; seeded=true; fs.writeFileSync(STATE,JSON.stringify({lastId,at:new Date().toISOString()})); console.log(new Date().toISOString(),`Seeded at #${newest.name} — watching for new orders`); return; }
const news=[]; for(const o of orders){ if(String(o.id)===String(lastId)) break; news.push(o); }
if(news.length){ news.reverse().forEach(ring); lastId=newest.id; fs.writeFileSync(STATE,JSON.stringify({lastId,at:new Date().toISOString()})); }
else console.log(new Date().toISOString(),`Watching — last #${orders[0].name}`);
}catch(e){ console.error(new Date().toISOString(),'poll error',e.message); }
}
console.log(`Shopify Order Bell — poller — store ${STORE} every ${POLL_MS/1000}s`);
if(!TOKEN) { console.error('Set SHOPIFY_ORDERS_TOKEN in .env or secrets-manager'); process.exit(1); }
poll(); setInterval(poll,POLL_MS);