"""Supplier reputation checks against Czech public registers. Sources (all free, public): - ARES (ares.gov.cz) REST → existence, name, address, legal form, dates, VAT registration (stavZdrojeDph), insolvency flag (stavZdrojeIr), and name→IČO search. - MFČR CRPDPH SOAP → 'nespolehlivý plátce DPH' (unreliable VAT payer) + published bank accounts. """ import logging import re import xml.etree.ElementTree as ET import httpx logger = logging.getLogger(__name__) ARES_BASE = "https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/ekonomicke-subjekty" MFCR_URL = ("https://adisrws.mfcr.cz/adistc/axis2/services/" "rozhraniCRPDPH.rozhraniCRPDPHSOAP") _TIMEOUT = 20.0 def _clean_ico(ico: str) -> str: return re.sub(r"\D", "", str(ico or "")) def ares_by_ico(ico: str) -> dict | None: """Look up one subject by IČO. Returns a normalized dict or None.""" ico = _clean_ico(ico).zfill(8) if _clean_ico(ico) else "" if len(ico) != 8: return None try: r = httpx.get(f"{ARES_BASE}/{ico}", headers={"accept": "application/json"}, timeout=_TIMEOUT) if r.status_code == 404: return None r.raise_for_status() d = r.json() except Exception as exc: logger.warning("ARES lookup failed for %s: %s", ico, exc) return None reg = d.get("seznamRegistraci") or {} sidlo = (d.get("sidlo") or {}).get("textovaAdresa") return { "ico": d.get("ico"), "dic": d.get("dic"), "name": d.get("obchodniJmeno"), "address": sidlo, "legal_form": d.get("pravniForma"), "founded": d.get("datumVzniku"), "dissolved": d.get("datumZaniku"), # AKTIVNI in the business/commercial register or RES "active": reg.get("stavZdrojeVr") == "AKTIVNI" or reg.get("stavZdrojeRes") == "AKTIVNI", "vat_registered": reg.get("stavZdrojeDph") == "AKTIVNI", "in_insolvency": reg.get("stavZdrojeIr") == "AKTIVNI", } def ares_search_by_name(name: str, limit: int = 5) -> list[dict]: """Find candidates by trade name. Returns [{ico, name, address}].""" name = (name or "").strip() if len(name) < 3: return [] try: r = httpx.post(f"{ARES_BASE}/vyhledat", json={"obchodniJmeno": name, "pocet": limit}, headers={"accept": "application/json"}, timeout=_TIMEOUT) r.raise_for_status() d = r.json() except Exception as exc: logger.warning("ARES name search failed for %r: %s", name, exc) return [] out = [] for s in (d.get("ekonomickeSubjekty") or [])[:limit]: out.append({ "ico": s.get("ico"), "name": s.get("obchodniJmeno"), "address": (s.get("sidlo") or {}).get("textovaAdresa"), }) return out def mfcr_unreliable_vat(ico_or_dic: str) -> dict: """MFČR 'nespolehlivý plátce DPH' check. DIČ number = CZ + IČO usually, the service takes the numeric part. Returns {checked, unreliable, accounts}.""" num = _clean_ico(ico_or_dic) if len(num) < 8: return {"checked": False, "unreliable": None, "accounts": []} body = ( '' '' '' f'{num}' '' ) try: r = httpx.post(MFCR_URL, content=body, headers={"Content-Type": "text/xml; charset=utf-8", "SOAPAction": ""}, timeout=_TIMEOUT) r.raise_for_status() # strip namespaces for easy find xml = re.sub(r'\sxmlns(:\w+)?="[^"]*"', "", r.text) xml = re.sub(r"<(/?)\w+:", r"<\1", xml) root = ET.fromstring(xml) node = root.find(".//statusPlatceDPH") if node is None: return {"checked": False, "unreliable": None, "accounts": []} flag = node.get("nespolehlivyPlatce") # ANO/NE/NENALEZEN accounts = [] for u in node.findall(".//ucet"): std = u.find("standardniUcet") nst = u.find("nestandardniUcet") if std is not None: pref = std.get("predcisli") cislo = std.get("cislo") kb = std.get("kodBanky") acc = (f"{pref}-" if pref else "") + f"{cislo}/{kb}" accounts.append(acc) elif nst is not None: accounts.append(nst.get("cislo")) return { "checked": flag in ("ANO", "NE"), "unreliable": (flag == "ANO") if flag in ("ANO", "NE") else None, "vat_status": flag, "accounts": accounts, } except Exception as exc: logger.warning("MFČR VAT check failed for %s: %s", num, exc) return {"checked": False, "unreliable": None, "accounts": []} def reputation(ico: str) -> dict | None: """Full reputation report for one IČO. None if the subject doesn't exist.""" ares = ares_by_ico(ico) if ares is None: return None vat = mfcr_unreliable_vat(ares.get("dic") or ico) # Simple flag set for the UI to highlight. flags = [] if ares.get("dissolved"): flags.append(("danger", "Subjekt zanikl")) if not ares.get("active"): flags.append(("danger", "Není aktivní v rejstříku")) if ares.get("in_insolvency"): flags.append(("danger", "Vedeno v insolvenčním rejstříku")) if vat.get("unreliable") is True: flags.append(("danger", "Nespolehlivý plátce DPH")) if not flags: flags.append(("ok", "Bez zjištěných rizik v rejstřících")) return {"ares": ares, "vat": vat, "flags": flags}