← back to Yolo Agent
create-missing-collections.js
229 lines
#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
function loadLocalEnv(file) {
if (!fs.existsSync(file)) return;
for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=(.*)\s*$/);
if (!match || process.env[match[1]] !== undefined) continue;
let value = match[2].trim();
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
process.env[match[1]] = value;
}
}
loadLocalEnv(path.join(__dirname, '.env'));
/**
* Create Smart Collections for vendors missing dedicated collection pages
* Reads from collection-audit-report.json, creates Shopify smart collections
*/
const https = require('https');
const SHOPIFY_STORE = 'designer-laboratory-sandbox.myshopify.com';
const SHOPIFY_TOKEN = process.env.SHOPIFY_ORDERS_TOKEN;
const API_VERSION = '2024-01';
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;
if (!SHOPIFY_TOKEN || !SLACK_WEBHOOK) {
console.error('Missing SHOPIFY_ORDERS_TOKEN or SLACK_WEBHOOK_URL.');
process.exit(1);
}
// Internal/private label vendors — never create public collections
const SKIP_VENDORS = new Set([
'Steve Abrams Studios',
'Steven Abrams Photography',
'Sancar',
'DW Home',
'Designer Laboratory',
'DW Internal',
'DW Exclusive Wallpaper',
'Zeuxis Parrhasius at DW',
]);
// Vendors whose name already contains a product category — no suffix needed
const CATEGORY_IN_NAME = {
'Schumacher Trim': true, // already says "Trim"
'Luxury Murals': true, // already says "Murals"
'Natural Paint': true, // already says "Paint"
'Apartment Wallpaper': true, // vendor name (keep as-is)
'Wallpaper NYC': true, // vendor name (keep as-is)
'Atomic 50 Ceilings': true, // already says "Ceilings"
};
// Special suffix overrides
const SUFFIX_OVERRIDES = {
'Fabricut': 'Fabrics',
'Backdrop': '', // paint brand, no suffix
'SUGARBOO': '', // art/decor, no suffix
};
const MIN_PRODUCTS = 3;
function getCollectionTitle(vendor) {
if (CATEGORY_IN_NAME[vendor]) return vendor;
const suffix = SUFFIX_OVERRIDES[vendor];
if (suffix !== undefined) return suffix ? `${vendor} ${suffix}` : vendor;
return `${vendor} Wallcoverings`;
}
function shopifyRequest(method, endpoint, body) {
return new Promise((resolve, reject) => {
const options = {
hostname: SHOPIFY_STORE,
path: `/admin/api/${API_VERSION}/${endpoint}`,
method,
headers: {
'X-Shopify-Access-Token': SHOPIFY_TOKEN,
'Content-Type': 'application/json',
},
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
try {
const parsed = JSON.parse(data);
if (res.statusCode >= 400) {
reject({ status: res.statusCode, body: parsed });
} else {
resolve({ status: res.statusCode, body: parsed });
}
} catch (e) {
reject({ status: res.statusCode, raw: data });
}
});
});
req.on('error', reject);
if (body) req.write(JSON.stringify(body));
req.end();
});
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
function sendSlack(text) {
return new Promise((resolve) => {
const url = new URL(SLACK_WEBHOOK);
const req = https.request(
{ hostname: url.hostname, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json' } },
(res) => { let d = ''; res.on('data', (c) => (d += c)); res.on('end', () => resolve(d)); }
);
req.on('error', () => resolve('slack error'));
req.write(JSON.stringify({ text }));
req.end();
});
}
async function main() {
const reportPath = path.join(__dirname, 'logs', 'collection-audit-report.json');
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
const missing = report.missing || [];
console.log(`\n📋 Total missing vendors: ${missing.length}`);
const created = [];
const skipped = [];
const failed = [];
for (const v of missing) {
const vendor = v.vendor;
const products = v.activeProducts;
// Skip internal/private label
if (SKIP_VENDORS.has(vendor)) {
skipped.push({ vendor, reason: 'internal/private label', products });
console.log(`⏭️ SKIP (internal): ${vendor} (${products} products)`);
continue;
}
// Skip < 3 active products
if (products < MIN_PRODUCTS) {
skipped.push({ vendor, reason: `only ${products} active products (min: ${MIN_PRODUCTS})`, products });
console.log(`⏭️ SKIP (too few): ${vendor} (${products} products)`);
continue;
}
const title = getCollectionTitle(vendor);
try {
const res = await shopifyRequest('POST', 'smart_collections.json', {
smart_collection: {
title,
rules: [{ column: 'vendor', relation: 'equals', condition: vendor }],
published: true,
sort_order: 'best-selling',
},
});
const coll = res.body.smart_collection;
created.push({
vendor,
collectionId: coll.id,
handle: coll.handle,
title: coll.title,
products,
});
console.log(`✅ CREATED: "${title}" → handle: ${coll.handle} (${products} products)`);
} catch (err) {
const errMsg = err.body?.errors || err.message || JSON.stringify(err);
failed.push({ vendor, title, error: errMsg, products });
console.log(`❌ FAILED: "${title}" — ${JSON.stringify(errMsg)}`);
}
// Rate limit: 600ms between requests
await sleep(600);
}
// Save results
const results = {
timestamp: new Date().toISOString(),
summary: {
created: created.length,
skipped: skipped.length,
failed: failed.length,
},
created,
skipped,
failed,
};
const outPath = path.join(__dirname, 'logs', 'collections-created-report.json');
fs.writeFileSync(outPath, JSON.stringify(results, null, 2));
console.log(`\n📄 Report saved to: ${outPath}`);
// Summary
console.log(`\n=== SUMMARY ===`);
console.log(`✅ Created: ${created.length}`);
console.log(`⏭️ Skipped: ${skipped.length}`);
console.log(`❌ Failed: ${failed.length}`);
// Slack notification
const slackMsg = [
`✅ *Smart Collections Created*`,
`• Created: ${created.length} new collections`,
`• Skipped: ${skipped.length} (internal/private/too few products)`,
`• Failed: ${failed.length}`,
``,
`*New collections:*`,
...created.map((c) => ` - ${c.title} (${c.products} products) → /collections/${c.handle}`),
``,
`Report: ${outPath}`,
].join('\n');
await sendSlack(slackMsg);
console.log('📨 Slack notification sent');
}
main().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});