# Nyugtaszerver API — fejlesztői leírás

Ez a leírás annak szól, aki **egy webalkalmazásból nyugtát (vagy más adóügyi
bizonylatot) akar nyomtatni** a boltban álló Sam4s NR-330 pénztárgépen, a
nyugtaszerveren keresztül. Tartalmazza az API teljes működését **és** azt, hogy
logikailag miképp célszerű használni — mert a lánc túlsó vége adóügyi
bizonylatnyomtató, ahol a rossz hívási minta valódi pénzügyi hibát okoz.

**Ez a dokumentum önmagában elég az integrációhoz.**

> **Szóhasználat.** *Vezérlő* = a Windows program az ügyfél gépén, ami a soros kábelen
> fogja a pénztárgépet. *Nyugtaszerver* = a PHP szerver, amivel te beszélsz. *Terminál*
> = a pénztárgép **bejegyzése** a nyugtaszerveren (adatbázissor, nem program).

---

## 0. Egy perc alatt

```bash
curl -X POST "https://pelda.hu/nyugtaszerver/api/v1/nyugta" \
  -H "Authorization: Bearer nyk_..." \
  -H "Content-Type: application/json" \
  -d '{"tipus":"nyugta",
       "tetelek":[{"arunev":"Kávé","ar":500,"mennyiseg":2,"teljesErtek":1000,"afaKor":"A00"}],
       "fizetesek":[{"fizetoeszkoz":1,"ertek":1000}]}'
```

```json
{ "feladatId": "6c1f0c8e-...-9b2a", "allapot": "var", "terminal": "A31500001" }
```

A hívás **nem várja meg a nyomtatást**. A `feladatId`-t tedd el a rendelés mellé, és
később kérdezd le:

```bash
curl -H "Authorization: Bearer nyk_..." \
     "https://pelda.hu/nyugtaszerver/api/v1/nyugta/6c1f0c8e-...-9b2a"
```

```json
{
  "feladatId": "6c1f0c8e-...-9b2a",
  "allapot": "kesz",
  "eredmeny": { "statusz": "siker", "nyugtaszam": "0012/00034", "datum": "2026.08.24. 14:32" }
}
```

Ennyi az egész. A dokumentum többi része arról szól, **mikor melyik hibát hogyan kell
kezelni** — és ez a nehezebbik fele.

---

## 1. A lánc — ki mit csinál

```
[a te webalkalmazásod]
        │  POST /api/v1/nyugta          (HTTP, Bearer kulcs)
        ▼
[nyugtaszerver]  ── feladat sorba ──┐
   PHP + MySQL                      │  a vezérlő 2 mp-enként lekér egy
        ▲                           │  statikus jelzőfájlt (304 Not Modified),
        │  POST .../jelentkezes      │  és csak változáskor jelentkezik be
        └───────────────────────────┘
        │
[vezérlő]  Windows program az ügyfél gépén
        │  PTG protokoll, soros port 9600 8N1
        ▼
[Sam4s NR-330]  ← itt jön ki a papír
```

Amit ebből tudni kell:

- **A nyomtatás aszinkron, és nem is lehet más.** A pénztárgép a bolt gépén van, a
  vezérlő húzza magához a munkát. A te kérésed a szerverig jut el, nem a papírig.
- **A vezérlő húz, a szerver nem tol.** A szerver nem hívja a vezérlőt; a vezérlő
  jelentkezik be a munkáért. Feléd ezzel szemben **tud értesítést küldeni** (4/a), de a
  lekérdezés akkor is kell: a webhookot egy óra után feladjuk.
- **A feladat sorban áll.** Ha a vezérlő nem fut, vagy a pénztárgép nem elérhető, a
  feladat `var` állapotban marad a szerveren, amíg a helyzet nem rendeződik. Ez
  **szándékos**: az elmaradt bizonylat pótolható, a duplán kiadott nem vonható vissza.

### Időzítés — mire számíts

| Lépés | Jellemző idő |
|---|---|
| `POST /api/v1/nyugta` visszatér | néhány tized másodperc |
| a vezérlő észreveszi a munkát | **kb. 2–5 másodperc** (statikus jelzőfájl figyelése) |
| a bizonylat kinyomtatása | 1–3 másodperc (9600 baud) |
| az eredmény megjelenik az API-ban | a nyomtatás után a **következő** lekérdezési körben, tipikusan **2–5 másodperc** |

Vagyis egy egészséges rendszerben a leadástól a `kesz` állapotig **~5–10 másodperc**
telik el. Ha 30 másodperc után is `var` az állapot, akkor nem lassú a rendszer, hanem
**áll valami** — lásd a 7.4 pontot.

---

## 2. Mielőtt kódolsz — mi kell hozzá

Három dolog, mindhármat a nyugtaszerver admin felületén intézi az üzemeltető:

1. **Ügyfél** — a számlázási entitás. Alá tartoznak a pénztárgépek és az alkalmazások.
2. **API kulcs** (*Alkalmazások* lap). `nyk_` előtaggal kezdődő szöveg. **Csak a
   létrehozáskor látszik**: a szerver a SHA-256 lenyomatát tárolja, visszanyerni nem
   lehet. Egy kulcs **egy ügyfélhez** tartozik, és csak annak a termináljait éri el.
3. **Aktivált terminál, aktív előfizetéssel.** A terminál akkor jön létre, amikor
   valaki beváltja az egyszer használatos aktiválási kódot — normál esetben **a
   vezérlő teszi ezt** a *Nyugtaszerver* lapján. Neked jellemzően nincs vele dolgod.

**Az integráció első hívása mindig ez legyen** — nem nyomtat semmit, mégis egyszerre
igazolja az URL-t, a kulcsot, a terminált és az előfizetést:

```bash
curl -H "Authorization: Bearer nyk_..." "https://pelda.hu/nyugtaszerver/api/v1/terminalok"
```

---

## 3. Az API alapjai

### Alapcím és a három URL-alak

A szerver **`mod_rewrite` nélkül is teljes értékű**. Osztott tárhelyen gyakori, hogy az
`.htaccess`-t a szerver figyelmen kívül hagyja; ilyenkor a nyitólap még bejön, de
minden végpont 404-et adna. Ezért a szerver háromféle alakot ért, és mindhárom
ugyanazt tudja:

| Alak | Példa | Feltétel |
|---|---|---|
| szép | `https://pelda.hu/nyugtaszerver/api/v1/nyugta` | `mod_rewrite` |
| útvonal | `https://pelda.hu/nyugtaszerver/index.php/api/v1/nyugta` | `PATH_INFO` |
| lekérdezés | `https://pelda.hu/nyugtaszerver/index.php?ut=/api/v1/nyugta` | **mindig működik** |

**Ajánlott eljárás:** üzembe helyezéskor egyszer derítsd ki, melyik alak jár azon a
tárhelyen, és **tedd be a beállításaidba**. Ne próbálkozz kérésenként.

A felismerés módja: hívd meg a `GET /api/v1/terminalok` végpontot a szép alakban. Ha a
válasz **értelmezhető JSON** (akár `401`!), az alak jó — mert az útválasztás működik.
Ha HTML jön (az Apache 404-es lapja), válts a lekérdezéses alakra.

> A lekérdezéses alakban az útvonalat URL-kódolva kell átadni:
> `index.php?ut=%2Fapi%2Fv1%2Fnyugta`. A per-jeleket a szerver kódoltan is elfogadja.

### Hitelesítés

Minden hívás:

```http
Authorization: Bearer nyk_...
Accept: application/json
Content-Type: application/json; charset=utf-8      (csak POST-nál)
```

Néhány Apache-beállításnál az `Authorization` fejléc nem jut át a PHP-hoz; a szerver
erre fel van készítve (`HTTP_AUTHORIZATION`, `REDIRECT_HTTP_AUTHORIZATION` és
`apache_request_headers` — mindhárom helyről megpróbálja kiolvasni).

### Kérés- és válaszalak

- Kódolás **UTF-8**, a törzs JSON objektum.
- A **mezőnevek magyarul** vannak (`arunev`, `fizetoeszkoz`, `teljesErtek`) — ez a
  projekt konvenciója, végig egységes.
- A hibaválasz alakja **mindig** ez:

```json
{ "hiba": { "kod": "nincs_elofizetes", "uzenet": "A(z) A31500001 AP számú pénztárgép előfizetése nem aktív." } }
```

A `kod` gépi feldolgozásra való, az `uzenet` emberi olvasásra. **A kódra ágazz, ne a
szövegre.**

---

## 4. Végpontok

Négy végpont van, és **mind a kilenc feladattípus ugyanazon az egyen megy** — a
`tipus` mező dönti el, mit csinál a vezérlő. Nincs típusonként külön végpont.

### 4.1 `POST /api/v1/nyugta` — feladat leadása

**Törzs:** ugyanaz a job-JSON, mint a fájl alapú munkasorban (lásd az 5. pontot és a
[`FAJL-INTERFESZ.md`](FAJL-INTERFESZ.md) teljes mezőleírását), plusz két **opcionális,
a szervernek szóló** mező: `terminal` és `kulsoAzonosito`.

```json
{
  "tipus": "nyugta",
  "terminal": "A31500001",
  "kulsoAzonosito": "WEB-10432",
  "tetelek": [
    { "arunev": "Hamburger", "ar": 1200, "mennyiseg": 2,
      "mennyisegiEgyseg": "db", "teljesErtek": 2400, "afaKor": "A00" }
  ],
  "fizetesek": [
    { "fizetoeszkoz": 2, "ertek": 1400 },
    { "fizetoeszkoz": 1, "ertek": 1000 }
  ]
}
```

**Válasz — `202 Accepted`:**

```json
{ "feladatId": "6c1f0c8e-3a41-4a9d-b0e2-9f8c7d6e5b2a", "allapot": "var", "terminal": "A31500001" }
```

Amit tudni kell róla:

- **A `feladatId`-t a szerver adja** (UUID v4), és ez lesz a job `jobId`-je is. Amit te
  küldesz `jobId` néven, azt a szerver **felülírja**. Ez szándékos: így a vezérlő
  duplikátumvédelme minden ügyfélen és minden alkalmazáson át egyedi azonosítón
  dolgozik.
