# ČPZP — Automatické stahování zpráv

## Co to dělá

Dva skripty které se přihlásí na portál ČPZP a stáhnou všechny zprávy ze schránek:

1. `01_prihlaseni.py` — přihlásí se certifikátem, uloží cookies do `cpzp_cookies.json`
2. `02_stahuj_vse.py` — použije cookies, projde schránky, stáhne soubory do `Staženo/`

---

## Jak funguje přihlášení

Portál **nepoužívá heslo** — autentizuje certifikátem přes 3 kroky:

### Krok 1 — Získej session a challenge

```
GET https://portal.cpzp.cz/app/login/
→ server nastaví cookie: PHPSESSID=...
→ v HTML stránce je vložen JS objekt s challengem a CSRF tokenem
```

Challenge je v HTML jako JS výraz (ne JSON):
```javascript
CPZP = {
    settings : {
        certificateLoginKey : 'Prohlášení:'+ String.fromCharCode(13, 10) +
            'Tímto se přihlašuji k Portálu ČPZP'+ String.fromCharCode(13, 10) +
            ''+ String.fromCharCode(13, 10) +
            'Okamžik vygenerování tohoto prohlášení: 20.04.2026 14:52:47',
        ...
    }
}
```

CSRF token je v HTML formuláři:
```html
<form name="frmPrihlasCert" method="post" action="/app/">
  <input name="csrfCert" type="hidden" value="42ead46f7d374805c5d9...">
  <input id="sign" name="sign" type="hidden" value="">
</form>
```

### Krok 2 — Podpis certifikátem

Challenge (sestavená z JS výrazu) se podepíše jako **PKCS7 / CMS SignedData**:
- Algoritmus: **RSA + SHA-256**
- Typ: **DetachedSignature** (obsah není vložen do podpisu)
- **BEZ CA řetězu** — pouze end-entity certifikát
- Výsledný formát: **PEM s hlavičkami** (`-----BEGIN PKCS7-----`)

```python
pem_podpis = (
    pkcs7.PKCS7SignatureBuilder()
    .set_data(challenge.encode("utf-8"))
    .add_signer(cert, private_key, hashes.SHA256())
    .sign(Encoding.PEM, [PKCS7Options.DetachedSignature])
)
```

> ⚠️ Na rozdíl od VoZP portál ČPZP očekává **celý PEM string včetně hlaviček**,
> ne jen base64 DER (přestože oboje přes NMSigner). Bez hlaviček vrátí 500.

### Krok 3 — Přihlášení

```
POST https://portal.cpzp.cz/app/
Content-Type: application/x-www-form-urlencoded

csrfCert=<token_z_formulare>&sign=<pem_podpis>

Úspěch: odpověď neobsahuje "frmPrihlasCert" (login stránka)
```

---

## Certifikát

| Položka | Hodnota |
|---|---|
| Soubor | `U:\ordinaceprojekt\Insurance\Certificates\MBQualifiedCert.pfx` |
| Vlastník | MUDr. Michaela Buzalková |
| Vydavatel | I.CA EU Qualified CA2/RSA 06/2022 |
| Platnost | do 2027-01-16 |
| Thumbprint | `056ED80A3CDDE31DD36EECE0181B4E78D61122A7` |

---

## Stahování souborů

`02_stahuj_vse.py` používá `requests` (bez Playwright) — portál nevyžaduje JS pro navigaci.

### Schránky které se prochází

| URL | Název |
|---|---|
| `/app/schranka/` | Schránka klienta |
| `/app/schranka-pzs/` | Schránka PZS |

> Obě schránky obsahují stejné zprávy — skript deduplicuje podle ID zprávy.

### Paginace

Portál zobrazuje 20 zpráv na stránku, stránkování přes `?offset=N`:

```
GET /app/schranka/?offset=0    → zprávy 1–20
GET /app/schranka/?offset=20   → zprávy 21–40
...
```

Detekce konce: pokud stránka neobsahuje žádné nové ID, zastav.

### Struktura zprávy v seznamu

```html
<tr id="message-69cd7aa38ec68a837b5d8cbc" class="mail-read status-done">
  <td>...</td>
  <td>zpracováno</td>
  <td>09305000 - MUDr. Michaela Buzalková</td>
  <td>VYÚČTOVÁNÍ ZDRAVOTNÍ PÉČE Ref. č. 26274350</td>
  <td>01.04.2026 22:05:42</td>
  <td><a href="/app/schranka/detail/69cd7aa38ec68a837b5d8cbc/">zobrazit detail</a></td>
</tr>
```

ID zprávy je hex string (24 znaků), ne číslo jako u VoZP.

### Detail zprávy a stažení

```
GET /app/schranka/detail/{hex_id}/
→ stránka obsahuje odkaz na soubor

<a href="/app/schranka/protokol/?path=c387260ba8f041609a663038be79281c">
  ZU250168094V1.pdf          ← název souboru (nebo "26274350 (stáhnout protokol)")
</a>
```

```
GET /app/schranka/protokol/?path={hash}
→ vrátí soubor (PDF nebo HTML podle druhu zprávy)
```

### Typy souborů

| Druh zprávy | Typ souboru | Obsah |
|---|---|---|
| IČZ: ... Č.faktury: ... | **PDF** | Zúčtovací zpráva |
| VYÚČTOVÁNÍ ZDRAVOTNÍ PÉČE | **HTML** | Protokol přijetí vyúčtování |
| KLIENTELA | **HTML** | Protokol přijetí |
| Konečné vyúčtování | **PDF** | Závěrečné vyúčtování |

Přípona se určuje podle textu linku (`.pdf`) nebo Content-Type odpovědi.

### Pojmenování stažených souborů

```
YYYY-MM-DD Druh zprávy (Ref. XXXXXXXXX).přípona
```

Příklady:
```
2026-03-26 IČZ_ 09305000 Č.faktury_ 260027 (Ref. 26220064).pdf
2026-04-01 VYÚČTOVÁNÍ ZDRAVOTNÍ PÉČE (Ref. 26274350).html
2025-05-29 Konečné vyúčtování (Ref. 24774290).pdf
```

Znaky nepovolené ve Windows názvech (`/ : * ? " < > |`) se nahrazují podtržítkem.

---

## Rozdíly oproti VoZP

| | VoZP | ČPZP |
|---|---|---|
| Challenge zdroj | JSON API (`/json-api/prihlaseni/prihlasovaci-zprava`) | HTML stránka (JS výraz) |
| Challenge formát | JSON string s `\r\n` | JS string concatenation + `String.fromCharCode` |
| Podpis formát | PEM s hlavičkami | PEM s hlavičkami |
| Odeslání | POST JSON na API endpoint | POST form-data na `/app/` |
| CSRF | ne | ano (`csrfCert`) |
| ID zprávy | číselné | hex string (24 znaků) |
| Stahování | Playwright + `context.request.get()` | čisté `requests` |
| Typy souborů | vždy HTML | HTML i PDF |

---

## Závislosti

```
pip install requests cryptography beautifulsoup4
```

---

## Spuštění

```bash
python 01_prihlaseni.py   # přihlásí, uloží cookies (nutné při expiraci session)
python 02_stahuj_vse.py   # stáhne vše, přeskočí již existující soubory
```

Session cookie (`PHPSESSID`) expiruje — pokud `02_stahuj_vse.py` zahlásí
*"Cookies expirovala"*, spusť nejdřív `01_prihlaseni.py`.
