/** * Live fixture capture: joins one game's price guide with its product list and saves a few products * (Magic Black Lotus-like foil card, a Pokémon card with a disambiguator, a One Piece numbered card, * a sealed non-single). Usage: pnpm tsx connectors/api/cardmarket-priceguide/_capture.ts */ import { saveFixture } from '@rareindex/connectors/testing'; import meta from './meta.json' with { type: 'json' }; import { PriceRowSchema, ProductSchema, hasAnyPrice, type CardmarketPayload, type PriceRow } from './index.js'; import { JSON_HEADERS } from '../_g1-cards-eu-jp-lib/index.js'; const BASE = 'https://downloads.s3.cardmarket.com/productCatalog'; const get = async (u: string) => (await (await fetch(u, { headers: JSON_HEADERS })).json()) as { createdAt?: string; priceGuides?: unknown[]; products?: unknown[] }; async function game(gameId: number) { const guide = await get(`${BASE}/priceGuide/price_guide_${gameId}.json`); const prices = new Map(); for (const r of guide.priceGuides ?? []) { const p = PriceRowSchema.safeParse(r); if (p.success) prices.set(p.data.idProduct, p.data); } const singles = await get(`${BASE}/productList/products_singles_${gameId}.json`); const nonsingles = await get(`${BASE}/productList/products_nonsingles_${gameId}.json`); return { guide, prices, singles, nonsingles }; } const games = (meta.config.games as Record); async function capture(gameId: number, name: string, pick: (products: unknown[], prices: Map) => unknown, single: boolean, note: string) { const g = await game(gameId); const rawProduct = pick((single ? g.singles.products : g.nonsingles.products) ?? [], g.prices); if (!rawProduct) throw new Error(`${name}: product not found`); const product = ProductSchema.parse(rawProduct); const cfg = games[String(gameId)]!; const payload: CardmarketPayload = { gameId, game: { slug: cfg.slug, name: cfg.name, franchise: cfg.franchise ?? null, brand: cfg.brand ?? null, foilVariant: cfg.foilVariant ?? 'Foil' }, product, prices: g.prices.get(product.idProduct) ?? null, single, priceGuideCreatedAt: g.guide.createdAt ?? null, productListCreatedAt: (single ? g.singles : g.nonsingles).createdAt ?? null }; saveFixture('cardmarket-priceguide', name, { raw: { url: `${BASE}/priceGuide/price_guide_${gameId}.json#idProduct=${product.idProduct}`, externalId: String(product.idProduct), kind: 'catalog_item', engine: 'api', fetchedAt: new Date(), payload }, expect: { minCount: 2, kinds: ['catalog_item', 'price_observation'], requiredFields: ['attributes.identifiers.cardmarket_id', 'attributes.name'] }, note: `Live capture ${new Date().toISOString().slice(0, 10)} — ${note} (price guide createdAt ${g.guide.createdAt})`, }); console.log(name, product.idProduct, product.name, JSON.stringify(g.prices.get(product.idProduct))); } const byName = (re: RegExp, needFoil = false) => (products: unknown[], prices: Map) => products.find((p) => re.test(String((p as { name: string }).name)) && (!needFoil || hasAnyPrice(prices.get((p as { idProduct: number }).idProduct), true))); await capture(1, 'magic-foil-single', byName(/^Lightning Bolt$/, true), true, 'Magic single with foil and non-foil price rows'); await capture(6, 'pokemon-disambiguated', byName(/^Charizard \[/), true, 'Pokémon single whose name carries a [move | set] disambiguator; foil = Reverse Holo'); await capture(18, 'one-piece-numbered', byName(/^Roronoa Zoro \(OP01-001\)$/), true, 'One Piece single with the card number in parentheses'); await capture(1, 'magic-nonsingle-booster', byName(/^Alpha Booster$/), false, 'Magic sealed product from products_nonsingles (no foil, completeness sealed)');