← back to All Designerwallcoverings

scripts/adapters/romo.js

65 lines

// adapters/romo.js — Romo trade-portal live-stock adapter (PORTAL+CREDS).
//
// Reuses the EXISTING Romo login mechanics from lib/romo-lib.js (the romo-scraper-manager
// login flow — trade.theromogroup.com, #fmregister-email/#fmregister-password, SSO state
// handling) rather than reinventing the portal. Creds are read from the DB inside that helper;
// they never travel on a command line or get echoed.
//
// PUBLIC-SAFE: emits availability only. Romo's portal price is the NET/TRADE (cost) price with no
// known retail transform in this context, so `price` is always null — never emitted.
const { resolveCatalogUrl, NAV_RE, releaseSession } = require('./resolve');

async function run({ sku, pool, newSession, romoLogin, catalogTable, log }) {
  let sessionOpened = false, browser = null, session = null, bb = null;
  try {
    // 1. Resolve the grid SKU → its Romo portal product page (bridged via shopify_products).
    const r = await resolveCatalogUrl(pool, catalogTable || 'romo_catalog', sku);
    if (r.error) return { available: false, reason: r.error };
    const url = r.product_url;

    // 2. Metered Browserbase session + Romo portal login (documented mechanics from romo-lib).
    const sess = await newSession();
    ({ browser, session, bb } = sess); const page = sess.page;
    sessionOpened = true;
    log('bb session', session && session.id);
    if (typeof romoLogin === 'function') { try { await romoLogin(page); } catch (e) { log('romo login note:', e.message); } }

    // 3. Product page → read availability (broad selectors + schema.org JSON-LD + add-to-cart state).
    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
    await page.waitForTimeout(6000);

    // schema.org availability is authoritative — prefer it over noisy DOM text.
    const ld = await page.$$eval('script[type="application/ld+json"]', (els) => els.map((e) => e.textContent).join(' ')).catch(() => '');
    // product-scoped availability text (avoid the global nav, which carries "Discontinued List" /
    // "Sale List" / "Check Stock" menu links that would falsely trip out-of-stock).
    let availTxt = await page.$$eval(
      '[itemprop=availability], .product [class*=stock], .product [class*=availab], .product-form__inventory, button[name=add], [class*=add-to]',
      (els) => els.map((e) => (e.getAttribute('content') || e.getAttribute('aria-label') || e.textContent || '').trim()).filter(Boolean).join(' | ')
    ).catch(() => '');
    // strip obvious nav-menu chrome so it can't masquerade as product stock state
    availTxt = String(availTxt).replace(NAV_RE, '').trim();

    let in_stock = null;
    if (/OutOfStock|SoldOut/i.test(ld)) in_stock = false;
    else if (/InStock/i.test(ld)) in_stock = true;
    else if (/out\s*of\s*stock|sold\s*out|unavailable/i.test(availTxt)) in_stock = false;
    else if (/in\s*stock|in-?stock|add to (cart|basket|bag)/i.test(availTxt)) in_stock = true;
    // else: unknown (null) — never guess from nav chrome

    const label = in_stock === true ? 'in stock' : in_stock === false ? 'out of stock'
      : (availTxt ? availTxt.slice(0, 60) : 'unknown (parse pending verification)');

    return {
      available: true, session_opened: true, vendor: 'Romo',
      in_stock, lead_time: null, price: null, // Romo portal price is net/cost → never emitted
      raw_stock_label: label, as_of: new Date().toISOString(), source: 'romo-live',
    };
  } catch (e) {
    return { available: false, session_opened: sessionOpened, reason: 'live scrape error: ' + (e.message || e) };
  } finally {
    await releaseSession({ browser, bb, session });
  }
}

module.exports = { run };