- **A saját rendelésszámodat a `feladatId` mellé tárold el.** Ha a papíron is látni
  akarod, arra az `azonosito` mező való — de azt a pénztárgép **rá is nyomtatja** (5.4
  pont, óvatosan).
- A `terminal` és a `kulsoAzonosito` mező **a szervernek szól**, nem a pénztárgépnek: a
  szerver mindkettőt kiveszi, mielőtt a payloadot eltárolja. A vezérlő tehát sosem látja
  őket, és a payload bájtra ugyanaz marad, mint a fájl alapú úton.
- A törzs legfeljebb **256 KB** lehet (egy nyugta ennél nagyságrendekkel kisebb).

#### `kulsoAzonosito` — ez védi meg a kettőzött nyugtától

**Add meg mindig, ahol van saját azonosítód** (rendelésszám, tranzakció-azonosító). Ha
megadod, a szerver **ugyanarra a kulcsra nem hoz létre második feladatot**, hanem
visszaadja az elsőt — vagyis a leadás ismételhetővé válik:

```json
{ "feladatId": "6c1f0c8e-…", "allapot": "var", "terminal": "A31500001", "ismetelt": true }
```

| | Válasz |
|---|---|
| **első** leadás ezzel a kulccsal | `202`, `ismetelt` nincs a válaszban |
| **ismételt** leadás, azonos tartalommal | `200`, `ismetelt: true`, és **ugyanaz a `feladatId`** |
| ismételt leadás, **más** tartalommal | `409 kulso_azonosito_utkozes` |

Enélkül a leadás **nem idempotens** (7.3 pont): egy válasz nélkül elszálló kérés után
nem tudható, létrejött-e a feladat, és a vak újraküldés **második nyugtát nyomtatna** —
amit adóügyi bizonylatnál nem lehet visszavonni. A `kulsoAzonosito` pontosan ezt a
kétértelműséget szünteti meg.

Amit érdemes tudni róla:

- **A hatóköre az alkalmazás**, nem az ügyfél. Két alkalmazásod ugyanazt a kulcsot
  egymástól függetlenül használhatja — szándékosan: a rendelésszámok tipikusan mindkét
  rendszerben 1-től indulnak, és ügyfél-szintű egyediség esetén a **másik** alkalmazás
  feladatát kapnád vissza.
- **Legfeljebb 100 karakter.** A `\` és a `|` **megengedett** benne (ellentétben minden
  más szövegmezővel): ez a kulcs sosem jut el a pénztárgépig.
- Az **üres** szöveget „nem adtál meg"-nek tekintjük, nem hibának.
- **Nem ugyanaz, mint az `azonosito`.** A `kulsoAzonosito` a szervernek szól és nem
  látszik a papíron; az `azonosito` a papírra kerül (5.4 pont). Az `azonosito` nem is
  tudná ezt a szerepet betölteni: abban például a törzsvásárlói kártyaszám áll, ami
  **szándékosan ismétlődik** vásárlásonként.
- **Egy kulcs egy bizonylathoz tartozik, visszavonás után is.** Ha a feladatot
  visszavontad, és ugyanazzal a kulccsal küldesz újra, a **visszavont** feladatot kapod
  vissza, és nem készül bizonylat. Ha tényleg új bizonylat kell, adj **új kulcsot**.

**Terminálválasztás:**

| Helyzet | Mit tegyél |
|---|---|
| az ügyfélnek **egy** pénztárgépe van | a `terminal` elhagyható |
| több pénztárgép, van közte **alapértelmezett** | elhagyható → az alapértelmezettre megy |
| több pénztárgép, nincs alapértelmezett | **kötelező**, különben `400 terminal_kotelezo` |

A `terminal` értéke lehet **AP szám** (`A31500001`) vagy a terminál **neve**
(`bolt-1`). Az AP szám a biztos: az egyedi az egész szerveren.

**Hibák:**

| HTTP | `kod` | Mikor | Mit tegyél |
|---|---|---|---|
| 401 | `nincs_token` | hiányzik az `Authorization` fejléc | javítsd a kérést |
| 401 | `ervenytelen_kulcs` | ismeretlen vagy visszavont kulcs | új kulcs kell az admintól |
| 400 | `ures_keres`, `hibas_json` | a törzs nem JSON objektum | javítsd a kérést |
| 400 | `terminal_kotelezo` | több pénztárgép, nincs megadva melyik | add meg a `terminal` mezőt |
| 400 | `hibas_kulso_azonosito` | a `kulsoAzonosito` nem szöveg/szám, vagy 100 karakternél hosszabb | javítsd a kérést |
| **409** | `kulso_azonosito_utkozes` | ugyanazt a kulcsot **más tartalommal** használtad | ha ez új bizonylat, adj új kulcsot; ha a korábbit ismételnéd, küldd ugyanazt a tartalmat |
| **503** | `sema_frissites_kell` | a szerver adatbázisán még nincs meg a kulcs oszlopa | üzemeltetői teendő (egy `ALTER TABLE`); a kulcs **nélküli** leadás közben is működik |
| 404 | `ismeretlen_terminal` | nincs ilyen AP számú/nevű terminál **ennél az ügyfélnél** | ellenőrizd az AP számot |
| 409 | `nincs_terminal` | az ügyfélnek nincs aktivált pénztárgépe | a vezérlőt aktiválni kell |
| **402** | `nincs_elofizetes`, `lejart_elofizetes` | az előfizetés nem aktív vagy lejárt | **üzleti kérdés, nem programhiba** — jelezd az üzemeltetőnek |
| 413 | `tul_nagy` | a feladat > 256 KB | ne küldj ekkorát |
| 503 | `sema_nincs_telepitve` | a szerver telepítése félbemaradt | üzemeltetői teendő |
| 500 | `szerverhiba` | váratlan hiba | a részlet a szerver hibanaplójában |

> **A `402` minden feladattípusra vonatkozik a webes úton — a `statusz`-ra is.** A
> fájl alapú úton a `statusz` és a `megszakitas` előfizetés nélkül is lefut; a
> nyugtaszerver viszont már a leadást elutasítja, ha nincs aktív előfizetés.

### 4.2 `GET /api/v1/nyugta/{feladatId}` — az eredmény lekérdezése

A `{feladatId}` a 36 karakteres UUID. Csak a **saját ügyfeled** feladatai
kérdezhetők le; másé `404 ismeretlen_feladat`.

```json
{
  "feladatId": "6c1f0c8e-3a41-4a9d-b0e2-9f8c7d6e5b2a",
  "allapot": "kesz",
  "tipus": "nyugta",
  "terminal": "A31500001",
  "letrehozva": "2026-08-24 14:31:58",
  "befejezve": "2026-08-24 14:32:11",
  "eredmeny": {
    "feladatId": "6c1f0c8e-3a41-4a9d-b0e2-9f8c7d6e5b2a",
    "jobId": "6c1f0c8e-3a41-4a9d-b0e2-9f8c7d6e5b2a",
    "statusz": "siker",
    "tipus": "nyugta",
    "nyugtaszam": "0012/00034",
    "datum": "2026.08.24. 14:32",
    "nyersValasz": "PTG|1|0012/00034|2026.08.24. 14:32",
    "hibakod": null,
    "hibaUzenet": null,
    "feldolgozva": "2026-08-24T14:32:11+02:00"
  }
}
```

- Az `eredmeny` **`null`**, amíg a vezérlő nem jelentett vissza.
- Az `eredmeny` szerkezete **bájtra ugyanaz**, mint a fájl alapú út
  `{azonosito}.eredmeny.json` fájlja — a vezérlő ugyanazt az objektumot tölti fel.
- `statusz` feladatnál az `eredmeny` egy `allapot` blokkot is tartalmaz a pénztárgép
  állapotával (5.5 pont).
- **A lekérdezés léptetheti az állapotot.** A szerveroldali időzár (6.1) itt is fut: egy
  régóta álló `var` feladat erre a hívásra válik `lejart`-tá. Ez szándékos — te vagy az,
  akit a válasz érdekel, és te pollozol akkor is, ha a bolti gép halott.
- A `lejart` és a **szerver által** feladott `bizonytalan` feladat `eredmeny`-e
  **`null`** marad; a magyarázat az állapotban van, nem az eredményben (6.1).

### 4.3 `POST /api/v1/nyugta/{feladatId}/visszavonas` — a feladat visszavonása

Egy **még át nem vett** feladat visszavonása: a bizonylat már nem kell, mert a vevő
elállt, a rendelés törlődött, vagy rossz terminálra adtad le.

```bash
curl -X POST "https://pelda.hu/nyugtaszerver/api/v1/nyugta/6c1f0c8e-.../visszavonas" \
  -H "Authorization: Bearer nyk_..."
