← back to Fix Live Board

probes/shopify-recent.mjs

86 lines

#!/usr/bin/env node
// "Last N days" probe — every product created in the last N days (all statuses), across all
// vendors, checked against Steve's standing go-live rule: sample variant, sellable variant,
// complete price, description, >=2 tags, featured image. READ-ONLY live DW Shopify.
// Env: DAYS (default 5). Token: SHOPIFY_ADMIN_TOKEN -> secrets-manager/.env -> flock-fix-viewer/.token
import https from 'https';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { fileURLToPath } from 'url';

const ROOT = path.dirname(fileURLToPath(import.meta.url));
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const DAYS = parseInt(process.env.DAYS || '5', 10);
// Showroom lines are SAMPLE-ONLY by design — a sample with no sellable roll/price is CORRECT, not
// broken. Editable list; seeded with Phillip Jeffries.
let SHOWROOM = new Set(['phillip jeffries']);
try { SHOWROOM = new Set(JSON.parse(fs.readFileSync(path.join(ROOT, '..', 'config', 'showroom-vendors.json'), 'utf8')).map(s => String(s).trim().toLowerCase())); } catch (e) {}
function token() {
  if (process.env.SHOPIFY_ADMIN_TOKEN) return process.env.SHOPIFY_ADMIN_TOKEN.trim();
  try { const env = fs.readFileSync(path.join(os.homedir(), 'Projects/secrets-manager/.env'), 'utf8'); const m = env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m); if (m) return m[1].trim(); } catch (e) {}
  return fs.readFileSync(path.join(os.homedir(), 'Projects/flock-fix-viewer/.token'), 'utf8').trim();
}
const TOKEN = token();
const since = new Date(Date.now() - DAYS * 864e5).toISOString().slice(0, 10);
const QUERY = 'created_at:>=' + since;
const PRODUCT_Q = `query($c:String){
  products(first:100, query:${JSON.stringify(QUERY)}, sortKey:CREATED_AT, reverse:true, after:$c){
    pageInfo{hasNextPage endCursor}
    edges{node{ handle title status createdAt vendor descriptionHtml featuredImage{url} tags
      variants(first:20){edges{node{sku title price}}}
    }}
  }
}`;
function gql(query, variables) {
  return new Promise((resolve, reject) => {
    const body = JSON.stringify({ query, variables: variables || {} });
    const req = https.request({ host: SHOP, path: '/admin/api/2024-10/graphql.json', method: 'POST',
      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } },
      res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(e); } }); });
    req.on('error', reject); req.write(body); req.end();
  });
}
(async () => {
  let cur = null, all = [];
  do {
    const r = await gql(PRODUCT_Q, { c: cur });
    const p = r && r.data && r.data.products; if (!p) { if (r && r.errors) process.stderr.write(JSON.stringify(r.errors)); break; }
    all = all.concat(p.edges);
    cur = p.pageInfo.hasNextPage ? p.pageInfo.endCursor : null;
  } while (cur && all.length < 3000);
  const rows = all.map(e => {
    const n = e.node;
    const vs = (n.variants.edges || []).map(v => v.node);
    const isSample = v => /sample/i.test(v.title || '') || /-sample$/i.test(v.sku || '');
    const sampleV = vs.find(isSample);
    const sellV = vs.find(v => v.sku && !isSample(v)); // sellable = a real (non-sample) variant
    const sellPrice = sellV ? (parseFloat(sellV.price) || 0) : 0;
    const tags = n.tags || [];
    const showroom = SHOWROOM.has((n.vendor || '').trim().toLowerCase());
    const f = {
      sample: !!sampleV,
      // showroom lines are sample-only by design → sellable + price are waived (shown satisfied)
      sellable: showroom ? true : !!sellV,
      price: showroom ? true : sellPrice > 0,
      desc: !!(n.descriptionHtml && n.descriptionHtml.replace(/<[^>]*>/g, '').trim().length > 0),
      tags: tags.length >= 2,
      image: !!(n.featuredImage && n.featuredImage.url)
    };
    return {
      id: (sellV && sellV.sku) || (sampleV && sampleV.sku) || n.handle,
      sku: (sellV && sellV.sku) || (sampleV && sampleV.sku) || '—',
      handle: n.handle,
      title: '[' + (n.vendor || '?') + '] ' + n.title,
      status: n.status, price: sellPrice,
      img: (n.featuredImage && n.featuredImage.url) || null,
      created: n.createdAt, note: showroom ? 'showroom' : undefined, fields: f,
      // showroom lines are fixed when the sample + quality fields are present (no sellable/price needed)
      fixed: showroom
        ? (f.sample && f.desc && f.tags && f.image)
        : (f.sample && f.sellable && f.price && f.desc && f.tags && f.image)
    };
  });
  process.stdout.write(JSON.stringify(rows));
})().catch(e => { process.stderr.write(String(e)); process.exit(1); });