From 4aee1a05bd740392884aa7bfb8b28d1208ea1de0 Mon Sep 17 00:00:00 2001 From: "vladimir.buzalka" Date: Tue, 28 Apr 2026 16:53:36 +0200 Subject: [PATCH] z230 --- Insurance/KdoJeLékař/NOTES.md | 102 +++++++++++++++++++++++++++++ Insurance/KdoJeLékař/_test_temp.py | 74 +++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 Insurance/KdoJeLékař/NOTES.md create mode 100644 Insurance/KdoJeLékař/_test_temp.py diff --git a/Insurance/KdoJeLékař/NOTES.md b/Insurance/KdoJeLékař/NOTES.md new file mode 100644 index 0000000..8bef44c --- /dev/null +++ b/Insurance/KdoJeLékař/NOTES.md @@ -0,0 +1,102 @@ +# KdoJeLékař — poznámky k vývoji + +## Cíl + +Zjistit pro pacienty z Medicus DB, kdo je jejich registrující **praktický lékař (001)**, **gynekolog (002)** a **stomatolog (014)** — dotazem na VZP B2B portál. + +--- + +## VZP B2B služba: `RegistracePojistencePZSB2B` + +### Endpoint (produkce) +``` +https://prod.b2b.vzp.cz/B2BProxy/HttpProxy/RegistracePojistencePZSB2B +``` + +### Autentizace +mTLS — klientský certifikát `.pfx` od **I.CA** (První certifikační autorita). +Stejný mechanismus jako u `stavPojisteniB2B`. + +### SOAP požadavek +```xml + + 7309208104 + 2026-04-28 + + 001 + 002 + 014 + + +``` + +XML namespace odpovědi: `http://xmlns.gemsystem.cz/B2B/RegistracePojistencePZSB2B/1` + +### Struktura odpovědi +Pro každou odbornost vrátí jeden `` element: + +| XML tag | Popis | +|---------|-------| +| `ICZ` | IČZ zdravotnického zařízení | +| `ICP` | IČP lékaře | +| `nazevICP` | Jméno lékaře | +| `nazevSZZ` | Název zdravotnického zařízení | +| `zdravotniPojistovna/kod` | Kód pojišťovny pacienta | +| `zdravotniPojistovna/zkratka` | Zkratka pojišťovny | +| `odbornost/kod` | Kód odbornosti (001/002/014) | +| `odbornost/nazev` | Název odbornosti | +| `datumRegistrace` | Datum registrace u tohoto lékaře | +| `datumZahajeni` | Začátek registrace | +| `datumUkonceni` | Konec registrace (null = stále aktivní) | +| `stavVyrizeniPozadavku` | Stavový kód odpovědi VZP | + +Pokud pacient nemá v dané odbornosti registrovaného lékaře, VZP vrátí prázdný seznam — je třeba uložit placeholder řádek (viz vzor v `u:\insurance\02.2 Testík.py`, funkce `upsert_rows`, negativní cesta). + +--- + +## Zdrojový kód pro inspiraci + +Vše je hotové v `u:\insurance\`, stačí přenést a adaptovat: + +| Soubor | Co obsahuje | +|--------|-------------| +| `u:\insurance\02 testík.py` | Hlavní logika: `build_envelope`, `parse_registrace`, `upsert_rows`, batch smyčka přes Medicus KAR | +| `u:\insurance\02.2 Testík.py` | Vylepšená verze — ukládá i negativní výsledky (pacient bez lékaře) | +| `u:\insurance\functions.py` | `get_medicus_connection()`, `get_mysql_connection()` — připojení k Firebird a MySQL | +| `u:\insurance\knihovny\vzpb2b_client.py` | Třída `VZPB2BClient` — obecný mTLS klient pro VZP B2B | + +--- + +## Certifikáty — stav k 28. 4. 2026 + +| Soubor | Platnost | Stav | +|--------|----------|------| +| `u:\insurance\Certificates\MBcert.pfx` | do 22. 1. 2026 | **EXPIROVAL** | +| `u:\insurance\10 Tests\MBcert.pfx` | do 22. 1. 2026 | **EXPIROVAL** | +| `u:\insurance\Certificates\ICAPublicMBdo16JAN2027.pfx` | do cca. 16. 1. 2027 | Platný, ale **neznáme heslo** — zjistit! | +| `u:\ordinaceprojekt\Insurance\Certificates\MBQualifiedCert.pfx` | do 16. 1. 2027 | Platný, ale **VZP odmítá** (kvalifikovaný certifikát, ne komerční) | +| `u:\ordinaceprojekt\Insurance\Certificates\picka.pfx` | neznámá | Neznáme heslo | +| `u:\insurance\Certificates\picka.pfx` | neznámá | Neznáme heslo | + +### Klíčový problém k vyřešení jako první +**`ICAPublicMBdo16JAN2027.pfx` je pravděpodobně správný certifikát** (název naznačuje I.CA, správná CA pro VZP B2B, platný do 2027). Vyzkoušená hesla nepasují: +- `Vlado7309208104++`, `vlado7309208104++`, `7309208104`, `Vlado9674+`, `masterkey`, `""` + +→ **Zjistit heslo** pod kterým byl exportován (nebo exportovat znovu z Windows certstore). + +--- + +## Testovací skript + +`u:\ordinaceprojekt\Insurance\KdoJeLékař\_test_temp.py` +Dotáže se na RC `7309208104`, odbornosti 001+002+014, vypíše raw XML i parsovaný výsledek bez zápisu do DB. +Aktuálně nastavený na `MBQualifiedCert.pfx` — po zjištění hesla přepnout na správný certifikát. + +--- + +## Plán dalšího postupu + +1. **Zjistit heslo k `ICAPublicMBdo16JAN2027.pfx`** (nebo exportovat nový .pfx z Windows certstore) +2. Ověřit funkčnost — spustit `_test_temp.py` s funkčním certifikátem +3. Vytvořit produkční skript v tomto adresáři — batch zpracování všech pacientů z Medicus KAR +4. Navrhnout cílovou MySQL tabulku `vzp_registrace_lekari` (nebo přidat sloupce do existující `vzp_registrace`) diff --git a/Insurance/KdoJeLékař/_test_temp.py b/Insurance/KdoJeLékař/_test_temp.py new file mode 100644 index 0000000..159fa10 --- /dev/null +++ b/Insurance/KdoJeLékař/_test_temp.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys +sys.path.insert(0, r"u:\insurance") + +from requests_pkcs12 import Pkcs12Adapter +import requests +import xml.etree.ElementTree as ET +from datetime import date + +ENDPOINT = "https://prod.b2b.vzp.cz/B2BProxy/HttpProxy/RegistracePojistencePZSB2B" +PFX_PATH = r"u:\ordinaceprojekt\Insurance\Certificates\MBQualifiedCert.pfx" +PFX_PASS = "Vlado7309208104++" + +RC = "7309208104" +K_DATU = date.today().isoformat() +ODBORNOSTI = ["001", "002", "014"] # VPL, gynekologie, stomatologie + +NS = { + "soap": "http://schemas.xmlsoap.org/soap/envelope/", + "rp": "http://xmlns.gemsystem.cz/B2B/RegistracePojistencePZSB2B/1", +} + +odb_xml = "".join(f"{k}" for k in ODBORNOSTI) +envelope = f""" + + + + {RC} + {K_DATU} + {odb_xml} + + +""" + +session = requests.Session() +session.mount("https://", Pkcs12Adapter(pkcs12_filename=PFX_PATH, pkcs12_password=PFX_PASS)) + +resp = session.post(ENDPOINT, data=envelope.encode("utf-8"), + headers={"Content-Type": "text/xml; charset=utf-8", "SOAPAction": "process"}, + timeout=30, verify=True) + +print(f"HTTP: {resp.status_code}\n") +print("=== RAW XML ===") +print(resp.text) +print("\n=== PARSED ===") + +root = ET.fromstring(resp.text) +items = root.findall(".//rp:seznamOdbornosti/rp:odbornost", NS) +if not items: + st = root.find(".//rp:stavVyrizeniPozadavku", NS) + print(f"Žádné záznamy. stavVyrizeniPozadavku={st.text if st is not None else '?'}") +else: + for it in items: + def g(tag): + el = it.find(f"rp:{tag}", NS) + return el.text.strip() if el is not None and el.text else None + + odb = it.find("rp:odbornost", NS) + odb_kod = odb.find("rp:kod", NS).text.strip() if odb is not None and odb.find("rp:kod", NS) is not None else None + odb_naz = odb.find("rp:nazev", NS).text.strip() if odb is not None and odb.find("rp:nazev", NS) is not None else None + + print(f" odbornost: {odb_kod} – {odb_naz}") + print(f" ICZ: {g('ICZ')}") + print(f" ICP: {g('ICP')}") + print(f" nazevICP: {g('nazevICP')}") + print(f" nazevSZZ: {g('nazevSZZ')}") + print(f" datumRegistrace: {g('datumRegistrace')}") + print(f" datumZahajeni: {g('datumZahajeni')}") + print(f" datumUkonceni: {g('datumUkonceni')}") + print() + +st = root.find(".//rp:stavVyrizeniPozadavku", NS) +print(f"stavVyrizeniPozadavku: {st.text.strip() if st is not None and st.text else '?'}")