```

**Válasz — `200 OK`**, ugyanabban az alakban, mint a lekérdezés:

```json
{
  "feladatId": "6c1f0c8e-3a41-4a9d-b0e2-9f8c7d6e5b2a",
  "allapot": "visszavonva",
  "tipus": "nyugta",
  "terminal": "A31500001",
  "letrehozva": "2026-10-01 14:31:58",
  "befejezve": "2026-10-01 14:32:04",
  "eredmeny": null
}
```

`DELETE /api/v1/nyugta/{feladatId}` ugyanezt teszi. **A POST alakot használd:** a
`DELETE` metódust osztott tárhelyen és vállalati proxy mögött nem egyszer megszűrik, és
a tünet alattomos — a fejlesztői gépen megy, élesben `405`-öt vagy egy HTML hibalapot
kapsz.

#### Ez a hívás idempotens — a leadás nem

A már visszavont feladatra is **`200`** jön, nem hiba. Ez szándékos, és **ez a
legfontosabb tudnivaló róla:**

> Ha a visszavonás válasz nélkül szállt el, **bátran megismételheted.** A leadásnál ez
> pont fordítva van (7.3 pont): ott a vak ismétlés egy második nyugtát nyomtathat.

Az aszimmetria oka egyszerű: a visszavonás nem hoz létre semmit, csak egy állapotot
állít be, amit kétszer beállítani ugyanaz, mint egyszer.

#### Amit nem lehet visszavonni

| Állapot | HTTP | `kod` | Miért, és mit tegyél |
|---|---|---|---|
| `atvette` | 409 | `mar_atvette` | A vezérlő már elvitte, és **lehet, hogy éppen nyomtatja**. Nincs csatorna, amin visszaszólhatnánk neki. Kérdezd le az állapotát; ha elkészült, sztornóval rendezd |
| `kesz` | 409 | `mar_elkeszult` | A bizonylat kész. **Adóügyi bizonylatot utólag nem lehet érvénytelenné tenni** — a helyes út egy `sztorno` feladat. Az üzenet megnevezi a bizonylatszámot |
| `bizonytalan` | 409 | `mar_lezart` | Nem tudható, kinyomtatódott-e. Emberi ellenőrzés kell a szalagról |
| `hiba` | 409 | `mar_lezart` | Lezárt állapot, nincs mit visszavonni |
| `lejart` | 409 | `mar_lezart` | A szerver már lezárta: a vezérlő a határidőig nem vette át (6.1). **Nincs mit visszavonni — bizonylat nem készült, és nem is fog** |

A három kód **szándékosan külön van**, mert a hívónak mást kell tennie: várni az
eredményre, sztornót adni, vagy emberre bízni.

**A visszavonás nem futtatja az időzárat.** Ha egy határidőn túli, de még `var`
állapotban álló feladatot vonsz vissza, `visszavonva` lesz, nem `lejart`. A gyakorlati
eredmény ugyanaz: bizonylat nem készült, és nem is fog.

#### Két részlet, ami integráláskor számít

**A visszavonás nem töröl sort.** A `GET /api/v1/nyugta/{feladatId}` a visszavonás után
is válaszol, `allapot: visszavonva`-val — a feladat sora üzleti nyilvántartás, és a
„sosem volt ilyen feladat" meg a „visszavontam" nem ugyanaz a mondat.

**A versenyhelyzet megoldott.** Ha a vezérlő épp a visszavonás pillanatában jelentkezik
be, pontosan egy győztes van: vagy a visszavonás sikerül (`200`), vagy a feladat átvett
lesz, és `409 mar_atvette` jön. **Nem fordulhat elő, hogy a feladat visszavontnak
látszik, de a vezérlő mégis kinyomtatja.**

### 4.4 `GET /api/v1/terminalok` — a pénztárgépeid

```json
{
  "terminalok": [
    {
      "apSzam": "A31500001",
      "nev": "bolt-1",
      "alapertelmezett": true,
      "allapot": "aktiv",
      "elofizetes": "aktiv",
      "ervenyesEddig": null,
      "online": true,
      "eletjel": "rendben",
      "akadaly": null,
      "utolsoJelentkezes": "2026-10-01 14:32:10",
      "eletjelPerc": 2
    }
  ]
}
```

| Mező | Jelentés |
|---|---|
| `allapot` | a terminál állapota a szerveren (`aktiv` / letiltott) |
| `elofizetes` | `aktiv` / `felfuggesztve` / `felmondva` |
| `ervenyesEddig` | `null` = határozatlan idejű |
| `eletjel` | **háromállapotú**: `rendben` / `akadaly` / `nem_jelentkezik` |
| `akadaly` | ha a vezérlő akadályt jelez, annak magyar szövege; különben `null` |
| `online` | az `eletjel` kétállapotú összevonása: jelentkezett-e a várt időn belül |
| `utolsoJelentkezes` | szerveridő |
| `eletjelPerc` | milyen ütemben jelentkezik a vezérlő, percben. `null` = régi vezérlő |

#### A három állapot

| `eletjel` | Mit jelent | Mit tegyen az üzemeltető |
|---|---|---|
| `rendben` | a vezérlő a várt ütemben jelentkezett, és eléri a pénztárgépet | semmit |
| `akadaly` | a vezérlő **fut**, de a gépet nem éri el (kihúzott USB-soros átalakító, elaltatott gép) | a bolti gépet és a kábelt kell megnézni; az `akadaly` szövege megmutatható a kezelőnek |
| `nem_jelentkezik` | a várt időn belül nem jelentkezett: **a vezérlő nem fut, vagy nincs internet** | a bolti PC-t kell megnézni |

A „várt idő" **nem fix**: a terminál által bejelentett ütem (`eletjelPerc`) kétszerese —
egy kimaradt életjelet tolerálunk. Egy **régi vezérlő** nem jelenti be az ütemét
(`eletjelPerc: null`), és rá a korábbi, óránkénti elvárás él; így a szerver frissítése
nem mutat halottnak egy még nem frissített boltot.

> **A leadást továbbra se kösd az `eletjel` mezőhöz.** Nem azért, mert hazudik — mostantól
> nem hazudik —, hanem mert **bármelyik állapot elavulhat** két életjel között, és mert a
> feladat akkor sem veszik el azonnal, ha a vezérlő épp nem elérhető: ott marad a
> szerveren, és a helyzet rendezése után lefut — a lejáratig (6.1). A megbízható jel a
> leadás után az, hogy a feladat meddig marad `var` állapotban (7.4 pont). Az `eletjel`
> **üzemeltetői kijelzésre és riasztásra** való.

---

## 4/a. Webhook — hogy ne kelljen pollozni

A szerver **értesítést küld**, amikor egy feladat végállapotba ér. Ez opcionális: ha nincs
beállítva, a 7.4 pont szerinti lekérdezés marad az egyetlen út.

> **A webhook NEM váltja ki teljesen a pollozást, és ezt érdemes előre tudni.** Egy óra
> után feladjuk a kiszállítást (lásd lentebb), és ilyenkor a feladat állapotát csak
> lekérdezéssel tudod megtudni. Tarts meg egy **ritka egyeztető kört** a nyitott
> feladataidra — nem a másodperces pollozást, csak egy percenkéntinél jóval ritkábbat.
>
> Ha a **felhasználó a képernyő előtt vár**, a pollozás továbbra is gyorsabb: a webhook
> kiküldése cronból megy, tehát 0–60 másodperc késleltetéssel. A webhook értéke nem a
> késleltetés, hanem az, hogy nem kell háttérfeladatot építened minden rendeléshez.

### Beállítás

Az **üzemeltető** állítja be az admin felületen, alkalmazásonként: a címet és a hozzá
tartozó **titkot**. A titkot egyszer mutatja meg a felület — azt kell a fogadódba
beégetni (környezeti változóba, nem a kódba).

A cím **csak `https`** lehet. Kivétel a `http://127.0.0.1` és a `http://localhost`, hogy
fejlesztés közben a saját gépeden is próbálhasd. Belső hálózatra mutató címet a szerver
elutasít — ez elgépelés elleni védelem, nem támadás elleni.

### A kérés, amit kapsz

```http
POST https://ugyfel.hu/nyugta-webhook HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: Nyugtaszerver/1.0
X-Nyugtaszerver-Esemeny: feladat.kesz
X-Nyugtaszerver-Alairas: t=1790000000,v1=9f86d081884c7d65…
```

```json
{
  "esemenyId": 4711,
  "esemeny": "feladat.kesz",
  "letrehozva": "2026-10-02T14:32:11+02:00",
  "feladatId": "6c1f0c8e-3a41-4a9d-b0e2-9f8c7d6e5b2a",
  "kulsoAzonosito": "WEB-10432",
  "terminal": "A31500001",
  "allapot": "kesz",
  "tipus": "nyugta",
  "eredmeny": {
    "statusz": "siker",
    "nyugtaszam": "0012/00034",
    "datum": "2026.10.02. 14:32",
    "hibakod": null,
    "hibaUzenet": null
  }
}
```

A **`kulsoAzonosito` benne van**, és ez teszi igazán használhatóvá: a saját
rendelésszámodat látod, nem kell a `feladatId`-ről visszakeresned.

| `esemeny` | Mikor |
|---|---|
| `feladat.kesz` | elkészült a bizonylat |
| `feladat.hiba` | nem készült el — a `hibakod` dönti el, újraküldhető-e (7.5) |
| `feladat.bizonytalan` | nem tudható, kinyomtatódott-e (7.6). Ha az `eredmeny` **null**, a szerver adta fel a várakozást (6.1) |
| `feladat.visszavonva` | te vontad vissza |
| `feladat.lejart` | a vezérlő a határidőig nem vette át (6.1) |
| `webhook.proba` | az üzemeltető próbaeseménye, **valódi feladat nélkül** — a `feladatId` ilyenkor `null` |

**`atvette` eseményről NEM kapsz értesítést.** Nem végállapot, viszont minden feladatnál
elsülne, tehát megduplázná a kéréseket azért az információért, hogy „elkezdődött".

> **PHP-ban ne írd meg újra.** A `src/Php_Kliens/` mappában ott a kész fogadó:
> `kliens/WebhookFogado.php` (ellenőrzés) és `fogado-minta.php` (a végpont mintája, amit
> két helyen átírsz). Az alábbi kódpélda azt mutatja meg, amit az osztály elvégez — hogy
> más nyelvben is megírható legyen.

### Az ellenőrzés három lépése — ezt mindháromat végezd el

```php
<?php
$nyers  = file_get_contents('php://input');            // A NYERS törzs kell!
$fejlec = $_SERVER['HTTP_X_NYUGTASZERVER_ALAIRAS'] ?? '';
$titok  = getenv('NYUGTA_WEBHOOK_TITOK');

// 1. Aláírás a NYERS bájtokra, hash_equals-szel (nem ===).
if (preg_match('/^t=(\d+),v1=([0-9a-f]+)$/', $fejlec, $m) !== 1) {
    http_response_code(400);
    exit;
}
[$egesz, $t, $v1] = $m;

if (!hash_equals(hash_hmac('sha256', $t . '.' . $nyers, $titok), $v1)) {
    http_response_code(401);
    exit;
}

// 2. Időablak: egy elfogott kérés így nem játszható vissza korlátlanul.
if (abs(time() - (int)$t) > 300) {
    http_response_code(401);
    exit;
}

// 3. Duplikátumszűrés az esemenyId-re.
$adat = json_decode($nyers, true);
if (mar_feldolgoztam((int)$adat['esemenyId'])) {
    http_response_code(200);      // már megvolt - 2xx-szel nyugtázzuk
    exit;
}

sorba_teszem($adat);              // a MUNKA a háttérbe, lásd lentebb
http_response_code(200);
```

