SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
3.8 KB · 49 lines typescript
Raw Blame History
1/**2 * Live fixture capture: joins one game's price guide with its product list and saves a few products3 * (Magic Black Lotus-like foil card, a Pokémon card with a disambiguator, a One Piece numbered card,4 * a sealed non-single). Usage: pnpm tsx connectors/api/cardmarket-priceguide/_capture.ts5 */6import { saveFixture } from '@rareindex/connectors/testing';7import meta from './meta.json' with { type: 'json' };8import { PriceRowSchema, ProductSchema, hasAnyPrice, type CardmarketPayload, type PriceRow } from './index.js';9import { JSON_HEADERS } from '../_g1-cards-eu-jp-lib/index.js';1011const BASE = 'https://downloads.s3.cardmarket.com/productCatalog';12const get = async (u: string) => (await (await fetch(u, { headers: JSON_HEADERS })).json()) as { createdAt?: string; priceGuides?: unknown[]; products?: unknown[] };1314async function game(gameId: number) {15  const guide = await get(`${BASE}/priceGuide/price_guide_${gameId}.json`);16  const prices = new Map<number, PriceRow>();17  for (const r of guide.priceGuides ?? []) {18    const p = PriceRowSchema.safeParse(r);19    if (p.success) prices.set(p.data.idProduct, p.data);20  }21  const singles = await get(`${BASE}/productList/products_singles_${gameId}.json`);22  const nonsingles = await get(`${BASE}/productList/products_nonsingles_${gameId}.json`);23  return { guide, prices, singles, nonsingles };24}2526const games = (meta.config.games as Record<string, { slug: string; name: string; franchise?: string | null; brand?: string | null; foilVariant?: string }>);2728async function capture(gameId: number, name: string, pick: (products: unknown[], prices: Map<number, PriceRow>) => unknown, single: boolean, note: string) {29  const g = await game(gameId);30  const rawProduct = pick((single ? g.singles.products : g.nonsingles.products) ?? [], g.prices);31  if (!rawProduct) throw new Error(`${name}: product not found`);32  const product = ProductSchema.parse(rawProduct);33  const cfg = games[String(gameId)]!;34  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 };35  saveFixture('cardmarket-priceguide', name, {36    raw: { url: `${BASE}/priceGuide/price_guide_${gameId}.json#idProduct=${product.idProduct}`, externalId: String(product.idProduct), kind: 'catalog_item', engine: 'api', fetchedAt: new Date(), payload },37    expect: { minCount: 2, kinds: ['catalog_item', 'price_observation'], requiredFields: ['attributes.identifiers.cardmarket_id', 'attributes.name'] },38    note: `Live capture ${new Date().toISOString().slice(0, 10)} — ${note} (price guide createdAt ${g.guide.createdAt})`,39  });40  console.log(name, product.idProduct, product.name, JSON.stringify(g.prices.get(product.idProduct)));41}4243const byName = (re: RegExp, needFoil = false) => (products: unknown[], prices: Map<number, PriceRow>) => products.find((p) => re.test(String((p as { name: string }).name)) && (!needFoil || hasAnyPrice(prices.get((p as { idProduct: number }).idProduct), true)));4445await capture(1, 'magic-foil-single', byName(/^Lightning Bolt$/, true), true, 'Magic single with foil and non-foil price rows');46await capture(6, 'pokemon-disambiguated', byName(/^Charizard \[/), true, 'Pokémon single whose name carries a [move | set] disambiguator; foil = Reverse Holo');47await capture(18, 'one-piece-numbered', byName(/^Roronoa Zoro \(OP01-001\)$/), true, 'One Piece single with the card number in parentheses');48await capture(1, 'magic-nonsingle-booster', byName(/^Alpha Booster$/), false, 'Magic sealed product from products_nonsingles (no foil, completeness sealed)');49