import * as cheerio from "cheerio"; import type { NormalizedQuery, Offer, StoreAdapter } from "../core/types.js"; import { getBrowserHub } from "../core/browserHub.js"; // IC24.lv — Inter Cars' Latvian B2C storefront (the working sibling of motointegrator.lv, // which is currently down). NOT Cloudflare-walled: the catalog and part-number search are // open. The catch is the PRICE — Inter Cars renders prices into the search HTML only for a // warm anonymous session (the token its homepage/`/auth.php` establishes). A cold direct GET // returns the same page with EMPTY price boxes. So we fetch through the resident browser // (BrowserHub.fetchOriginHtml warms the homepage first, then same-origin-fetches the search // URL) — same mechanism as trodo, minus any challenge. It rides onDemand purely for the warm // session; with no CF wall it never asks for a solve. // // Search URL: `/detalas/search=` (e.g. W71252 → "VzcxMjUy"). Results are // the searched part plus cross-brand analogues (like handler/trodo). // // PRICE PARITY: Inter Cars shows a net (ex-VAT, B2B-style) price too, but every other store // here compares on the VAT-inclusive consumer price. We therefore read `.price_gross_2` // (gross, VAT-incl — 6.76) NOT `.price_net_2` (hidden net — 5.59), so ic24 isn't made to // look falsely cheapest. Fixture: fixtures/ic24/W71252.html (2026-07-13). const HOME = "https://www.ic24.lv"; const SEARCH_URL = (canonical: string): string => `${HOME}/detalas/search=${Buffer.from(canonical).toString("base64")}`; const SELECTORS = { card: "[data-product-impresion]", // one offer per element; id="wynik_N" brand: 'input[id^="manufacture_"]', // value="MANN-FILTER" partNumber: 'input[id^="index_"]', // value="W 712/52" title: 'input[id^="description_"]', // value="Eļļas filtrs MANN-FILTER W 712/52" link: 'a[href^="/produkti/"]', image: 'img[src*="webcdn.intercars"], img[src*="/thumbnails/"]', priceGross: ".price_gross_2", // VAT-inclusive selling price (see PRICE PARITY note) stock: 'span[datatest-id="tap-item-product-stock"]', // in-stock unit count, e.g. "9" }; export const ic24: StoreAdapter = { id: "ic24", displayName: "IC24.lv", storeHomepage: HOME, enabled: true, fetchMode: "onDemand", // routes through the resident browser for its warm session (no CF) timeoutMs: 45000, // homepage warm-up + same-origin search fetch of a ~390KB page async fetchHtml(q: NormalizedQuery): Promise { return getBrowserHub().fetchOriginHtml(this.id, HOME, SEARCH_URL(q.canonical)); }, parse(html: string): Offer[] { const $ = cheerio.load(html); const offers: Offer[] = []; $(SELECTORS.card).each((_, el) => { const card = $(el); const partNumber = (card.find(SELECTORS.partNumber).attr("value") ?? "").trim(); const href = card.find(SELECTORS.link).attr("href") ?? ""; // Gross price text is "6.76" (dot decimal); strip anything else defensively. const priceEur = Number( card.find(SELECTORS.priceGross).first().text().replace(",", ".").replace(/[^\d.]/g, ""), ); if (!partNumber || !href || !Number.isFinite(priceEur) || priceEur <= 0) return; const brand = (card.find(SELECTORS.brand).attr("value") ?? "").trim() || null; const title = (card.find(SELECTORS.title).attr("value") ?? "").trim() || partNumber; const imageUrl = card.find(SELECTORS.image).first().attr("src") ?? null; const qty = parseInt(card.find(SELECTORS.stock).first().text().trim(), 10); const availability = Number.isFinite(qty) && qty > 0 ? `Pieejams (${qty} gab.)` : "Nav pieejams"; offers.push({ store: ic24.id, brand, partNumber, title, priceEur, availability, deliveryNote: null, url: new URL(href, HOME).toString(), imageUrl, attributes: [], }); }); return offers; }, };