Fejlesztői dokumentáció

Nyugtaszerver API

Végpontok, webhook, állapotok, hibakódok — és ami a legtöbbet ér: a hívási minták.

Tartalom

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

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}]}'
{ "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:

curl -H "Authorization: Bearer nyk_..." \
     "https://pelda.hu/nyugtaszerver/api/v1/nyugta/6c1f0c8e-...-9b2a"
{
  "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ésJellemző idő
POST /api/v1/nyugta visszatérnéhány tized másodperc
a vezérlő észreveszi a munkátkb. 2–5 másodperc (statikus jelzőfájl figyelése)
a bizonylat kinyomtatása1–3 másodperc (9600 baud)
az eredmény megjelenik az API-bana 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:

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:

AlakPéldaFeltétel
széphttps://pelda.hu/nyugtaszerver/api/v1/nyugtamod_rewrite
útvonalhttps://pelda.hu/nyugtaszerver/index.php/api/v1/nyugtaPATH_INFO
lekérdezéshttps://pelda.hu/nyugtaszerver/index.php?ut=/api/v1/nyugtamindig 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:

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:
{ "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 teljes mezőleírását), plusz két opcionális, a szervernek szóló mező: terminal és kulsoAzonosito.

{
  "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:

{ "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:

{ "feladatId": "6c1f0c8e-…", "allapot": "var", "terminal": "A31500001", "ismetelt": true }
Válasz
első leadás ezzel a kulccsal202, ismetelt nincs a válaszban
ismételt leadás, azonos tartalommal200, ismetelt: true, és ugyanaz a feladatId
ismételt leadás, más tartalommal409 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:

HelyzetMit tegyél
az ügyfélnek egy pénztárgépe vana terminal elhagyható
több pénztárgép, van közte alapértelmezettelhagyható → az alapértelmezettre megy
több pénztárgép, nincs alapértelmezettkö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:

HTTPkodMikorMit tegyél
401nincs_tokenhiányzik az Authorization fejlécjavítsd a kérést
401ervenytelen_kulcsismeretlen vagy visszavont kulcsúj kulcs kell az admintól
400ures_keres, hibas_jsona törzs nem JSON objektumjavítsd a kérést
400terminal_kotelezotöbb pénztárgép, nincs megadva melyikadd meg a terminal mezőt
400hibas_kulso_azonositoa kulsoAzonosito nem szöveg/szám, vagy 100 karakternél hosszabbjavítsd a kérést
409kulso_azonosito_utkozesugyanazt a kulcsot más tartalommal használtadha ez új bizonylat, adj új kulcsot; ha a korábbit ismételnéd, küldd ugyanazt a tartalmat
503sema_frissites_kella 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
404ismeretlen_terminalnincs ilyen AP számú/nevű terminál ennél az ügyfélnélellenőrizd az AP számot
409nincs_terminalaz ügyfélnek nincs aktivált pénztárgépea vezérlőt aktiválni kell
402nincs_elofizetes, lejart_elofizetesaz előfizetés nem aktív vagy lejártüzleti kérdés, nem programhiba — jelezd az üzemeltetőnek
413tul_nagya feladat > 256 KBne küldj ekkorát
503sema_nincs_telepitvea szerver telepítése félbemaradtüzemeltetői teendő
500szerverhibaváratlan hibaa 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.

{
  "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.

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:

{
  "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

ÁllapotHTTPkodMiért, és mit tegyél
atvette409mar_atvetteA 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
kesz409mar_elkeszultA 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
bizonytalan409mar_lezartNem tudható, kinyomtatódott-e. Emberi ellenőrzés kell a szalagról
hiba409mar_lezartLezárt állapot, nincs mit visszavonni
lejart409mar_lezartA 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

{
  "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
allapota terminál állapota a szerveren (aktiv / letiltott)
elofizetesaktiv / felfuggesztve / felmondva
ervenyesEddignull = határozatlan idejű
eletjelháromállapotú: rendben / akadaly / nem_jelentkezik
akadalyha a vezérlő akadályt jelez, annak magyar szövege; különben null
onlineaz eletjel kétállapotú összevonása: jelentkezett-e a várt időn belül
utolsoJelentkezesszerveridő
eletjelPercmilyen ütemben jelentkezik a vezérlő, percben. null = régi vezérlő

A három állapot

eletjelMit jelentMit tegyen az üzemeltető
rendbena vezérlő a várt ütemben jelentkezett, és eléri a pénztárgépetsemmit
akadalya 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_jelentkezika várt időn belül nem jelentkezett: a vezérlő nem fut, vagy nincs interneta 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

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…
{
  "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.

esemenyMikor
feladat.keszelkészült a bizonylat
feladat.hibanem készült el — a hibakod dönti el, újraküldhető-e (7.5)
feladat.bizonytalannem tudható, kinyomtatódott-e (7.6). Ha az eredmeny null, a szerver adta fel a várakozást (6.1)
feladat.visszavonvate vontad vissza
feladat.lejarta vezérlő a határidőig nem vette át (6.1)
webhook.probaaz ü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
$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óbaMikor (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

tipusMit csinálNyomtat?Ismételhető ugyanazzal az azonosítóval?
nyugtanyugtát nyomtat (ez az alapértelmezés, ha nincs tipus)igennem
sztornosztornó bizonylatigennem
visszaruvisszáru bizonylatigennem
szamlaegyszerűsített számlaigennem
penzmozgaspénz be / pénz kiigennem
napnyitasnapnyitásigennem
napizarasnapi forgalmi jelentésigennem
megszakitasa beragadt bizonylatot megszakítjanemigen
statuszlekérdezi a gép állapotátnemigen

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

{
  "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).

"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

{ "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-ben).

{ "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:

"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)
                                                  └─────────────┘
allapotJelentésMit tegyél
vara szerveren sorban áll, a vezérlő még nem vette átvárj; ha sokáig tart, riassz (7.4)
atvettea vezérlő dolgozik rajtavárj — másodpercek kérdése
keszelkészült, az eredmeny.nyugtaszam a bizonylatszámtárold el, kész
hibanem készült ela hibakód dönti el, újraküldhető-e (7.5)
bizonytalannem tudható, kinyomtatódott-e — vagy a vezérlő jelentette, hogy félbeszakadt, vagy a szerver adta fel a várakozástsoha ne próbáld újra automatikusan — emberi ellenőrzés (7.6)
visszavonvate vontad vissza, még azelőtt, hogy a vezérlő átvette volnasemmi — bizonylat nem készült és nem is fog (4.3)
lejarta vezérlő a határidőig nem vette át — bizonylat nem készült, és nem is fogha 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:

HonnanMikorMireNyomtatódott?
var2 óra a leadás utánlejartbiztosan nem — a vezérlő át sem vette
atvettea vezérlő azóta jelentkezett, de erről a feladatról 15 perce nem beszél; vagy eltelt 24 órabizonytalan, eredmeny: nullnem 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:

allapoteredmenyMi történt
bizonytalannulla szerver adta fel a várakozást: a vezérlő elvitte a feladatot, és soha nem jelentett róla
bizonytalankitöltvea 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_ida 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_allapotleadva / var / atvette / kesz / hiba / bizonytalan / visszavonva / lejart / leadas_bizonytalan
nyugta_leadvaidőbélyeg — ebből számolod, mióta áll
nyugtaszama kesz eredményből (0012/00034)
nyugta_hibakod, nyugta_hibauzeneta hiba eredményből
nyugta_ellenorzendojelző 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 ótaLekérdezés gyakorisága
0–30 másodperc2–3 másodpercenként (ha a felhasználó a képernyő előtt vár)
30 mp – 5 perc15–30 másodpercenként
5 perc felettpercenké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 / hibakodKiment a portra?Teendő
hiba, hibakod: penztargep_allapotnem — 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_valaszolnem — a gép az ártalmatlan állapotlekérdezésre sem válaszoltellenőriztesd a kábelt/portot, majd add le újra
hiba, hibakod: nincs_engedelynem — az engedélykapu a foglalás előtt futrendezd az előfizetést (vagy a Számla kapcsolót), majd add le újra
hiba, hibakod: el_sem_kezdtuknem — 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 kiadd 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 vanjaví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
bizonytalannem tudhatóember dönt (7.6)
visszavonvanem — a vezérlő soha nem vette átsemmi nem nyomtatódott. Ha mégis kell a bizonylat, add le új feladatként
lejartnem — 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
keszigen, sikerrelké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

AntipatternMiért baj
a POST hálózati hibája után automatikus újraküldésmásodik nyugtát nyomtathat, amit nem lehet visszavonni
a webkérés megvárja a nyomtatástmásodpercekig vagy percekig blokkol, és időtúllépésnél elveszíted a kimenetelt
statusz feladat másodpercenkénta 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őreké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ésepontosan 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 bizonylatotnem megy, és nem is mehet: adóügyi bizonylatot csak sztorno javít
a hiba szöveges üzenetére ágazniaz ü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-bea \ é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":…}})

kodHTTPJelentés
nincs_token401hiányzik az Authorization: Bearer fejléc
ervenytelen_kulcs401ismeretlen vagy visszavont API kulcs
ures_keres400üres kérés-törzs
hibas_json400a törzs nem értelmezhető JSON objektum
hibas_payload400a feladat nem alakítható vissza JSON-ná
hibas_kulso_azonosito400a kulsoAzonosito nem szöveg/szám, vagy 100 karakternél hosszabb
terminal_kotelezo400több pénztárgép van, meg kell adni, melyikre menjen
nincs_elofizetes402a terminál előfizetése nem aktív
lejart_elofizetes402az előfizetés lejárt
ismeretlen_terminal404nincs ilyen AP számú vagy nevű terminál ennél az ügyfélnél
ismeretlen_feladat404nincs ilyen feladat (vagy nem a te ügyfeledé)
ismeretlen_vegpont404rossz útvonal vagy HTTP metódus
mar_atvette409visszavonás: a vezérlő már elvitte a feladatot, lehet, hogy nyomtatja
mar_elkeszult409visszavonás: a bizonylat elkészült — csak sztorno javítja
mar_lezart409visszavonás: a feladat hiba, bizonytalan vagy lejart állapotban lezárult
kulso_azonosito_utkozes409leadás: ugyanazt a kulcsot más tartalommal használtad
nincs_terminal409az ügyfélhez nincs aktivált pénztárgép
tul_nagy413a feladat nagyobb 256 KB-nál
sema_nincs_telepitve503a szerver telepítése nem fejeződött be
sema_frissites_kell503a kulsoAzonosito oszlopa még nincs meg az adatbázisban — a kulcs nélküli leadás közben is működik
szerverhiba500váratlan hiba; a részlet a szerver hibanaplójában

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

hibakodKiment a portra?Jelentés
penztargep_allapotnemzárt nap, vagy a gépen nyitva maradt egy bizonylat
penztargep_nem_valaszolnema gép az állapotlekérdezésre sem válaszolt
nincs_engedelynemnincs érvényes bérlet (előfizetés), vagy a típus nincs engedélyezve (pl. szamla)
el_sem_kezdtuknema 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 üzenettelnema küldés előtti validáció fogta meg
a pénztárgép saját kódja (040B, 0310, 8334, 045F, …)igena 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.

    { "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átRészlet
A leadás csak kulsoAzonosito-val idempotensa 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ésfeladatonként egy GET; sok függő feladatnál ritkítsd az ütemet
A webhook egy óra után feladjahat 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ésikcronbó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 nincsaki beállít webhookot, minden végállapotról kap értesítést
Nincs listázó végponta saját feladataid listáját neked kell nyilvántartanod (ezért horgony a feladatId)
A visszavonás csak var állapotban megyonnantó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 olvasonnan ma nem lehet feladatot visszavonni vagy újraküldeni; a visszavonás az API-n megy
Egy jelentkezés legfeljebb 10 feladatot ad kiFIFO 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 elavulhataz 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álvaa 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 útona 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):

php src/Php_Kliens/onproba.php
dotnet test