← back to All Designerwallcoverings

scripts/adapters/wallquest.js

94 lines

// adapters/wallquest.js — WallQuest trade-portal live-stock adapter (PORTAL+CREDS).
//
// Extracted UNCHANGED from the original monolithic live-scrape.js. Backs the display labels
// "Malibu Wallpaper" / "PS Removable Wallpaper" / "Malibu Walls" / "Malibu" (WallQuest's real
// name is never customer-facing). Documented mechanics from wallquest-scraper-manager: #Email +
// #Password, submit via Enter (button click hangs); price AJAX lazy-loads ~9s; price-value = OUR
// COST → transformed to our-retail (cost/0.65/0.85) and the raw cost is NEVER emitted.
const { releaseSession } = require('./resolve');

const WQ_LOGIN = 'https://www.wallquest.com/login';
const round2 = (n) => Math.round(n * 100) / 100;
// Standard DW retail formula: cost / PRICE_D1 / PRICE_D2 = our retail price.
const PRICE_D1 = 0.65;
const PRICE_D2 = 0.85;

async function run({ sku, pool, newSession, log }) {
  let sessionOpened = false, browser = null, session = null, bb = null;
  try {
    // 1. Resolve the SKU in the WallQuest catalog (the private-label backing line).
    const { rows } = await pool.query(
      `SELECT dw_sku, mfr_sku, product_url, price_retail, in_stock, discontinued, pattern_name, color_name
         FROM wallquest_catalog
        WHERE upper(dw_sku)=upper($1) OR upper(mfr_sku)=upper($1)
        LIMIT 1`, [sku]);
    if (!rows.length) return { available: false, reason: 'not a WallQuest/Malibu catalog SKU' };
    const row = rows[0];
    const url = row.product_url || '';
    const lastSeg = url.replace(/\/+$/, '').split('/').pop() || '';
    if (!url || /^WQ-/i.test(lastSeg) || /^\d+$/.test(lastSeg)) {
      return { available: false, reason: 'variant-gated SKU — no single live price on the portal' };
    }

    // 2. WallQuest trade creds (admin-only; private-label real name never surfaces publicly).
    const { rows: creds } = await pool.query(
      `SELECT trade_username, trade_password FROM vendor_registry
        WHERE vendor_name ILIKE 'wallquest' OR vendor_code ILIKE 'wallquest' LIMIT 1`);
    const user = (creds[0] && creds[0].trade_username) || 'info@designerwallcoverings.com';
    const pass = creds[0] && creds[0].trade_password;
    if (!pass) return { available: false, reason: 'wallquest trade password not configured' };

    // 3. Open a Browserbase session (billable) + login.
    const sess = await newSession();
    ({ browser, session, bb } = sess); const page = sess.page;
    sessionOpened = true;
    log('bb session', session && session.id);

    await page.goto(WQ_LOGIN, { waitUntil: 'domcontentloaded' });
    await page.waitForTimeout(2500);
    const emailEl = await page.$('#Email').catch(() => null);
    if (emailEl) {
      await page.fill('#Email', user).catch(() => {});
      await page.fill('#Password', pass).catch(() => {});
      await page.waitForTimeout(400);
      await page.press('#Password', 'Enter').catch(() => {});
      for (let i = 0; i < 18; i++) { await page.waitForTimeout(1000); if (!/\/login/i.test(page.url())) break; }
    }

    // 4. Product page → wait ~9s for the AJAX price, then read price-value + availability.
    await page.goto(url, { waitUntil: 'domcontentloaded' });
    await page.waitForTimeout(9500);

    const priceTxt = await page.$$eval('[class*=price-value]', (els) => els.map((e) => e.textContent).join(' ')).catch(() => '');
    const m = (priceTxt || '').match(/[\d,]+\.\d{2}/);
    const cost = m ? parseFloat(m[0].replace(/,/g, '')) : null;               // OUR COST — never emitted
    const price = cost && cost > 0 ? round2(cost / PRICE_D1 / PRICE_D2) : null; // PUBLIC our-retail

    const availTxt = await page.$$eval(
      '[class*=stock], [class*=availab], [itemprop=availability]',
      (els) => els.map((e) => (e.getAttribute('content') || e.textContent || '').trim()).filter(Boolean).join(' | ')
    ).catch(() => '');
    let in_stock = row.in_stock;
    if (/out\s*of\s*stock|sold\s*out|unavailable|InStock:\s*false/i.test(availTxt)) in_stock = false;
    else if (/in\s*stock|in-?stock|available|InStock/i.test(availTxt)) in_stock = true;
    if (row.discontinued) in_stock = false;

    const label = row.discontinued ? 'discontinued'
      : (availTxt ? availTxt.slice(0, 60) : (in_stock ? 'in stock' : (in_stock === false ? 'out of stock' : 'unknown')));

    return {
      available: true, session_opened: true, vendor: 'Malibu Wallpaper',
      in_stock: in_stock == null ? null : !!in_stock,
      lead_time: null, price,
      raw_stock_label: label,
      as_of: new Date().toISOString(), source: 'wallquest-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 };