Három részlet, amin a legtöbb hiba múlik:

1. **A nyers törzsre számolj HMAC-ot**, ne a `json_decode` → `json_encode` körbejárt
   alakra. A szóközök és a mezősorrend különben elcsúsznak, és minden aláírás hibásnak
   látszik.
2. **`hash_equals`, ne `===`.** Az időzítéses összehasonlítás mérhető különbséget ad.
3. **Az időbélyeg az aláírt értékben van** (`t . "." . torzs`), ezért nem cserélhető ki.
   Ha csak a törzset írnád alá, a `t` szabadon átírható lenne.

### A kiszállítás LEGALÁBB EGYSZERI

Egy időtúllépés után a szerver **nem tudja**, megérkezett-e a kérés — ezért újrapróbál.
Ebből két szabály következik:

- **Duplikátumszűrés az `esemenyId`-re**, és a feldolgozás legyen idempotens.
- **Válaszolj gyorsan 2xx-szel, a munkát tedd háttérbe.** Ha a feldolgozás 10
  másodpercnél tovább tart, időtúllépést kapsz — és **újraküldést**. Vagyis a lassú
  feldolgozás maga gyártja a duplikátumot.

| Próba | Mikor (az esemény után) |
|---|---|
| 1. | kb. 0–60 másodperc (a következő cron-percben) |
| 2. | +1 perc |
| 3. | +3 perc |
| 4. | +8 perc |
| 5. | +23 perc |
| 6. | +53 perc |

A hatodik után a szerver **feladja**. Nem veszik el semmi: a feladat állapotát a
lekérdezés ugyanúgy megadja, és az üzemeltető az admin *Webhookok* lapján látja a
feladott sorokat.

**Sikeres = bármilyen 2xx.** A `301`/`302` nem jó: a szerver **nem követ átirányítást**
(egy átirányítás az aláírt törzzsel együtt bárhová elvihetné a kérést).

---

## 5. A feladat törzse

### 5.1 A kilenc típus

| `tipus` | Mit csinál | Nyomtat? | Ismételhető ugyanazzal az azonosítóval? |
|---|---|---|---|
| `nyugta` | nyugtát nyomtat (ez az alapértelmezés, ha nincs `tipus`) | igen | **nem** |
| `sztorno` | sztornó bizonylat | igen | **nem** |
| `visszaru` | visszáru bizonylat | igen | **nem** |
| `szamla` | egyszerűsített számla | igen | **nem** |
| `penzmozgas` | pénz be / pénz ki | igen | **nem** |
| `napnyitas` | napnyitás | igen | **nem** |
| `napizaras` | napi forgalmi jelentés | igen | **nem** |
| `megszakitas` | a beragadt bizonylatot megszakítja | nem | igen |
| `statusz` | lekérdezi a gép állapotát | nem | igen |

A webes úton az „ismételhető" a *vezérlő* duplikátumvédelmére vonatkozik. Mivel a
`feladatId`-t a szerver adja, két külön POST **mindig két külön feladat** — a
duplikátumvédelem a vak újraküldéstől nem véd meg, csak attól, hogy ugyanaz a
feladatsor kétszer fusson le. A vak újraküldés elkerülése **a te felelősséged** (7.3).

### 5.2 Egy minimális nyugta

```json
{
  "tipus": "nyugta",
  "tetelek": [ { "arunev": "Kávé", "ar": 500, "mennyiseg": 2, "teljesErtek": 1000 } ],
  "fizetesek": [ { "fizetoeszkoz": 1, "ertek": 1000 } ]
}
```

Tétel mezői: `arunev` (kötelező, max **99** karakter), `ar` (kötelező), `mennyiseg`
(alap `1`, lehet tört, **mindig pozitív**), `mennyisegiEgyseg` (alap `"db"`),
`teljesErtek` (ha megadod, **egyeznie kell** az `ar × mennyiseg` szorzattal), `afaKor`
(`A00`…`E00`, alap `A00`), `cikkszam`, `gyujtoSorszam` (1–30), `megjegyzes` (max **50**
karakter), `felar`.

Fizetőeszköz kódok **alapbeállításban**: `1` készpénz · `2` bankkártya · `3` csekk ·
`4` utalvány · `5` Euró · `13` AFR · `14` Erzsébet utalvány · `15` SZÉP kártya ·
`16` ajándékutalvány · `17` hűségkártya · `18` smart kártya · `19` göngyölegjegy ·
`20` kupon. **Gépenként programozható — üzembe helyezéskor egyeztetni kell.**

### 5.3 A négy szabály, ami különben a papíron derülne ki

Ezek mindegyike **éles hardveren elkövetett hibából** származik. A vezérlő küldés előtt
ellenőrzi őket, de neked is érdemes már a saját oldaladon elkapni: úgy meg sem terheled
a láncot.

**1. A készpénz legyen az UTOLSÓ fizetés.** Visszajárót a pénztárgép **csak
készpénzből** tud adni. Ha egy nem készpénzes fizetés többet ad, mint amennyi
hátravan, a gép a nyugta **legvégén** `040B — Érték túl nagy!` hibát ad, és
**megszakítja a már majdnem kész bizonylatot**.

```
végösszeg 4000
|1|1400  készpénz → hátra 2600
|2|3000  kártya   → 400-zal több   ✗ a nyugta megszakad

végösszeg 4000
|2|3000  kártya   → hátra 1000     ✓
|1|1000  készpénz → pontosan annyi ✓
```

Készpénznek alapból az `1` (készpénz) és az `5` (Euró) számít.

**2. A `\` és a `|` tiltott karakter.** A pénztárgép protokolljában elválasztók, ezért
egyik szövegmezőben sem szerepelhetnek. A vezérlő az ilyen feladatot **elutasítja, nem
javítja ki** — egy `"Coca-Cola 0,5 l | akciós"` terméknév különben némán elrontaná a
bizonylat szerkezetét. **Ha a terméknevek felhasználói tartalomból jönnek, ezt neked
kell kiszűrnöd** még a rendelés felvételekor, nem a nyugta leadásakor.

**3. A fizetéseknek fedezniük kell a végösszeget.** Az alulfizetést a vezérlő
elutasítja.

**4. A hosszkorlátok karakterben, nem bájtban értendők.** `arunev` 99, `megjegyzes` 50,
`azonosito.nev` és `azonosito.ertek` 20–20, `vevo.*` mezőnként (név 50, irányítószám
10, település 20, közterület 50, jelleg 20, házszám 10, adószám 20).

### 5.4 Az `azonosito` mező — a leggyakoribb csapda

**Ez NEM belső azonosító.** A pénztárgép **rányomtatja a bizonylatra**, az összesen sor
után. A protokollban ez az egyetlen hely, ahová a nyugta végére saját szöveg kerülhet
(itt van tehát a törzsvásárlói kártyaszám helye is).

```json
"azonosito": { "nev": "rendeles", "ertek": "WEB-10432" }
```

- **Csak akkor add meg, ha tényleg látni akarod a papíron.** A rendelés és a nyugta
  összerendeléséhez nem kell: arra a `feladatId` való.
- **A pénztárgép elutasítja az adóügyi kifejezéseket tartalmazó szöveget.** Ha például
  a „nyugta" szó szerepel benne, a gép a bizonylat végén `8334` hibát ad és
  **megszakítja a majdnem kész nyugtát**; a papíron egy „NYUGTA-MEGSZAKÍTÁS" marad.
  Ezt **előre nem lehet ellenőrizni** — a tiltott kifejezések listája a pénztárgép
  belső ügye. Használj semleges szöveget, például puszta rendelésszámot.

### 5.5 A többi típus röviden

```json
{ "tipus": "penzmozgas", "irany": "be", "fizetoeszkoz": 1, "ok": 1, "osszeg": 50000 }
```
`irany`: `be` / `ki`. Befizetés okai 1–8, kifizetésé 1–11 (lista a
[`FAJL-INTERFESZ.md`](FAJL-INTERFESZ.md)-ben).

```json
{ "tipus": "napnyitas", "nyitoOsszeg": 50000 }
{ "tipus": "napizaras" }
{ "tipus": "megszakitas" }
{ "tipus": "statusz" }
```

`sztorno` / `visszaru`: a nyugta törzse **plusz** kötelező fejléc — `indok` (`S0`–`S8`
sztornónál, `V1`–`V3` visszárunál), `eredetiBizonylat` (`sorszam`, `datum`) és `vevo`
(a `nev` kötelező). **A tételeket itt is pozitívan kell megadni**; hogy sztornó vagy
visszáru, azt a `tipus` dönti el.

`szamla` (egyszerűsített számla): a nyugta törzse **plusz** `vevo`. Eredeti bizonylat és
indokkód nem tartozik hozzá. **Két helyen kell engedélyezni**: a nyugtaszerver admin
felületén a *Számla* kapcsolóval (enélkül a vezérlő `nincs_engedely` hibát ad), és
magán a pénztárgépen (enélkül a gép utasítja el).

A `statusz` eredménye a gép állapotát adja vissza:

```json
"allapot": {
  "statuszKod": 0,
  "leiras": "nap nyitva, nincs nyitott bizonylat",
  "napNyitva": true,
  "apSzam": "A31500001",
  "zarasokSzama": 12,
  "utolsoBizonylat": "12/00034",
  "nyugtakSzama": 21,
  "utolsoBizonylatOsszege": 2400,
  "megszakitottBizonylatok": 0,
  "utolsoBizonylatIdeje": "2026.08.24. 14:32"
}
```

Státuszkódok: `0` nap nyitva, nincs nyitott bizonylat · `1` nyitott nyugta ·
`2` nyitott nem adóügyi bizonylat · `4` **a nap zárva** · `5` nyitott sztornó ·
`6` nyitott visszáru · `7` nyitott göngyölegjegy.

A bővebb mezők (`nyugtakSzama`, `utolsoBizonylatOsszege`, `megszakitottBizonylatok`,
`utolsoBizonylatIdeje`) **nem mindig érkeznek meg** — a gép nem minden válaszban küldi
őket. **Ne feltételezd a jelenlétüket.** A darabszámok a pénztárgép saját **napi**
számlálói: minden bizonylatot számolnak, azt is, amit nem a te alkalmazásod adott le.

---

## 6. A feladat életútja

```
   POST /api/v1/nyugta
          │
          ▼
       ┌──────┐   a vezérlő bejelentkezik és átveszi
       │ var  │ ─────────────────────────────────────►┌─────────┐
       └──────┘                                        │ atvette │
          │  │                                         └─────────┘
          │  │ POST .../visszavonas                      │     │
          │  │ (CSAK innen lehet)                        │     │ a vezérlő
          │  ▼                                           │     │ visszajelent
          │ ┌─────────────┐                              │     ▼
          │ │ visszavonva │                              │  ┌──────┬──────┬─────────────┐
          │ └─────────────┘                              │  │ kesz │ hiba │ bizonytalan │
          │                                              │  └──────┴──────┴─────────────┘
          │ 2 óra után a szerver lezárja      a vezérlő elvitte, de
          ▼ (NEM nyomtatódik ki)              SOHA nem jelentett róla
      ┌────────┐                                         │
      │ lejart │                                         ▼
      └────────┘                                  ┌─────────────┐
                                                  │ bizonytalan │  (eredmeny: null)
                                                  └─────────────┘
