feat(ares): ARES company lookup with prefill in customer/supplier modals
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>
This commit is contained in:
116
src/__tests__/ares.test.ts
Normal file
116
src/__tests__/ares.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { aresLookupByIco, aresSearchByName } from "../services/ares.service";
|
||||
|
||||
// ARES is a true external — mock global fetch (the only allowed mock class).
|
||||
const SUBJECT = {
|
||||
ico: "22599851",
|
||||
obchodniJmeno: "BOHA Automation s.r.o.",
|
||||
dic: "CZ22599851",
|
||||
sidlo: {
|
||||
nazevStatu: "Česká republika",
|
||||
nazevObce: "Turnov",
|
||||
nazevCastiObce: "Turnov",
|
||||
nazevUlice: "Nádražní",
|
||||
cisloDomovni: 485,
|
||||
psc: 51101,
|
||||
textovaAdresa: "Nádražní 485, 51101 Turnov",
|
||||
},
|
||||
};
|
||||
|
||||
function mockFetchOnce(status: number, body?: unknown) {
|
||||
return vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => body,
|
||||
} as Response);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("aresLookupByIco", () => {
|
||||
it("maps a subject to the normalized prefill shape", async () => {
|
||||
mockFetchOnce(200, SUBJECT);
|
||||
const res = await aresLookupByIco("22599851");
|
||||
expect(res).toEqual({
|
||||
ico: "22599851",
|
||||
dic: "CZ22599851",
|
||||
name: "BOHA Automation s.r.o.",
|
||||
street: "Nádražní 485",
|
||||
city: "Turnov",
|
||||
postal_code: "511 01",
|
||||
country: "Česká republika",
|
||||
});
|
||||
});
|
||||
|
||||
it("builds Prague-style orientation numbers as 'Ulice 485/12a'", async () => {
|
||||
mockFetchOnce(200, {
|
||||
...SUBJECT,
|
||||
sidlo: {
|
||||
...SUBJECT.sidlo,
|
||||
cisloOrientacni: 12,
|
||||
cisloOrientacniPismeno: "a",
|
||||
},
|
||||
});
|
||||
const res = await aresLookupByIco("22599851");
|
||||
expect("street" in res && res.street).toBe("Nádražní 485/12a");
|
||||
});
|
||||
|
||||
it("rejects a non-8-digit IČO without calling ARES", async () => {
|
||||
const spy = vi.spyOn(globalThis, "fetch");
|
||||
expect(await aresLookupByIco("123")).toEqual({ error: "invalid_ico" });
|
||||
expect(await aresLookupByIco("abcdefgh")).toEqual({
|
||||
error: "invalid_ico",
|
||||
});
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts an IČO with stray whitespace", async () => {
|
||||
const spy = mockFetchOnce(200, SUBJECT);
|
||||
const res = await aresLookupByIco(" 225 99851 ");
|
||||
expect("ico" in res && res.ico).toBe("22599851");
|
||||
expect(String(spy.mock.calls[0][0])).toContain("/22599851");
|
||||
});
|
||||
|
||||
it("maps 404 to not_found and network failure to ares_unavailable", async () => {
|
||||
mockFetchOnce(404);
|
||||
expect(await aresLookupByIco("12345678")).toEqual({ error: "not_found" });
|
||||
|
||||
vi.spyOn(globalThis, "fetch").mockRejectedValueOnce(new Error("ETIMEDOUT"));
|
||||
expect(await aresLookupByIco("12345678")).toEqual({
|
||||
error: "ares_unavailable",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("aresSearchByName", () => {
|
||||
it("maps the result list and sends the name in the POST body", async () => {
|
||||
const spy = mockFetchOnce(200, { ekonomickeSubjekty: [SUBJECT] });
|
||||
const res = await aresSearchByName("BOHA Automation");
|
||||
expect(Array.isArray(res)).toBe(true);
|
||||
if (Array.isArray(res)) {
|
||||
expect(res).toHaveLength(1);
|
||||
expect(res[0].name).toBe("BOHA Automation s.r.o.");
|
||||
expect(res[0].postal_code).toBe("511 01");
|
||||
}
|
||||
const init = spy.mock.calls[0][1] as RequestInit;
|
||||
expect(JSON.parse(String(init.body))).toMatchObject({
|
||||
obchodniJmeno: "BOHA Automation",
|
||||
pocet: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns [] for a blank query without calling ARES", async () => {
|
||||
const spy = vi.spyOn(globalThis, "fetch");
|
||||
expect(await aresSearchByName(" ")).toEqual([]);
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("maps upstream failure to ares_unavailable", async () => {
|
||||
mockFetchOnce(503);
|
||||
expect(await aresSearchByName("BOHA")).toEqual({
|
||||
error: "ares_unavailable",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user