← back to Fineartamerica Price Sync

lib/faa-cost.js

86 lines

// Fine Art America cost resolver.
//
// TWO TIERS (see recon 2026-07-16):
//   (1) HTTP fast path — FAA renders `#productPrice` server-side from the
//       ?product=<type> URL param. Free, no login, but only the DEFAULT config
//       for that product type (e.g. framed default = $74 at 7"x8").
//   (2) Browserbase configurator — FAA only prices the exact size/frame/mat/
//       finish combo through its live JS configurator, so per-variant cost needs
//       a real browser session (logged in as the seller so pricing + availability
//       reflect Steve's account). This is the metered path (~$0.10–0.30/session).
//
// Every fetched cost is cached to data/cost-cache.json keyed by the config string
// so re-runs only re-fetch changed configs (FAA raises base prices over time —
// that drift is exactly what the recurring sync + loss-guard canary catch).

const fs = require('fs');
const path = require('path');
const { configKey } = require('./parse-sku');

const BB = '/Users/macstudio3/.claude/skills/browserbase';
const SECRETS = '/Users/macstudio3/Projects/secrets-manager/.env';
const CACHE = path.join(__dirname, '..', 'data', 'cost-cache.json');

function secret(k) {
  const m = fs.readFileSync(SECRETS, 'utf8').match(new RegExp('^' + k + '=(.*)$', 'm'));
  return m ? m[1].trim().replace(/^["']|["']$/g, '') : null;
}
function loadCache() { try { return JSON.parse(fs.readFileSync(CACHE, 'utf8')); } catch { return {}; } }
function saveCache(c) { fs.writeFileSync(CACHE, JSON.stringify(c, null, 2)); }

// ---- Tier 1: HTTP product-type default price -------------------------------
async function httpTypeDefaultPrice(pageUrl, productType) {
  const u = new URL(pageUrl);
  u.searchParams.set('product', productType);
  const html = await (await fetch(u.toString(), {
    headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)' },
  })).text();
  const m = html.match(/productPrice'>([0-9]+\.[0-9]{2})/);
  return m ? parseFloat(m[1]) : null;
}

// ---- Tier 2: Browserbase configurator --------------------------------------
async function newSession() {
  const { chromium } = require(BB + '/node_modules/playwright-core');
  const Browserbase = require(BB + '/node_modules/@browserbasehq/sdk').default;
  const bb = new Browserbase({ apiKey: secret('BROWSERBASE_API_KEY') });
  const session = await bb.sessions.create({
    projectId: secret('BROWSERBASE_PROJECT_ID'),
    browserSettings: { solveCaptchas: true },
  });
  const browser = await chromium.connectOverCDP(session.connectUrl);
  const ctx = browser.contexts()[0];
  const page = ctx.pages()[0] || await ctx.newPage();
  page.setDefaultTimeout(45000);
  return { bb, session, browser, ctx, page };
}

async function loginFaa(page) {
  const email = secret('FAA_EMAIL'), pass = secret('FAA_PASSWORD');
  await page.goto('https://fineartamerica.com/login.html', { waitUntil: 'domcontentloaded' });
  await page.waitForTimeout(2500);
  const emailSel = await page.$('input[name=email], #email, input[type=email]');
  if (!emailSel) return { ok: !!(await page.$('a[href*=logout]')), note: 'no login form (maybe already in)' };
  await page.fill('input[name=email], #email, input[type=email]', email);
  await page.fill('input[name=password], #password, input[type=password]', pass);
  await page.waitForTimeout(400);
  await page.keyboard.press('Enter');
  await page.waitForTimeout(4000);
  return { ok: !/login\.html/.test(page.url()), url: page.url() };
}

// Read the currently-rendered configurator price.
async function readPrice(page) {
  return page.evaluate(() => {
    const el = document.querySelector('#productPrice, .productPrice, [class*=productPrice]');
    if (!el) return null;
    const n = parseFloat((el.textContent || '').replace(/[^0-9.]/g, ''));
    return isNaN(n) ? null : n;
  });
}

module.exports = {
  httpTypeDefaultPrice, newSession, loginFaa, readPrice,
  loadCache, saveCache, configKey, secret,
};