← back to Trending Dw
scripts/push-to-shopify.js
82 lines
#!/usr/bin/env node
// push-to-shopify.js — create real DW Shopify products from the staged DWPV queue.
//
// SAFE BY DEFAULT: refuses to run unless SHOPIFY_STORE + SHOPIFY_ADMIN_TOKEN are set,
// and creates every product as status:'draft' (NOT published) so nothing goes
// customer-facing without an explicit publish. Live publish stays a Steve-gated step.
//
// env SHOPIFY_STORE=xxxx.myshopify.com SHOPIFY_ADMIN_TOKEN=shpat_… \
// node scripts/push-to-shopify.js # push all 'staged'
// ... --dry # print payloads, no API calls
//
// Idempotent: only pushes queue items with status==='staged'; on success flips them
// to 'pushed' + records shopifyId + handle, so re-runs never double-create.
const fs = require('fs');
const path = require('path');
const https = require('https');
const QUEUE = path.join(__dirname, '..', 'data', 'shopify-queue.json');
const STORE = process.env.SHOPIFY_STORE; // xxxx.myshopify.com
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN; // shpat_…
const API_VER = process.env.SHOPIFY_API_VERSION || '2024-01';
const DRY = process.argv.includes('--dry');
if (!DRY && (!STORE || !TOKEN)){
console.error('REFUSING TO RUN: set SHOPIFY_STORE + SHOPIFY_ADMIN_TOKEN (route via /secrets). Use --dry to preview payloads.');
process.exit(2);
}
function priceFromTier(tier){ // "Commercial — $495" -> "495.00"
const m = String(tier || '').match(/\$\s*([\d,]+)/);
return m ? m[1].replace(/,/g, '') + '.00' : '149.00';
}
function buildProduct(p){
return { product: {
title: p.title,
body_html: `<p>${p.style} · ${p.color}. Original Designer Wallcoverings pattern (Pattern Vault). Trend signal: ${p.trendSignal || ''} (rank ${p.signalRank || ''}).</p>`,
vendor: p.vendor || 'Designer Wallcoverings',
product_type: 'Wallcovering',
status: 'draft', // never auto-publish
tags: [p.series, 'Pattern Vault', 'AI-Original', p.style, p.color].filter(Boolean).join(', '),
variants: [{ sku: p.sku, price: priceFromTier(p.priceTier), inventory_management: null }],
images: p.image ? [{ src: p.image }] : []
}};
}
function apiPost(bodyObj){
return new Promise((resolve, reject) => {
const body = JSON.stringify(bodyObj);
const req = https.request({
hostname: STORE, path: `/admin/api/${API_VER}/products.json`, method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN, 'Content-Length': Buffer.byteLength(body) }
}, res => {
let s = ''; res.on('data', d => s += d); res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) resolve(JSON.parse(s));
else reject(new Error(`HTTP ${res.statusCode}: ${s.slice(0, 300)}`));
});
});
req.on('error', reject); req.write(body); req.end();
});
}
async function main(){
const q = JSON.parse(fs.readFileSync(QUEUE, 'utf8'));
const todo = q.filter(p => p.status === 'staged');
console.log(`${todo.length} staged product(s) to push${DRY ? ' (DRY RUN)' : ` to ${STORE} as DRAFT`}`);
let ok = 0;
for (const p of todo){
const payload = buildProduct(p);
if (DRY){ console.log(` ${p.sku} ${p.title} $${payload.product.variants[0].price}`); continue; }
try {
const r = await apiPost(payload);
p.status = 'pushed'; p.shopifyId = r.product.id; p.handle = r.product.handle; p.pushedAt = new Date(Date.now()).toISOString();
ok++; console.log(` ✓ ${p.sku} -> product ${r.product.id} (draft)`);
} catch (e){ console.error(` ✗ ${p.sku}: ${e.message}`); }
}
if (!DRY){ fs.writeFileSync(QUEUE, JSON.stringify(q, null, 2)); console.log(`pushed ${ok}/${todo.length} as DRAFT — review + publish in Shopify admin.`); }
}
main().catch(e => { console.error(e); process.exit(1); });