← back to All Designerwallcoverings
scripts/adapters/thibaut.js
97 lines
// adapters/thibaut.js — Thibaut trade-portal live-stock adapter (PORTAL + VAULT CREDS).
//
// Thibaut (thibautdesign.com) is a Shopify trade storefront: the anonymous PDP exposes
// `availableForSale` / `stocked` / `backorder` markup but states outright that live stock status is
// "available to registered trade accounts" — so real inventory is behind the trade login. Creds are
// resolved by scripts/adapters/creds.js from the dw-price-stock vault (prefix THIBAUT), referenced
// IN PLACE — never copied here. They travel only in memory for this one login and are never logged.
//
// PUBLIC-SAFE: emits availability only. Thibaut's logged-in price is the trade/net price (no known
// retail transform in this context), so `price` is ALWAYS null — never emitted.
const { resolveCatalogUrl, releaseSession } = require('./resolve');
const creds = require('./creds');
const DEFAULT_LOGIN = 'https://www.thibautdesign.com/account/login';
async function run({ sku, pool, newSession, catalogTable, log }) {
let sessionOpened = false, browser = null, session = null, bb = null;
try {
// 1. Resolve the grid SKU → its Thibaut PDP (bridged via shopify_products → thibaut_catalog).
const r = await resolveCatalogUrl(pool, catalogTable || 'thibaut_catalog', sku);
if (r.error) return { available: false, reason: r.error };
const url = r.product_url;
// 2. Vault creds (prefix THIBAUT) — referenced in place; registry fallback. NEVER logged.
const c = await creds.resolve({ prefix: 'THIBAUT', pool, vendorLike: 'thibaut' });
if (!c || !c.pass) return { available: false, reason: 'thibaut trade credentials not available (vault)' };
const loginUrl = c.loginUrl || DEFAULT_LOGIN;
// 3. Metered Browserbase session + Thibaut trade login (#email / #password).
const sess = await newSession();
({ browser, session, bb } = sess); const page = sess.page;
sessionOpened = true;
log('bb session', session && session.id);
await page.goto(loginUrl, { waitUntil: 'domcontentloaded', timeout: 45000 });
await page.waitForTimeout(2500);
// dismiss any cookie/consent gate that could overlay the form
for (const sel of ['button:has-text("Accept")', 'button:has-text("ACCEPT")', '#onetrust-accept-btn-handler']) {
const el = await page.$(sel).catch(() => null);
if (el) { await el.click().catch(() => {}); await page.waitForTimeout(600); break; }
}
const emailEl = await page.$('#email, input[name=email], input[type=email]').catch(() => null);
if (emailEl) {
await page.fill('#email, input[name=email], input[type=email]', c.user || '').catch(() => {});
await page.fill('#password, input[name=password], input[type=password]', c.pass).catch(() => {});
await page.waitForTimeout(400);
await page.click('button[type=submit], form button:has-text("Sign"), form button:has-text("Log")')
.catch(async () => { await page.press('#password, input[type=password]', 'Enter').catch(() => {}); });
// wait for the login page to fall away (redirect off /login)
for (let i = 0; i < 18; i++) { await page.waitForTimeout(1000); if (!/\/(account\/)?login/i.test(page.url())) break; }
}
const loggedIn = !/\/(account\/)?login/i.test(page.url());
// 4. PDP → wait for the trade-gated inventory to hydrate, then read availability.
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
await page.waitForTimeout(7000);
const html = await page.content().catch(() => '');
// Shopify variant flag + Thibaut's own stock fields (present in the embedded product JSON).
const availForSale = /"availableForSale"\s*:\s*true/i.test(html) ? true
: /"availableForSale"\s*:\s*false/i.test(html) ? false : null;
const stockedM = html.match(/"stocked"\s*:\s*(\d+)/i);
const stocked = stockedM ? parseInt(stockedM[1], 10) : null;
const backorder = /"backorder"\s*:\s*(1|true)/i.test(html);
// product-scoped visible status text (avoid global nav "Check Stock"/"Discontinued List" chrome)
const availTxt = await page.$$eval(
'[itemprop=availability], main [class*=stock], main [class*=availab], main [class*=inventory], button:has-text("Add")',
(els) => els.map((e) => (e.getAttribute('content') || e.getAttribute('aria-label') || e.textContent || '').trim()).filter(Boolean).join(' | ')
).catch(() => '');
let in_stock = null;
if (availForSale === false || /out\s*of\s*stock|sold\s*out|discontinued|unavailable/i.test(availTxt)) in_stock = false;
else if (availForSale === true || stocked === 1 || /in\s*stock|in-?stock|add to (cart|order|sample)/i.test(availTxt)) in_stock = true;
else if (stocked === 0) in_stock = false;
const label = in_stock === true ? (backorder ? 'in stock (some backorder)' : 'in stock')
: in_stock === false ? 'out of stock'
: (availTxt ? availTxt.slice(0, 60) : 'unknown (parse pending verification)');
return {
available: true, session_opened: true, vendor: 'Thibaut',
in_stock, lead_time: backorder ? 'backorder possible' : null,
price: null, // Thibaut logged-in price is trade/net → never emitted
raw_stock_label: label,
as_of: new Date().toISOString(),
source: loggedIn ? 'thibaut-live' : 'thibaut-live (anon fallback)',
};
} catch (e) {
return { available: false, session_opened: sessionOpened, reason: 'live scrape error: ' + (e.message || e) };
} finally {
await releaseSession({ browser, bb, session });
}
}
module.exports = { run };