deckhearth/lib/pricing/foolio-client.js

121 lines
3.5 KiB
JavaScript
Raw Permalink Normal View History

const DEFAULT_FOILIO_FEED_URL = 'https://foilio.net/api/prices/feed?game=lorcana';
export function getFolioFeedUrl() {
return process.env.FOILIO_PRICES_FEED_URL ?? DEFAULT_FOILIO_FEED_URL;
}
export function normalizeFolioName(name) {
return String(name ?? '').trim().toLowerCase().replace(/\s+/g, ' ');
}
function parseAmount(value) {
if (value == null || value === '') return null;
const amount = Number(value);
return Number.isFinite(amount) ? amount : null;
}
/**
* Parse a Foilio feed row into current/market prices.
* Prefer USD fields; fall back to EUR with currency noted for snapshots.
*/
export function parseFolioPriceEntry(entry) {
const usdLow =
parseAmount(entry.usd_low)
?? parseAmount(entry.low_usd)
?? parseAmount(entry.price_usd)
?? parseAmount(entry.usd);
const usdMarket =
parseAmount(entry.usd_market)
?? parseAmount(entry.market_usd)
?? parseAmount(entry.market)
?? parseAmount(entry.price);
if (usdLow != null || usdMarket != null) {
return {
currentPrice: usdLow ?? usdMarket,
marketPrice: usdMarket ?? usdLow,
currency: 'USD',
};
}
const eurLow = parseAmount(entry.eur_low) ?? parseAmount(entry.low_eur);
const eurMarket = parseAmount(entry.eur_market) ?? parseAmount(entry.eur) ?? parseAmount(entry.price_eur);
if (eurLow != null || eurMarket != null) {
return {
currentPrice: eurLow ?? eurMarket,
marketPrice: eurMarket ?? eurLow,
currency: 'EUR',
};
}
return { currentPrice: null, marketPrice: null, currency: 'USD' };
}
function feedEntries(payload) {
if (Array.isArray(payload)) return payload;
if (Array.isArray(payload?.prices)) return payload.prices;
if (Array.isArray(payload?.cards)) return payload.cards;
if (Array.isArray(payload?.data)) return payload.data;
return [];
}
/**
* Build lookup indexes from a Foilio feed payload.
*/
export function buildFolioPriceIndex(payload) {
const byCardId = new Map();
const byNameSet = new Map();
for (const entry of feedEntries(payload)) {
const parsed = parseFolioPriceEntry(entry);
if (parsed.currentPrice == null && parsed.marketPrice == null) continue;
const cardId = entry.card_id ?? entry.cardId ?? entry.id ?? entry.unique_id ?? entry.Unique_ID;
if (cardId) {
byCardId.set(String(cardId), parsed);
}
const setCode = entry.set ?? entry.set_id ?? entry.setId ?? entry.Set_ID;
const name = entry.name ?? entry.Name;
if (setCode && name) {
const key = `${normalizeFolioName(name)}|${String(setCode).trim()}`;
if (!byNameSet.has(key)) {
byNameSet.set(key, parsed);
}
}
}
return { byCardId, byNameSet };
}
/**
* Match a Lorcana catalog row against the Foilio price index.
*/
export function matchFolioPrice(index, row) {
const byId = index.byCardId.get(String(row.scryfallId));
if (byId) return byId;
const key = `${normalizeFolioName(row.name)}|${String(row.setCode ?? '').trim()}`;
return index.byNameSet.get(key) ?? null;
}
/**
* Fetch the Foilio Lorcana price feed once per sync run.
*/
export async function fetchFolioPriceFeed() {
const response = await fetch(getFolioFeedUrl(), {
headers: { Accept: 'application/json', 'User-Agent': 'DeckHearth/1.0' },
});
if (!response.ok) {
throw new Error(`Foilio feed error: ${response.status}`);
}
return response.json();
}
export async function fetchFolioPriceIndex() {
const payload = await fetchFolioPriceFeed();
return buildFolioPriceIndex(payload);
}