```

| `allapot` | Jelentés | Mit tegyél |
|---|---|---|
| `var` | a szerveren sorban áll, a vezérlő még nem vette át | várj; ha sokáig tart, riassz (7.4) |
| `atvette` | a vezérlő dolgozik rajta | várj — másodpercek kérdése |
| `kesz` | **elkészült**, az `eredmeny.nyugtaszam` a bizonylatszám | tárold el, kész |
| `hiba` | nem készült el | **a hibakód dönti el**, újraküldhető-e (7.5) |
| `bizonytalan` | **nem tudható, kinyomtatódott-e** — vagy a vezérlő jelentette, hogy félbeszakadt, vagy a szerver adta fel a várakozást | **soha ne próbáld újra automatikusan** — emberi ellenőrzés (7.6) |
| `visszavonva` | **te vontad vissza**, még azelőtt, hogy a vezérlő átvette volna | semmi — bizonylat nem készült és nem is fog (4.3) |
| `lejart` | a vezérlő a **határidőig nem vette át** — bizonylat nem készült, **és nem is fog** | ha még kell a bizonylat: add le újra, **ÚJ `kulsoAzonosito`-val** (6.1) |

Mind az öt — `kesz`, `hiba`, `bizonytalan`, `visszavonva`, `lejart` — **végállapot**: a
szerver a lezárt feladatot nem írja felül. Ha a lekérdezés ezek egyikét adja,
abbahagyhatod a pollozást.

### 6.1 A szerveroldali időzár — a `var` és az `atvette` magától lejár

A szerver **nem** hagyja a feladatokat örökké állni. Két külön időzár van, mert a két
állapot kockázata nem egyforma:

| Honnan | Mikor | Mire | Nyomtatódott? |
|---|---|---|---|
| `var` | **2 óra** a leadás után | `lejart` | **biztosan nem** — a vezérlő át sem vette |
| `atvette` | a vezérlő azóta **jelentkezett**, de erről a feladatról **15 perce** nem beszél; vagy eltelt **24 óra** | `bizonytalan`, `eredmeny: null` | **nem tudható** |

A határok a szerver `config.php`-jából állíthatók; a fentiek az alapértelmezések.

**A `var` lejárata a te érdekedet védi.** Enélkül egy pénteken leadott, elfelejtett
nyugta hétfőn kijön a nyomtatóból, amikor a vezérlő végre elindul — a vevő addigra régen
elment, a bizonylat viszont adóügyi, tehát nem vonható vissza. A `lejart` feladat
**soha nem kerül ki a vezérlőhöz**.

**Az `atvette` lejárata nem óra-alapú**, hanem azt kérdezi meg, ami tényleg érdekes:
*él-e a vezérlő, és mégsem beszél erről a feladatról?* Ezért egy újrainduló vezérlő
(reboot, frissítés) **nem** okoz téves `bizonytalan`-t: amíg nem jelentkezik, a feladat
érintetlen marad.

**A VEZÉRLŐ 1.3.0-TÓL MEGELŐZI EZT AZ IDŐZÁRAT, ha tudja.** A kiadott feladatot a
vezérlő azonnal lemezre teszi, és nyilvántartja, hogy el is KEZDTE-e. Ha egy átvett
feladatot **tíz percen belül** nem tudott elkezdeni — jellemzően mert közben megszakadt a
pénztárgép-kapcsolata —, akkor magától lezárja `hiba` állapotba
(`hibakod: el_sem_kezdtuk`), még a szerver türelmi ideje előtt. Ez a hívónak **jobb
válasz**, mint a `bizonytalan`: abból csak annyi derül ki, hogy *nem tudható*, ebből
viszont az, hogy a portra **egyetlen bájt sem ment ki**, tehát a bizonylat biztosan nem
készült el, és a feladat újraküldhető (7.5).

> A `bizonytalan` így arra az esetre szűkül, ami valóban kétséges: a vezérlő a bizonylat
> NYOMTATÁSA közben állt le. Egy régebbi (1.2.0 vagy korábbi) vezérlőnél viszont az
> eredeti viselkedés él, mert ott a megkülönböztetés nem létezik.

> ⚠ **A lejárt feladat KULCSA el van használva.** Ha ugyanazzal a `kulsoAzonosito`-val
> küldöd újra, az idempotencia miatt a **`lejart` feladatot kapod vissza**
> (`200`, `ismetelt: true`), és **nem készül bizonylat**. Ez szándékos: a kulcs
> feladása azt jelentené, hogy egy késői duplikátum mégis létre tudna hozni egy második
> feladatot. **Lejárat után tehát ÚJ kulcs kell.**

#### Mit jelent az üres `eredmeny` a `bizonytalan` mellett

Ez a mező **megmondja, ki adta fel**, és ez a hívónak is hasznos:

| `allapot` | `eredmeny` | Mi történt |
|---|---|---|
| `bizonytalan` | **`null`** | a **szerver** adta fel a várakozást: a vezérlő elvitte a feladatot, és soha nem jelentett róla |
| `bizonytalan` | kitöltve | a **vezérlő** jelentette, hogy a feldolgozás félbeszakadt |

A teendő mindkettőnél ugyanaz (7.6): **ember ellenőrizze a szalagot.** A különbség
annyi, hogy az első esetben a vezérlő egy későbbi jelentkezéskor **még felülírhatja** az
állapotot a valódi eredménnyel — tehát ha egy `bizonytalan` feladat később `kesz`-re
váltott a bizonylatszámmal, az nem hiba, hanem a helyes állapot megérkezése.

---

## 7. Logikai használat — hogyan építsd be

Az API maga egyszerű; a helyes **hívási minta** az, ami megvéd a duplán kinyomtatott
nyugtától.

### 7.1 Az alapszabály

> **A nyugta leadása egy állapotváltás a te adatbázisodban, nem egy HTTP hívás
> mellékhatása.**

Soha ne úgy építsd, hogy a webkérés megvárja a nyomtatást. Három ok:

1. A vezérlő 2–5 másodpercenként néz rá a szerverre — a felhasználó ennyit vár a
   képernyő előtt hiába.
2. Ha a pénztárgép épp nem elérhető, a várakozás **percekig vagy tovább** tarthat.
3. Ha a webkérés időtúllépéssel elszáll, **te nem tudod, mi lett a nyugtával** — a
   feladat viszont már a szerveren van, és ki fog nyomtatódni.

### 7.2 Az adatmodell, amit ajánlunk

A rendelés (vagy amit nyomtatsz) mellé legalább ennyi:

| Mező | Mire |
|---|---|
| `nyugta_feladat_id` | a szervertől kapott UUID — **ez a horgony** |
| (a rendelés saját azonosítója) | ezt küldd `kulsoAzonosito`-ként — nem kell új mező, már megvan |
| `nyugta_allapot` | `leadva` / `var` / `atvette` / `kesz` / `hiba` / `bizonytalan` / `visszavonva` / `lejart` / `leadas_bizonytalan` |
| `nyugta_leadva` | időbélyeg — ebből számolod, mióta áll |
| `nyugtaszam` | a `kesz` eredményből (`0012/00034`) |
| `nyugta_hibakod`, `nyugta_hibauzenet` | a `hiba` eredményből |
| `nyugta_ellenorzendo` | jelző az embernek — `bizonytalan` és `leadas_bizonytalan` esetén |

A `leadas_bizonytalan` a 7.3 pont esete: a POST hálózati hibával szállt el, és **nem
tudható, felvette-e a szerver**.

### 7.3 A leadás helyes kezelése

**A `kulsoAzonosito` nélkül a leadás nem idempotens, ezért az első dolgod az legyen,
hogy megadod.** Az alábbi ág két változatban létezik, és a különbség az, hogy a hálózati
hiba után van-e mit tenned.

```
tranzakció:
    ha a rendeléshez már van nyugta_feladat_id  →  KILÉPÉS (nem adjuk le kétszer)
    jelöld meg a rendelést: nyugta_allapot = 'leadas_folyamatban'
commit

POST /api/v1/nyugta     { …, "kulsoAzonosito": rendeles.id }
    ├─ 202  → mentsd el a feladatId-t, nyugta_allapot = 'var'
    ├─ 200 + ismetelt:true  → ugyanaz: a feladat MÁR megvolt, mentsd el a feladatId-t
    ├─ 409 kulso_azonosito_utkozes  → PROGRAMHIBA nálad: ezt a kulcsot már más
    │          tartalomhoz használtad. Ne küldd újra; nézd meg, miért változott
    │          a tartalom ugyanarra a rendelésre.
    ├─ 4xx  → a szerver ELUTASÍTOTTA: semmi nem jött létre.
    │          A hiba javítható (rossz adat, hiányzó terminál) → javíts és küldd újra.
    │          A 402 üzleti kérdés → jelezd az üzemeltetőnek, ne pörgesd újra.
    └─ hálózati hiba / időtúllépés
               kulccsal:    KÜLDD ÚJRA - a kulcs megvédi a második bizonylattól
               kulcs nélkül: ⚠ NEM TUDHATÓ, felvette-e a szerver
                             nyugta_allapot = 'leadas_bizonytalan'
                             naplózz, riassz - és NE KÜLDD ÚJRA AUTOMATIKUSAN
