← 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);
});