New /api/admin/ares proxy (browser cannot reach ares.gov.cz - CORS+CSP): GET /ico/:ico and GET /search?q= map the MFCR REST API to the form shape (street incl. orientation numbers, "511 01" PSC format, DIC). Guarded by customers.create/edit + warehouse.manage; token errors map to Czech messages. Shared AresAdornment button sits in the Nazev and ICO fields of both modals: ICO mode fills directly, name mode searches with a result picker. Tested against live ARES (BOHA, ICO 22599851). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
135 lines
4.3 KiB
TypeScript
135 lines
4.3 KiB
TypeScript
/**
|
|
* 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<AresSubject | { error: AresError }> {
|
|
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<AresSubject[] | { error: AresError }> {
|
|
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" };
|
|
}
|
|
}
|