```

**Miért nem szabad kulcs nélkül újraküldeni?** Mert a `feladatId`-t a szerver adja,
vagyis **a feladat azonosságát maga a kérés hozza létre**: két POST két feladat, és a
vezérlő duplikátumvédelme (ami a `feladatId`-ra megy) a kettőt két legitim munkának
látja. Ha az első kérés eljutott a szerverig és csak a válasz veszett el, a vak
újraküldés **második nyugtát nyomtat** — amit adóügyi bizonylatnál nem lehet
visszavonni.

Ez a kockázat nem elvi: a várólista-keretrendszerek (Laravel queue, Sidekiq, cron) és
egyes HTTP-kliensek **alapbeállításként újrapróbálnak** hálózati hibánál, anélkül hogy a
kódod döntött volna róla. Egy kétszer kattintott gomb ugyanez a hibafajta.

**Mit tegyél kulcs nélkül?** Jelöld meg a rendelést, és bízd emberre. A tisztázás ma
költséges: nincs listázó végpont, a lekérdezéshez pedig épp az az azonosító kellene,
amit nem kaptál meg — marad az admin *Feladatok* lapja, időpont alapján. **Ezért érdemes
a kulcsot mindig megadni.**

> A mellékelt PHP kliens a kulcs nélküli esetet kikényszeríti: időtúllépésnél nem
> ismétel, hanem `halozati_hiba` kóddal dob kivételt, és a
> `Nyugtaszerver::utolsoHivas()` — amit még a küldés **előtt** eltesz — megmondja, mi
> indult el.

**Amit a kulcs NEM old meg:** a kétszer leadott, de szándékosan **két külön** rendelés
(két vásárlás ugyanazzal a tartalommal) továbbra is két nyugta — ahogy kell. A kulcs
nem a tartalmat, hanem a **te azonosítódat** veti össze.

### 7.4 A lekérdezés ütemezése

A pollozás **háttérfeladat** dolga (cron, queue worker, ütemezett job), **nem** a
webkérésé.

> **Ha be van állítva a webhook (4/a), ezt az ütemet jóval ritkábbra veheted** — de ne
> hagyd el: a webhookot egy óra után feladjuk, és az azon kimaradt feladatokról csak a
> lekérdezés tud. Egy ritka egyeztető kör a nyitott feladataidra elég.

Ajánlott ütem egy nyugtára:

| Idő a leadás óta | Lekérdezés gyakorisága |
|---|---|
| 0–30 másodperc | 2–3 másodpercenként (ha a felhasználó a képernyő előtt vár) |
| 30 mp – 5 perc | 15–30 másodpercenként |
| 5 perc felett | percenként, és **riassz** |

**Leállási feltételek** (mindegyik kötelező):

1. `allapot` ∈ {`kesz`, `hiba`, `bizonytalan`, `visszavonva`, `lejart`} → kész, ne kérdezd többet.
2. Elérted a türelmi határt (javaslat: **2 perc**) és az állapot még `var` →
   **valami áll**. Ne küldd újra a feladatot; értesíts. A tipikus okok:
   - a vezérlő nem fut az ügyfél gépén;
   - a pénztárgép nem elérhető (kihúzott USB-soros átalakító, elaltatott gép) — ilyenkor
     a vezérlő **meg sem szólítja a szervert**, hogy ne vegyen át olyan feladatot,
     amit nem tud kinyomtatni;
   - lejárt a bérlet (előfizetés) — a szerver nem ad ki feladatot.
   Mindhárom esetben a feladat **megmarad**, és a helyzet rendezése után **magától
   lefut** — de csak a lejáratig (6.1: alapértelmezésben **2 óra**). Azután a szerver
   `lejart`-tá zárja, és akkor sem nyomtatódik ki, ha a bolt később helyreáll.
3. A feladat `atvette` állapotban áll **percek óta** → a vezérlő valószínűleg leállt
   nyomtatás közben. **Ne küldd újra.** Vagy a vezérlő tölti fel rá a `bizonytalan`
   eredményt a következő indulásakor, vagy — ha a vezérlő él, de erről a feladatról
   hallgat — a **szerver** zárja le `bizonytalan`-ra, üres `eredmeny`-nyel (6.1).
4. `allapot` = `lejart` → a szerver lezárta, mert a vezérlő nem vette át a határidőig.
   Bizonylat nem készült. Ha még kell: **új `kulsoAzonosito`-val** add le újra.

A 2. pontnál van egy harmadik lehetőség is: ha a bizonylat **már nem kell** (a vevő
eltávozott, a rendelés törlődött), akkor a `var` állapotban álló feladatot **vissza is
vonhatod** a 4.3 pont végpontjával — utána azonnal, a lejáratot meg sem várva biztos,
hogy nem nyomtatódik ki. Érdemes beépíteni: a szerveroldali időzár a **végső** védelem,
nem az első; két órán belül a vezérlő még bármikor elvihetné a feladatot.

**Ne pollozz feladatonként külön hívással, ha sok van.** Kötegelt lekérdezés ma nincs
(ez ismert korlát); ha egyszerre sok nyugtád van függőben, a háttérfeladat menjen végig
a nyitott sorokon egy körben, a fenti ütem szerint ritkítva.

### 7.5 Mi küldhető újra és mi nem — döntési tábla

A kérdés mindig ugyanaz: **kiment-e valami a soros portra?** Ha nem, a bizonylat
biztosan nem készült el, tehát bátran újraküldhető. Ha igen, akkor a kimenetel
bizonytalan, és **új feladatot** kell leadni (soha nem „ugyanazt még egyszer" — a webes
úton amúgy is minden POST új `feladatId`-t kap).

| Végállapot / `hibakod` | Kiment a portra? | Teendő |
|---|---|---|
| `hiba`, `hibakod: penztargep_allapot` | **nem** — a vezérlő a küldés előtt lekérdezte a gép állapotát, és nem volt értelme kiküldeni (zárt nap, nyitva maradt bizonylat) | rendezd a gépet (`napnyitas`, `megszakitas`), majd **add le újra ugyanazt a nyugtát** |
| `hiba`, `hibakod: penztargep_nem_valaszol` | **nem** — a gép az ártalmatlan állapotlekérdezésre sem válaszolt | ellenőriztesd a kábelt/portot, majd **add le újra** |
| `hiba`, `hibakod: nincs_engedely` | **nem** — az engedélykapu a foglalás előtt fut | rendezd az előfizetést (vagy a *Számla* kapcsolót), majd **add le újra** |
| `hiba`, `hibakod: el_sem_kezdtuk` | **nem** — a vezérlő átvette a feladatot, de tíz percen belül nem tudta elkezdeni (nem volt pénztárgép-kapcsolat), ezért egyetlen bájt sem ment ki | **add le újra** — ez a `bizonytalan` helyett kapott, PONTOSABB válasz (6.1) |
| `hiba`, `hibakod: null`, az üzenet ellenőrzési hibáról szól (tiltott karakter, alulfizetés, hosszkorlát, érték nem egyezik) | **nem** — a validáció a küldés előtt van | javítsd az adatot, és **add le újra** |
| `hiba`, a `hibakod` a **pénztárgép saját kódja** (pl. `040B`, `0310`, `8334`) | **IGEN** | ⚠ A gép már kapott parancsot, és egy megszakított bizonylat is elhasznál egy bizonylatszámot. **Ellenőriztesd a szalagot**, mielőtt új nyugtát adsz le |
| `bizonytalan` | **nem tudható** | ember dönt (7.6) |
| `visszavonva` | **nem** — a vezérlő soha nem vette át | semmi nem nyomtatódott. Ha mégis kell a bizonylat, add le **új feladatként** |
| `lejart` | **nem** — a vezérlő a határidőig át sem vette (6.1) | semmi nem nyomtatódott, és nem is fog. Ha mégis kell a bizonylat, add le újra **ÚJ `kulsoAzonosito`-val** — a régi kulcs el van használva |
| `kesz` | igen, sikerrel | kész |

A vezérlő oldalán a szabály így hangzik: *a hibás kísérlet is elhasznál egy
azonosítót*. Ezért a vezérlő a hibaágon **szándékosan nem oldja fel** a foglalást — és
ezért nem szabad neked sem automatikusan újraküldeni azt, ami már a gépnél járt.

### 7.6 A `bizonytalan` állapot

Kétféleképpen keletkezik, és a teendő **mindkettőnél ugyanaz**:

- a vezérlő átvette a feladatot, elkezdte, és **közben leállt** (a program kilépett, a
  gép újraindult) — ilyenkor ő maga jelenti, és az `eredmeny` **ki van töltve**;
- a vezérlő átvette, **soha nem jelentett** róla, de közben bejelentkezett a szerverre
  (tehát él) — ilyenkor a **szerver** zárja le, és az `eredmeny` **`null`** (6.1).

> **Ami 1.3.0-tól NEM kerül ide:** az a feladat, amit a vezérlő átvett, de **el sem
> kezdett**. Arról ő maga jelenti, hogy nem nyomtatódott
> (`hiba`, `hibakod: el_sem_kezdtuk`), tehát nem kell hozzá ember — egyszerűen
> újraküldhető. Ez korábban az első két eset egyikébe esett, holtbizonyosan tévesen.

Egyik esetben sem állapítható meg, kijött-e a papír. Ezt online senki nem tudhatja —
csak a pénztárgép szalagja.

**Amit tenned kell:**

- jelöld meg a rendelést emberi ellenőrzésre, és **állítsd le rá minden automatizmust**;
- ha az `eredmeny` ki van töltve, a döntéshez a vezérlő ad támpontot a
  `hibaUzenet` mezőben: ha az újrainduló vezérlő le tudta kérdezni a gépet, az üzenet a
  pénztárgép **utolsó bizonylatát** (szám, összeg, idő) is megnevezi — ebből az ügyfél a
  szalagról el tudja dönteni, elkészült-e; ha nem tudta lekérdezni, az üzenet arra
  utasít, hogy a gépen kell megnézni;
- ha az `eredmeny` **`null`** (a szerver adta fel), nincs ilyen támpont: ott a
  `letrehozva` időpontja és a bolt szalagja a kiindulás. Ez az eset jellemzően azt
  jelenti, hogy a bolti gépet **lecserélték vagy újratelepítették**, miközben egy
  átvett feladat még a régi gépen volt;
- ha **nem** készült el: adj le **új** feladatot (új `feladatId`);
- ha elkészült: kösd rá a rendelésre a bizonylatszámot kézzel.

**Amit soha ne tegyél:** automatikus újraküldés. Ez a rendszer egyik alapszabálya, és
nem óvatoskodás — a duplán kiadott adóügyi bizonylat nem vonható vissza.

### 7.7 A visszavonás beépítése

A visszavonás (4.3 pont) **nem a hibakezelés része, hanem a normál üzletmenet**: a vevő
elállt, a rendelést törölték, a pénztáros rossz kasszára adta le. Ha nincs beépítve, egy
ilyen feladat **ott marad a szerveren**, és ha a vezérlő a lejárat előtt elindul,
**kinyomtatja**. Ez a leggyakoribb kellemetlen meglepetés, és egyetlen hívással
elkerülhető.

A szerveroldali időzár (6.1) ezt **nem váltja ki**, csak a legrosszabb esetet zárja ki: a
2 óra arra van, hogy egy elfelejtett nyugta ne jöjjön ki napokkal később. Az első két
órában a feladat él, tehát aki a törlésnél nem von vissza, az továbbra is kaphat egy nem
kért bizonylatot.

**Hová tedd:** oda, ahol a rendelés törlődik vagy a fizetés meghiúsul. Ha a rendeléshez
tartozik `nyugta_feladat_id`, és az állapota még nem végállapot, próbáld meg visszavonni.

```
függvény rendelesTorlese(rendeles):
    ha nincs rendeles.nyugta_feladat_id:        kilépés
    ha rendeles.nyugta_allapot végállapot:      kilépés

    próbáld:
        v = POST(alap_url + "/api/v1/nyugta/" + rendeles.nyugta_feladat_id + "/visszavonas")
        rendeles.nyugta_allapot = v.allapot               # "visszavonva"
    elkapva szerverhiba:
        ha kod == "mar_atvette":    figyeld tovább — talán épp nyomtat
        ha kod == "mar_elkeszult":  a bizonylat KÉSZ → sztorno feladatot kell leadni
        ha kod == "mar_lezart":     nincs teendő, a feladat már lezárult
    elkapva hálózati_hiba:
        tedd újrapróbálandó sorba                        # EZT SZABAD ismételni
