← back to Vendor Discount Agent

create-vendor-pages.js

208 lines

#!/usr/bin/env node
/**
 * Create Shopify inquiry pages for vendor brands with < 5 products.
 * Skips vendors that already have a page (checks by handle).
 * Respects Shopify rate limits (2 req/s bucket, with 500ms delay between calls).
 */

const { Client } = require('pg');
const https = require('https');

const SHOPIFY_DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
const SHOPIFY_TOKEN = (process.env.SHOPIFY_ADMIN_TOKEN || '');
const API_VERSION = '2024-01';
const DB_URL = (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:15432/dw_unified');

// Vendors that already have pages (will also be checked dynamically)
const SKIP_HANDLES = new Set([
  'vendor-anthology',
  'vendor-carnegie',
  'brand-mckenzie',
]);

function buildBodyHtml(vendorName) {
  return `<div style="max-width:700px;margin:0 auto;padding:30px 20px;font-family:Georgia,serif;">
<h2 style="text-align:center;color:#2c2420;">${vendorName}</h2>
<p style="text-align:center;font-size:16px;line-height:1.7;color:#5a4a3a;">
We carry ${vendorName} products and can source any item from their current collections.
Contact us for pricing, samples, and availability.
</p>
<div style="text-align:center;margin:30px 0;">
<a href="/pages/contact-us" style="display:inline-block;background:#2c2420;color:white;padding:14px 36px;border-radius:4px;text-decoration:none;font-size:15px;">Contact Us for ${vendorName}</a>
</div>
</div>`;
}

function vendorCodeToHandle(vendorCode) {
  // Convert underscores to hyphens, lowercase
  return 'vendor-' + vendorCode.toLowerCase().replace(/_/g, '-');
}

function shopifyRequest(method, path, body = null) {
  return new Promise((resolve, reject) => {
    const options = {
      hostname: SHOPIFY_DOMAIN,
      path: `/admin/api/${API_VERSION}${path}`,
      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', () => {
        const remaining = res.headers['x-shopify-shop-api-call-limit'];
        try {
          const parsed = JSON.parse(data);
          resolve({ status: res.statusCode, data: parsed, rateLimit: remaining });
        } catch (e) {
          resolve({ status: res.statusCode, data: data, rateLimit: remaining });
        }
      });
    });

    req.on('error', reject);
    req.setTimeout(15000, () => {
      req.destroy();
      reject(new Error('Request timed out'));
    });

    if (body) {
      req.write(JSON.stringify(body));
    }
    req.end();
  });
}

function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function getExistingVendorPages() {
  // Fetch all pages and check for vendor- handles
  const existing = new Set();
  let sinceId = 0;
  let hasMore = true;

  while (hasMore) {
    const url = `/pages.json?limit=250&fields=id,handle&since_id=${sinceId}`;
    const res = await shopifyRequest('GET', url);
    if (res.status !== 200) {
      console.error('Failed to fetch pages:', res.status, res.data);
      break;
    }
    const pages = res.data.pages || [];
    for (const p of pages) {
      if (p.handle && (p.handle.startsWith('vendor-') || p.handle.startsWith('brand-'))) {
        existing.add(p.handle);
      }
      sinceId = Math.max(sinceId, p.id);
    }
    hasMore = pages.length === 250;
    if (hasMore) await sleep(500);
  }

  return existing;
}

async function main() {
  console.log('=== Vendor Inquiry Page Creator ===');
  console.log(`Started: ${new Date().toISOString()}\n`);

  // 1. Get existing Shopify pages
  console.log('Fetching existing Shopify pages...');
  const existingHandles = await getExistingVendorPages();
  console.log(`Found ${existingHandles.size} existing vendor/brand pages:`);
  for (const h of existingHandles) {
    console.log(`  - ${h}`);
    SKIP_HANDLES.add(h);
  }
  console.log('');

  // 2. Query DB for vendor list
  const client = new Client({ connectionString: DB_URL });
  await client.connect();

  const result = await client.query(
    `SELECT vendor_name, vendor_code
     FROM vendor_registry
     WHERE show_on_brands_page = true AND shopify_count < 5
     ORDER BY vendor_name`
  );
  await client.end();

  const vendors = result.rows;
  console.log(`Found ${vendors.length} vendors in DB with < 5 products.\n`);

  // 3. Create pages
  let created = 0;
  let skipped = 0;
  let errors = 0;

  for (let i = 0; i < vendors.length; i++) {
    const { vendor_name, vendor_code } = vendors[i];
    const handle = vendorCodeToHandle(vendor_code);

    // Skip if already exists
    if (SKIP_HANDLES.has(handle)) {
      console.log(`[${i + 1}/${vendors.length}] SKIP ${vendor_name} (${handle}) - already exists`);
      skipped++;
      continue;
    }

    // Create the page
    const pageData = {
      page: {
        title: vendor_name,
        handle: handle,
        body_html: buildBodyHtml(vendor_name),
        published: true,
      },
    };

    try {
      const res = await shopifyRequest('POST', '/pages.json', pageData);

      if (res.status === 201 || res.status === 200) {
        const pageId = res.data.page?.id || 'unknown';
        console.log(`[${i + 1}/${vendors.length}] CREATED ${vendor_name} (${handle}) -> ID: ${pageId} [${res.rateLimit}]`);
        created++;
        SKIP_HANDLES.add(handle);
      } else if (res.status === 422 && JSON.stringify(res.data).includes('handle')) {
        // Handle already taken
        console.log(`[${i + 1}/${vendors.length}] SKIP ${vendor_name} (${handle}) - handle conflict (422)`);
        skipped++;
      } else if (res.status === 429) {
        console.log(`[${i + 1}/${vendors.length}] RATE LIMITED - waiting 2s and retrying...`);
        await sleep(2000);
        i--; // Retry this vendor
        continue;
      } else {
        console.error(`[${i + 1}/${vendors.length}] ERROR ${vendor_name} (${handle}): ${res.status}`, JSON.stringify(res.data).substring(0, 200));
        errors++;
      }
    } catch (err) {
      console.error(`[${i + 1}/${vendors.length}] EXCEPTION ${vendor_name}: ${err.message}`);
      errors++;
    }

    // Rate limit: wait 500ms between requests (2 req/s bucket)
    await sleep(500);
  }

  console.log('\n=== SUMMARY ===');
  console.log(`Total vendors: ${vendors.length}`);
  console.log(`Created: ${created}`);
  console.log(`Skipped: ${skipped}`);
  console.log(`Errors:  ${errors}`);
  console.log(`Finished: ${new Date().toISOString()}`);
}

main().catch((err) => {
  console.error('Fatal error:', err);
  process.exit(1);
});