← 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,
};