```

**A hálózati hibát itt — a leadással ellentétben — bátran újrapróbálhatod**, mert a
visszavonás idempotens. A két hívás ellentétes jellege érdemes, hogy a kódban is
látszódjon: a leadás körüli hibaág riaszt és megáll, a visszavonás körüli újrapróbál.

**A `mar_elkeszult` nem programhiba, hanem üzleti helyzet.** A bizonylat kijött a
nyomtatóból; innentől a rendezés útja egy `sztorno` feladat, az eredeti bizonylat
számával és dátumával — mindkettő ott van a `kesz` feladat `eredmeny` mezőjében
(`nyugtaszam`, `datum`). Ezt érdemes a törlési folyamatba is bekötni, különben a papír
és a nyilvántartás elválik egymástól.

### 7.8 Több pénztárgép

Ha az ügyfélnek több pénztárgépe van, a **te alkalmazásodnak kell tudnia**, melyik
kasszához tartozik a tranzakció. A szerver **nem talál ki semmit**: több terminálnál és
alapértelmezett hiányában `400 terminal_kotelezo` a válasz.

Ajánlás: tárold az AP számot a bolt/pénztár/telephely rekordodon, és mindig add meg
kifejezetten a `terminal` mezőben — akkor is, ha ma csak egy gép van. Így egy második
gép üzembe helyezése nem igényel kódváltoztatást.

### 7.9 Az üzemi nap

**Nyugta csak nyitott adóügyi nappal készíthető.** Ha a nap zárva van, minden nyomtatás
`penztargep_allapot` hibával áll meg (a bizonylat **nem** vész el: az azonosító szabad
marad, a feladat újraküldhető).

Két járható út:

1. **A kezelő nyitja a napot a pénztárgépen** — ez a szokásos. Az alkalmazásod ilyenkor
   csak a `penztargep_allapot` hibát kezeli értelmesen: „a pénztárgépen nincs nyitva a
   nap".
2. **Az alkalmazásod adja le a `napnyitas` feladatot** a nap első nyugtája előtt.
   Ilyenkor előtte érdemes egy `statusz` feladattal megnézni a `napNyitva` mezőt — a
   `statusz` nem nyomtat és szabadon ismételhető.

A napi zárást (`napizaras`) **ne automatizáld óvatlanul**: valódi adóügyi bizonylat
keletkezik belőle, és a napi számlálókat nullázza.

### 7.10 Amit ne csinálj

| Antipattern | Miért baj |
|---|---|
| a POST hálózati hibája után automatikus újraküldés | **második nyugtát nyomtathat**, amit nem lehet visszavonni |
| a webkérés megvárja a nyomtatást | másodpercekig vagy percekig blokkol, és időtúllépésnél elveszíted a kimenetelt |
| `statusz` feladat másodpercenként | a soros vonalat terheled; a vezérlő amúgy is 15 másodpercenként magától lekérdezi a gép állapotát |
| a leadás gátlása az `online` vagy az `eletjel` mezőre | két életjel között elavulhat, és a feladat akkor sem veszik el, ha a vezérlő épp nem elérhető (4.4) |
| a `bizonytalan` automatikus újraküldése | pontosan az az eset, amit emberre kell bízni |
| a már nem kellő feladatot nem vonod vissza, csak „elfelejted" | ott marad a szerveren, és ha a vezérlő a lejárat előtt elindul, **kinyomtatódik** (7.7). Az időzár (6.1) csak a legrosszabb esetet zárja ki, az első két órában a feladat él |
| a `kesz` feladat visszavonásával próbálni érvénytelenné tenni a bizonylatot | nem megy, és nem is mehet: adóügyi bizonylatot csak `sztorno` javít |
| a `hiba` szöveges üzenetére ágazni | az üzenetek magyar, ember-olvasásra szánt szövegek, és változhatnak — a `kod`/`hibakod` a stabil felület |
| felhasználói szöveget szűrés nélkül `arunev`-be | a `\` és a `\|` tiltott karakter, a 99 karakter fölötti név elutasításra kerül |
| a rendelésszám az `azonosito` mezőbe „mert az az azonosító" | a gép **rányomtatja**, és tiltott szónál megszakítja a nyugtát |

---

## 8. Hibakódok — teljes lista

### 8.1 A szerver hibái (HTTP 4xx/5xx, `{"hiba":{"kod":…}}`)

| `kod` | HTTP | Jelentés |
|---|---|---|
| `nincs_token` | 401 | hiányzik az `Authorization: Bearer` fejléc |
| `ervenytelen_kulcs` | 401 | ismeretlen vagy visszavont API kulcs |
| `ures_keres` | 400 | üres kérés-törzs |
| `hibas_json` | 400 | a törzs nem értelmezhető JSON objektum |
| `hibas_payload` | 400 | a feladat nem alakítható vissza JSON-ná |
| `hibas_kulso_azonosito` | 400 | a `kulsoAzonosito` nem szöveg/szám, vagy 100 karakternél hosszabb |
| `terminal_kotelezo` | 400 | több pénztárgép van, meg kell adni, melyikre menjen |
| `nincs_elofizetes` | 402 | a terminál előfizetése nem aktív |
| `lejart_elofizetes` | 402 | az előfizetés lejárt |
| `ismeretlen_terminal` | 404 | nincs ilyen AP számú vagy nevű terminál ennél az ügyfélnél |
| `ismeretlen_feladat` | 404 | nincs ilyen feladat (vagy nem a te ügyfeledé) |
| `ismeretlen_vegpont` | 404 | rossz útvonal vagy HTTP metódus |
| `mar_atvette` | 409 | **visszavonás:** a vezérlő már elvitte a feladatot, lehet, hogy nyomtatja |
| `mar_elkeszult` | 409 | **visszavonás:** a bizonylat elkészült — csak `sztorno` javítja |
| `mar_lezart` | 409 | **visszavonás:** a feladat `hiba`, `bizonytalan` vagy `lejart` állapotban lezárult |
| `kulso_azonosito_utkozes` | 409 | **leadás:** ugyanazt a kulcsot más tartalommal használtad |
| `nincs_terminal` | 409 | az ügyfélhez nincs aktivált pénztárgép |
| `tul_nagy` | 413 | a feladat nagyobb 256 KB-nál |
| `sema_nincs_telepitve` | 503 | a szerver telepítése nem fejeződött be |
| `sema_frissites_kell` | 503 | a `kulsoAzonosito` oszlopa még nincs meg az adatbázisban — a kulcs nélküli leadás közben is működik |
| `szerverhiba` | 500 | váratlan hiba; a részlet a szerver hibanaplójában |

### 8.2 A vezérlő hibái (az `eredmeny.hibakod` mezőben)

| `hibakod` | Kiment a portra? | Jelentés |
|---|---|---|
| `penztargep_allapot` | nem | zárt nap, vagy a gépen nyitva maradt egy bizonylat |
| `penztargep_nem_valaszol` | nem | a gép az állapotlekérdezésre sem válaszolt |
| `nincs_engedely` | nem | nincs érvényes bérlet (előfizetés), vagy a típus nincs engedélyezve (pl. `szamla`) |
| `el_sem_kezdtuk` | nem | a vezérlő átvette a feladatot, de a pénztárgép tíz percen belül nem volt elérhető, ezért **el sem kezdte** kinyomtatni (vezérlő 1.3.0-tól) |
| `null`, ellenőrzési üzenettel | nem | a küldés előtti validáció fogta meg |
| a pénztárgép saját kódja (`040B`, `0310`, `8334`, `045F`, …) | **igen** | a gép utasította el vagy szakította meg a bizonylatot |

A pénztárgép hibáinál az `eredmeny.hibaUzenet` **a gép saját magyar szövegével
kezdődik** (pl. „Helytelen kulcs pozíció, nem eladás üzemmód!"), a kód a végén
zárójelben áll — ezt a szöveget lehet a kezelőnek megmutatni.

### 8.3 Hálózati hiba

Nem a szervertől jön, ezért nincs `kod`-ja. **A POST-nál külön eset:** ilyenkor nem
tudható, felvette-e a szerver a feladatot (7.3). A GET-nél ártalmatlan: nyugodtan
ismételhető.

---

## 9. Próba és üzembe helyezés — lépésről lépésre

1. **`GET /api/v1/terminalok`.** Nem nyomtat semmit. Ha JSON választ ad, jó az URL-alak
   és a kulcs. Ha üres a lista: nincs aktivált pénztárgép — a vezérlőt kell aktiválni.
2. **Egy `statusz` feladat.** Végigmegy a **teljes láncon** (szerver → vezérlő →
   pénztárgép → vissza), de nem nyomtat. Ez az egyetlen ártalmatlan végpróba.
   ```json
   { "tipus": "statusz" }
   ```
   Ha `kesz` lesz, és az `eredmeny.allapot.apSzam` a valódi AP szám, akkor a lánc él.
3. **Egy `nyugta`, kis összeggel.** Innentől **valódi adóügyi bizonylat** keletkezik,
   amit nem lehet visszavonni. Az ügyfél tudjon róla.
4. **A hibaágak kipróbálása** — ezeket érdemes szándékosan előidézni, mert élesben
   úgyis elő fognak fordulni:
   - zárt nappal küldött nyugta → `penztargep_allapot`;
   - kihúzott soros átalakító → a feladat `var` állapotban vár, és a visszadugás után
     magától lefut (ha a lejáraton belül visszadugták — lásd 6.1);
   - hibás fizetési sorrend (készpénz nem utolsó) → a vezérlő elutasítja, semmi nem
     megy ki a portra.
5. **Egy visszavonás.** Adj le egy nyugtát, és **mielőtt a vezérlő átvenné** (van rá
   néhány másodperc), vond vissza. Ez sem nyomtat semmit, viszont igazolja, hogy a
   törlési ágad működik. Utána kérdezd le: `visszavonva` kell lennie.
6. **Egy ismételt leadás.** Adj le egy `statusz` feladatot `kulsoAzonosito`-val, majd
   add le PONTOSAN ugyanazt még egyszer. A második válasz `200` és `ismetelt: true`,
   ugyanazzal a `feladatId`-vel. Ez sem nyomtat semmit, viszont igazolja a legfontosabb
   védelmet.
7. **A `lejart` ág kezelése.** Ezt szándékosan nehéz előidézni (két óra), de a kódodnak
   fel kell rá készülnie: a `lejart` **végállapot**, és a hozzá tartozó kulcs el van
   használva (6.1). A legegyszerűbb próba az, hogy a lekérdezés feldolgozó ágába felvett
   `lejart` eset tényleg lefut-e — ne az `ismeretlen állapot` ágra essen.

**Fejlesztéshez:** a vezérlőben van **szimulátor mód**, amivel a teljes lánc valódi
pénztárgép nélkül kipróbálható. Ha van saját fejlesztői nyugtaszervered, ott is
használható — csak arra ügyelj, hogy éles szerverre ne küldj próbafeladatot.

---

## 10. Korlátok és ismert hiányok

Ezeket ismerd, mielőtt tervezel:

| Korlát | Részlet |
|---|---|
| **A leadás csak `kulsoAzonosito`-val idempotens** | a mező nélkül egy válasz nélkül elszálló kérés után nem tudható, létrejött-e a feladat, és a vak újraküldés második nyugtát nyomtat. **Mindig adj kulcsot** (4.1, 7.3) |
| **Nincs kötegelt lekérdezés** | feladatonként egy `GET`; sok függő feladatnál ritkítsd az ütemet |
| **A webhook egy óra után feladja** | hat próba kb. 53 perc alatt; utána a feladat állapotát csak lekérdezéssel tudod meg (4/a) — ezért a ritka egyeztető kör nem elhagyható |
| **A webhook 0–60 másodpercet késik** | cronból megy, nem a kérésben. Ha a felhasználó a képernyő előtt vár, a pollozás gyorsabb (4/a) |
| **Eseményre szűrés nincs** | aki beállít webhookot, minden végállapotról kap értesítést |
| **Nincs listázó végpont** | a saját feladataid listáját neked kell nyilvántartanod (ezért horgony a `feladatId`) |
| **A visszavonás csak `var` állapotban megy** | onnantól, hogy a vezérlő átvette (`atvette`), nincs csatorna, amin visszaszólhatnánk neki — a `409 mar_atvette` az őszinte válasz. Egy elkészült bizonylatot pedig csak `sztorno` javít (4.3) |
| **Az admin felület Feladatok lapja csak olvas** | onnan ma nem lehet feladatot visszavonni vagy újraküldeni; a visszavonás az API-n megy |
| **Egy jelentkezés legfeljebb 10 feladatot ad ki** | FIFO sorrendben (`letrehozva` szerint); a többi a következő körben megy. Nagy löketnél ez lépcsőzetes átvételt jelent |
| **Az életjel két életjel között elavulhat** | az `eletjel` a legutóbbi jelentkezést tükrözi, nem a mostani pillanatot — üzemeltetői kijelzésre jó, a leadás kapujának nem (4.4) |
| **A lejárt feladat kulcsa el van használva** | a szerveroldali időzár (6.1) lezárja a régóta álló feladatot, de a `kulsoAzonosito`-t NEM szabadítja fel — újraküldéshez új kulcs kell |
| **A `statusz` is előfizetéshez kötött a webes úton** | a fájl alapú úton nem — ott a `statusz` és a `megszakitas` mentesül |

---

## 11. Integrációs recept — röviden

> A fentiek tömör összefoglalója — akkor hasznos, ha a beépítést más végzi, vagy ha a
> lényeg egy helyen kell.

**PHP alkalmazásba** ne írj új klienst: emeld át a `src/Php_Kliens/kliens/` mappát
(három fájl, nincs Composer-függőség, PHP 7.4+). Három `require`, egy `new
Nyugtaszerver(...)`, és a `Feladat::` építők adják a helyes payloadot **a küldés előtti
ellenőrzéssel együtt**.

**Más nyelvben** a minimális megvalósítás:

```
függvény nyugtaLeadasa(rendeles):
    ha rendeles.nyugta_feladat_id létezik: kilépés          # egyszer és csak egyszer
    torzs = { tipus: "nyugta", terminal: rendeles.bolt.ap_szam,
              kulsoAzonosito: rendeles.id,                  # EZ véd a kettőzött nyugtától
              tetelek: [...], fizetesek: [...] }            # készpénz UTOLSÓNAK
    ellenoriz(torzs)                                        # tiltott karakter, hossz, fedezet
    próbáld:
        valasz = POST(alap_url + "/api/v1/nyugta", torzs, bearer=api_kulcs)
    elkapva hálózati_hiba:
        kulccsal:     küldd újra — a szerver nem hoz létre második feladatot
        kulcs nélkül: rendeles.nyugta_allapot = "leadas_bizonytalan"; riaszt(); kilépés
    ha valasz.http >= 400:
        kezeld a valasz.hiba.kod szerint                     # 402 = üzleti, nem programhiba
        kilépés
    rendeles.nyugta_feladat_id = valasz.feladatId    # 200 + ismetelt:true esetén is
    rendeles.nyugta_allapot = valasz.allapot

