/** * ARES (Administrativní registr ekonomických subjektů) lookups — the public * MFČR REST API for Czech company data. No auth required. Used by the * customer/supplier modals to prefill company data from IČO or name. * * Docs: https://ares.gov.cz/swagger-ui (REST v3, ekonomicke-subjekty). * The browser cannot call ares.gov.cz directly (CORS + our CSP connect-src), * so these run server-side behind /api/admin/ares. */ const ARES_BASE = "https://ares.gov.cz/ekonomicke-subjekty-v-be/rest"; const FETCH_TIMEOUT_MS = 8000; /** Normalized subject shape consumed by the frontend prefill. */ export interface AresSubject { ico: string; dic: string | null; name: string; street: string; city: string; postal_code: string; country: string; } interface AresSidlo { nazevStatu?: string; nazevObce?: string; nazevCastiObce?: string; nazevUlice?: string; cisloDomovni?: number; cisloOrientacni?: number; cisloOrientacniPismeno?: string; psc?: number; textovaAdresa?: string; } interface AresEkonomickySubjekt { ico?: string; obchodniJmeno?: string; dic?: string; sidlo?: AresSidlo; } /** "51101" → "511 01" (Czech postal code display format). */ function formatPsc(psc: number | undefined): string { if (!psc) return ""; const s = String(psc); return s.length === 5 ? `${s.slice(0, 3)} ${s.slice(3)}` : s; } /** * Street line from the registered office: "Nádražní 485", Prague-style * orientation numbers become "Ulice 485/12a". Villages without street names * fall back to the municipality part name, then the municipality itself * (matching how ARES itself builds textovaAdresa). */ function buildStreet(sidlo: AresSidlo): string { const streetName = sidlo.nazevUlice || sidlo.nazevCastiObce || sidlo.nazevObce || ""; let num = sidlo.cisloDomovni != null ? String(sidlo.cisloDomovni) : ""; if (sidlo.cisloOrientacni != null) { num += `/${sidlo.cisloOrientacni}${sidlo.cisloOrientacniPismeno || ""}`; } return [streetName, num].filter(Boolean).join(" ").trim(); } function mapSubject(s: AresEkonomickySubjekt): AresSubject { const sidlo = s.sidlo || {}; return { ico: s.ico || "", dic: s.dic || null, name: s.obchodniJmeno || "", street: buildStreet(sidlo), city: sidlo.nazevObce || "", postal_code: formatPsc(sidlo.psc), country: sidlo.nazevStatu || "Česká republika", }; } export type AresError = "invalid_ico" | "not_found" | "ares_unavailable"; /** Lookup a single subject by its 8-digit IČO. */ export async function aresLookupByIco( ico: string, ): Promise { const normalized = ico.replace(/\s+/g, ""); if (!/^\d{8}$/.test(normalized)) return { error: "invalid_ico" }; try { const response = await fetch( `${ARES_BASE}/ekonomicke-subjekty/${normalized}`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }, ); if (response.status === 404) return { error: "not_found" }; if (!response.ok) { console.error(`[ares] lookup ${normalized} failed: ${response.status}`); return { error: "ares_unavailable" }; } const data = (await response.json()) as AresEkonomickySubjekt; if (!data.ico) return { error: "not_found" }; return mapSubject(data); } catch (err) { console.error("[ares] lookup failed:", err); return { error: "ares_unavailable" }; } } /** Search subjects by (part of) the business name; returns up to 10 matches. */ export async function aresSearchByName( name: string, ): Promise { const query = name.trim(); if (!query) return []; try { const response = await fetch(`${ARES_BASE}/ekonomicke-subjekty/vyhledat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ obchodniJmeno: query, pocet: 10, start: 0 }), signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), }); if (!response.ok) { console.error(`[ares] search "${query}" failed: ${response.status}`); return { error: "ares_unavailable" }; } const data = (await response.json()) as { ekonomickeSubjekty?: AresEkonomickySubjekt[]; }; return (data.ekonomickeSubjekty ?? []).map(mapSubject); } catch (err) { console.error("[ares] search failed:", err); return { error: "ares_unavailable" }; } }