függvény nyugtaEllenorzese(rendeles):                        # háttérfeladatból
    v = GET(alap_url + "/api/v1/nyugta/" + rendeles.nyugta_feladat_id, bearer=api_kulcs)
    rendeles.nyugta_allapot = v.allapot
    ha v.allapot == "kesz":         rendeles.nyugtaszam = v.eredmeny.nyugtaszam
    ha v.allapot == "hiba":         dontsd el a v.eredmeny.hibakod alapján (7.5 tábla)
    ha v.allapot == "bizonytalan":  rendeles.ellenorzendo = igaz; riaszt()   # SOHA ne ismételd
    ha v.allapot == "visszavonva":  nincs teendő — bizonylat nem készült
    ha v.allapot == "lejart":       bizonylat nem készült. Ha kell: ÚJ kulccsal add le újra

függvény webhookFogadasa(nyers, fejlec):                     # ha be van állítva (4/a)
    ellenoriz az alairast a NYERS torzsre, hash_equals-szel  # 1.
    ellenoriz az idoablakot (5 perc)                         # 2.
    ha mar lattam az esemenyId-t:  valasz 200; kilépés        # 3.
    sorba teszem a feldolgozast; valasz 200 AZONNAL
    # A lassu feldolgozas idotullepest, az pedig UJRAKULDEST eredményez.
    ha v.allapot == "var" és eltelt > 2 perc:  riaszt()      # de NE küldd újra

függvény nyugtaVisszavonasa(rendeles):                       # a rendelés törlésekor
    POST(alap_url + "/api/v1/nyugta/" + rendeles.nyugta_feladat_id + "/visszavonas")
    # 200 → "visszavonva". 409 mar_atvette / mar_elkeszult / mar_lezart → lásd 7.7.
    # Hálózati hibánál ISMÉTELHETŐ: ez a hívás idempotens.
```

**A hét szabály, amit nem szabad megsérteni:**

1. A POST hálózati hibája után **nincs automatikus újraküldés**.
2. A `bizonytalan` eredmény után **nincs automatikus újraküldés**.
3. A készpénz **az utolsó fizetés**.
4. A `\` és a `|` **nem kerülhet** semmilyen szövegmezőbe.
5. Az `azonosito` mező **a papírra kerül** — csak semleges szöveget tegyél bele.
6. A már nem kellő feladatot **vond vissza**, ne csak felejtsd el — a lejáratig
   (alapértelmezésben 2 óra) a vezérlő bármikor elviheti és kinyomtathatja.
7. **Mindig küldj `kulsoAzonosito`-t**, ahol van saját azonosítód — ez az egyetlen
   védelem a hálózati hiba után kettőződő bizonylat ellen.

**Ellenőrző parancsok** (a repó gyökeréből):

```bash
php src/Php_Kliens/onproba.php
```

```bash
dotnet test
```
