Laravel NAV Online Számla
Teljes, Laravel-natív kliens a NAV Online Számla v3.0 interfészhez.
composer require intragile/laravel-nav-online-szamla
Teljes, Laravel-natív kliens a NAV Online Számla v3.0 interfészhez.
- Mind a 10 művelet típusos kérés- és válaszobjektumokkal. Nyers XML-hez nem kell nyúlni.
- Típusos számla-modell, amelyet a hivatalos XSD-kből generálunk. Mellé jár egy builder, amely kiszámolja a tételösszegeket, az ÁFA-összesítőt és a végösszegeket. Tud CREATE/MODIFY/STORNO műveletet, egyszerűsített, gyűjtő-, devizás és előlegszámlát.
- Ellenőrzés még beküldés előtt: minden dokumentumot validálunk a csomagban lévő hivatalos XSD-k ellen, és lefuttatjuk a NAV blokkoló üzleti szabályait is, amennyiben az adatokból eldönthetők.
- A Laravel HTTP kliensére épül, így a
Http::fake(), a Telescope és a Pulse is látja a NAV-forgalmat. - Queue-s beküldés tranzakciónaplóval. A státusz-polling backoff-fal fut, és minden lépésről event megy ki. Timeout után a csomag egyeztet, és nem küld vakon újra, így egy számla sosem kerül kétszer a NAV-hoz.
- Több adóalany (tenant), artisan parancsok,
Nav::fake()assertion-ökkel.
Követelmények
- PHP 8.3+ (
bcmath,dom,libxml,openssl) - Laravel 12 vagy 13
Telepítés
composer require intragile/laravel-nav-online-szamla
php artisan vendor:publish --tag=nav-online-szamla-config
php artisan migrate
Hozz létre technikai felhasználót az Online Számla felületén (először a teszt-környezetben), majd állítsd be:
NAV_ENVIRONMENT=test # "production" csak tudatosan
NAV_USERNAME=...
NAV_PASSWORD=...
NAV_TAX_NUMBER=12345678 # bármilyen formátum; az első 8 számjegy számít
NAV_SIGN_KEY=...
NAV_EXCHANGE_KEY=...
NAV_SOFTWARE_ID= # lásd lent
NAV_SOFTWARE_DEV_NAME="Cégem Kft."
NAV_SOFTWARE_DEV_CONTACT=dev@example.com
NAV_SOFTWARE_DEV_TAX_NUMBER=12345678
Minden kérés egy 18 karakteres softwareId-vel azonosítja a szoftvert. Egyszer generáld
le, és utána ne változtasd: ez azonosítja a szoftvert minden telepítésben.
php artisan nav:software-id --write # beírja a NAV_SOFTWARE_ID-t a .env-be
php artisan nav:test-connection # tokenkérés és adózó-lekérdezés a saját adatokkal
Gyors kezdés
use IntrAgile\NavOnlineSzamla\Facades\Nav;
$taxpayer = Nav::queryTaxpayer('87654321-2-13'); // ?Taxpayer
$taxpayer?->name;
$taxpayer?->headquarters()?->city;
Számla összeállítása és beküldése queue-n át:
use IntrAgile\NavOnlineSzamla\Enums\InvoiceAppearance;
use IntrAgile\NavOnlineSzamla\Enums\PaymentMethod;
use IntrAgile\NavOnlineSzamla\Invoice\InvoiceBuilder;
use IntrAgile\NavOnlineSzamla\Invoice\LineItem;
use IntrAgile\NavOnlineSzamla\Invoice\Model\UnitOfMeasure;
use IntrAgile\NavOnlineSzamla\Invoice\Party;
use IntrAgile\NavOnlineSzamla\Invoice\Vat;
$invoice = InvoiceBuilder::create('INV-2026-0001')
->issueDate('2026-11-05')
->deliveryDate('2026-11-05')
->payment(PaymentMethod::Transfer, '2026-11-20')
->appearance(InvoiceAppearance::Electronic)
->supplier(Party::supplier(
'12345678-2-41',
'Eladó Kft.',
Party::address('HU', '1011', 'Budapest', 'Fő utca 1.'),
'11111111-22222222-33333333',
))
->customer(Party::domesticCustomer(
'87654321-2-13',
'Vevő Kft.',
Party::address('HU', '2000', 'Szentendre', 'Kossuth tér 2.'),
))
->line(LineItem::product('Laptop')->quantity(2, UnitOfMeasure::Piece)->unitPrice('250000')->vat(Vat::rate(27)))
->line(LineItem::product('Könyv')->quantity(3, UnitOfMeasure::Piece)->unitPrice('4990')->vat(Vat::rate(5)))
->line(LineItem::service('Kiszállítás')->netAmount('2500')->vat(Vat::rate(27)));
$transaction = Nav::submit($invoice, subject: $order); // NavTransaction, "pending" állapotban
A Nav::submit() eltárolja a pontos XML-t a tranzakciónaplóban, hozzáköti a saját modelledhez,
és elindítja a beküldő jobot. Az eredményről eventek tájékoztatnak (lásd:
Queue-s beküldés).
Szinkron módon:
$transactionId = Nav::manageInvoice($invoice->submission());
$status = Nav::queryTransactionStatus($transactionId);
if ($status->isFinished()) {
$result = $status->results[0];
$result->isAccepted(); // DONE
$result->warnings(); // list<BusinessValidationMessage>
}
Számla-builder
A builder minden összeget kiszámol: a tételek nettó, ÁFA- és bruttó összegét, devizás számlánál
a forintösszegeket, valamint az ÁFA-kulcsonkénti összesítőt és a végösszegeket. A kerekítés
„half away from zero” szabályt követ, mindenhol bcmath decimálisokkal, float nélkül.
| Igény | API |
|---|---|
| Tételtípus | LineItem::product(), ::service(), ::other() |
| Mennyiség / ár | ->quantity('1.5', UnitOfMeasure::Kilogram), ->unitPrice('400'), vagy ->netAmount(), ->grossAmount() |
| ÁFA | Vat::rate(27), Vat::exempt('AAM', 'Alanyi adómentes'), Vat::outOfScope(...), Vat::domesticReverseCharge(), Vat::marginScheme(...), Vat::noCharge(), Vat::content(...) (egyszerűsített számla) |
| Engedmény | ->discount('Árengedmény', rate: '0.1') |
| Előleg | ->asAdvance(), ->settlesAdvance('ELO-1', '2026-10-01', '392.17') |
| Vevő | Party::domesticCustomer(), ::euCustomer(), ::thirdStateCustomer(), ::otherCustomer(), ::privatePerson() |
| Számlafajta | ->simplified(), ->aggregate(), ->currency('EUR', '392.17'), ->completeElectronicInvoice() |
| Módosítás | InvoiceBuilder::modify('INV-1-M', 'INV-1', modificationIndex: 1), BatchModification::of(...) |
A build() validálás után a típusos InvoiceData modellt adja vissza, a submission() pedig
a beküldendő InvoiceSubmission-t.
Sztornó
A stornoOf() az eredeti számla minden tételét pontosan ellentett összeggel fordítja meg, a
felek és a számlaadatok változatlanok maradnak. Az eredetit a legegyszerűbb a NAV-tól lekérni:
use IntrAgile\NavOnlineSzamla\Requests\InvoiceNumberQuery;
$original = Nav::queryInvoiceData(InvoiceNumberQuery::outbound('INV-2026-0001'))?->invoice();
$storno = InvoiceBuilder::stornoOf($original, 'INV-2026-0001-S', modificationIndex: 1, issueDate: '2026-11-05');
Nav::submit($storno);
Ellenőrzés beküldés előtt
A NAV sok számlát csak a feldolgozás után, percekkel később utasít el ABORTED státusszal. Az
InvoiceValidator lefuttatja azokat a blokkoló szabályokat (a specifikáció 3.3.2. fejezete),
amelyek a beküldött adatból eldönthetők. Így a hibák többsége még a küldés előtt kivételként
jelentkezik:
use IntrAgile\NavOnlineSzamla\Exceptions\InvoiceValidationException;
try {
$invoice->build();
} catch (InvoiceValidationException $e) {
$e->codes(); // ['INVOICE_ISSUE_DATE_LATE']
$e->violations; // list<Violation>: kód, üzenet, útvonal
}
Néhány szabály olyan tényen múlik, ami a számlából nem látszik. Ha biztosan tudod, hogy a NAV elfogadja az esetedet, a szabály kihagyható egy számlánál vagy az egész alkalmazásban:
use IntrAgile\NavOnlineSzamla\Invoice\Validation\InvoiceValidator;
$invoice->validateWith((new InvoiceValidator)->skip('MISSING_CUSTOMER_DOMESTIC_TAXNUMBER'));
// config/nav-online-szamla.php
'validation' => ['enabled' => true, 'skip' => ['MISSING_CUSTOMER_DOMESTIC_TAXNUMBER']],
A queue job a bejelentkezett technikai felhasználó adószámával is ellenőriz, így a
SUPPLIER_TAX_NUMBER_MISMATCH hibát is elkapja, mielőtt a NAV jelezné.
Queue-s beküldés
Nav::submit() ─► pending ─► sending ─► processing ─► finished (minden tétel DONE vagy ABORTED)
│ │ └─────► abandoned (elfogyott a polling)
│ ├──► rejected (a NAV magát a kérést utasította el)
│ └──► unconfirmed ─► egyeztetés ─► processing | pending | abandoned
└──► rejected (kliensoldali validáció)
A beküldés előtt a job atomikusan lefoglalja a tranzakciót, így duplikált vagy újrafutó job sem
küldheti el kétszer. A manageInvoice automatikusan soha nem ismétlődik.
Egyeztetés vak újraküldés helyett. Timeout vagy elveszett válasz után nem tudni, mit tett a
NAV: lehet, hogy befogadta a számlát. A tranzakció unconfirmed állapotba kerül, és
reconciliation.delay másodperc múlva a csomag:
- lekéri a küldési időablak tranzakcióit (
queryTransactionList), - a jelöltek eredeti kérését összeveti a naplózott XML-lel,
- egyezés esetén a megtalált tranzakcióazonosítóval folytatja a pollingot,
- ha két, időben elválasztott ellenőrzés során sincs egyezés, és a
queryInvoiceChecksem ismeri egyik számlát sem, akkor újraküld. Ez az egyetlen eset, amikor bármi újra elmegy.
Végleges hibakódot tartalmazó NAV-hibaválasz elutasítja a tranzakciót. A NAV-hibakód nélküli válasz (például proxy-oldal vagy csonka válasz) nyitva hagyja a kimenetet, és egyeztetés következik.
| Event | Mikor |
|---|---|
TransactionSubmitted |
a NAV kiadta a tranzakcióazonosítót |
InvoiceAccepted / InvoiceRejected |
egy tétel DONE / ABORTED státuszt kapott (a NAV üzeneteivel) |
TransactionFinished |
minden tétel feldolgozása befejeződött |
TransactionRejected |
soha nem került befogadásra: kliensoldali validáció vagy NAV-hiba; lásd error_code |
TransactionUnconfirmed |
ismeretlen kimenet; egyeztetés ütemezve |
TransactionReconciled |
az egyeztetés megtalálta a tranzakciót (found: true), vagy újraküldést ütemezett |
TransactionAbandoned |
az automatikus kezelés feladta; kézi beavatkozás kell |
use IntrAgile\NavOnlineSzamla\Events\InvoiceAccepted;
Event::listen(function (InvoiceAccepted $event) {
$event->item->subject?->update(['nav_status' => 'reported']);
$event->result->warnings();
});
A queue kapcsolatát és nevét, a polling backoffját (polling.delays, az utolsó érték ismétlődik)
és az egyeztetés korlátait a config/nav-online-szamla.php állítja. A sync driver is működik,
de a várakozásokat a futó folyamatban tölti ki. Élesben valódi queue-t használj.
Biztonságos üzemeltetés:
-
A worker kényszerítse ki a job-timeoutot (Linuxon a pcntl extension alapból elérhető). A queue kapcsolat
retry_afterértéke legyen nagyobb a beküldő job timeoutjánál. Ez ahttpbeállításokból adódik, alapértékekkel kb. 95 másodperc. -
Ütemezd a
nav:resumeparancsot. Ez felveszi azokat a tranzakciókat, amelyeknek elveszett a jobja, például sikertelen dispatch vagy az utolsó próbálkozáson leállított worker után:Schedule::command('nav:resume')->everyTenMinutes(); -
Az eventek egyszer, az adatbázis-commit után mennek ki. Azok a listenerek, amelyeknek nem szabad eventet elveszíteniük, legyenek queue-sok (
ShouldQueue), így egy hibázó listener önállóan újrapróbálódik.
Több adóalany
// config/nav-online-szamla.php
'tenants' => [
'acme' => [
'login' => env('ACME_NAV_USERNAME'),
// password, tax_number, sign_key, exchange_key
],
],
Nav::for('acme')->queryTaxpayer('87654321');
Nav::submit($invoice, tenant: 'acme');
Ha a hitelesítő adatok máshonnan jönnek (például titkosított adatbázismezőből), implementáld az
IntrAgile\NavOnlineSzamla\Tenancy\CredentialsResolver interfészt, és add meg
credentials_resolver-ként, vagy kösd be a containerbe.
Műveletek
| Művelet | Metódus |
|---|---|
| tokenExchange | Nav::tokenExchange() |
| manageInvoice | Nav::manageInvoice($submissions), vagy queue-n: Nav::submit() |
| manageAnnulment | Nav::manageAnnulment($annulments), vagy queue-n: Nav::annul() |
| queryTransactionStatus | Nav::queryTransactionStatus($id, returnOriginalRequest: false) |
| queryTransactionList | Nav::queryTransactionList($from, $to), minden oldal: Nav::transactions() |
| queryInvoiceData | Nav::queryInvoiceData(InvoiceNumberQuery::outbound('...')) |
| queryInvoiceCheck | Nav::queryInvoiceCheck(...) |
| queryInvoiceDigest | Nav::queryInvoiceDigest(InvoiceDigestQuery::issuedBetween($from, $to)), minden oldal: Nav::invoiceDigests() |
| queryInvoiceChainDigest | Nav::queryInvoiceChainDigest(...), minden oldal: Nav::invoiceChain() |
| queryTaxpayer | Nav::queryTaxpayer('12345678') |
Az alapértelmezett adózó kliensét az IntrAgile\NavOnlineSzamla\NavClient injektálásával is
megkapod.
Hibák
Minden kivétel a NavException leszármazottja.
| Kivétel | Jelentés |
|---|---|
ConfigurationException |
hiányzó vagy hibás beállítás; még kérés előtt jelentkezik |
XsdValidationException |
a dokumentum sérti a hivatalos sémát; semmi nem ment el |
InvoiceValidationException |
egy blokkoló NAV üzleti szabály elutasítaná a számlát |
NavApiException |
a NAV hibával válaszolt: errorCode, isRetryable(), isAuthenticationError() |
TransportException |
a kérés nem jutott el a NAV-hoz, vagy elveszett a válasz |
A lekérdezéseket és a tokenExchange-et átmeneti hiba esetén új kérésazonosítóval
megismételjük. Kérést soha nem naplózunk, mert jelszó-hash-t és tokent tartalmaz. A titkok a
debug-kimenetben maszkolva jelennek meg.
Tesztelés
use IntrAgile\NavOnlineSzamla\Facades\Nav;
use IntrAgile\NavOnlineSzamla\Invoice\Model\InvoiceData;
Nav::fake()
->taxpayer('87654321', 'Vevő Kft.')
->rejectInvoice('INV-13', 'INVOICE_NUMBER_NOT_UNIQUE');
// ... a tesztelt kód ...
Nav::assertInvoiceSubmitted('INV-2026-0001');
Nav::assertInvoiceSubmitted(fn (InvoiceData $invoice) => $invoice->invoiceMain->invoice?->invoiceHead->customerInfo?->customerName === 'Vevő Kft.');
Nav::assertSubmittedCount(1);
Nav::assertTaxpayerQueried('87654321');
A fake egy memóriában futó NAV. Megőrzi a befogadott számlákat, így a queryInvoiceCheck, a
queryInvoiceData, a queryTransactionStatus és a queryTransactionList összhangban marad
azzal, amit a kódod beküldött. A kimenő dokumentumokat továbbra is validálja az XSD-k ellen. A
queue-s beküldés a valódi jobokon fut végig, várakozás nélkül.
| Beállítás | Assertion |
|---|---|
taxpayer($taxNumber, $névVagyTaxpayerVagyNull) |
assertInvoiceSubmitted($számVagyClosure, $tenant) |
rejectInvoice($number, $code) |
assertInvoiceNotSubmitted(...), assertNothingSubmitted() |
warnInvoice($number, $code) |
assertSubmittedCount($n) |
failNext($operation, NavFake::apiError(...) | NavFake::connectionFailure()) |
assertAnnulmentSubmitted(...) |
respond($operation, fn (...$args) => ...) |
assertTaxpayerQueried($taxNumber) |
withTaxNumber($taxNumber, $tenant) |
assertSent($operation, $closure), assertNotSent(), assertSentCount(), assertNothingSent() |
HTTP-szinten a Http::fake() is használható.
Artisan parancsok
| Parancs | Mire való |
|---|---|
nav:software-id [tax-number] [--write] [--force] |
a softwareId legenerálása (egyszer) |
nav:test-connection [--tenant=] |
a hitelesítő adatok ellenőrzése tokenkéréssel és adózó-lekérdezéssel |
nav:status [id] [--tenant=] |
a nyitott tranzakciók listája, vagy egy tranzakció (helyi vagy NAV-azonosító) élő NAV-státusza |
nav:resume [--idle=10] |
az adott ideje tétlen nyitott tranzakciók jobjának újraindítása; ütemezd |
Sémák és a generált modell
A hivatalos XSD-k a resources/xsd mappában vannak. Az src/Invoice/Model alatti modell ezekből
generált kód, kézzel soha ne szerkeszd. Ha a NAV új sémát ad ki:
git clone --depth 1 https://github.com/nav-gov-hu/Online-Invoice.git /tmp/nav
php bin/sync-xsd.php /tmp/nav # a csomag XSD-inek frissítése
php bin/generate-model.php # a modell újragenerálása
php bin/generate-model.php --check # CI: hibát jelez, ha a modell elavult
Egy heti CI-futás jelzi, ha a csomag sémái eltérnek a NAV repójától.
Fejlesztés
composer test # PHPUnit
composer analyse # PHPStan, max szint
composer format # Pint
Az end-to-end suite minden műveletet lefuttat a NAV teszt-környezete ellen. Csak külön kérésre
fut, és egy teszt technikai felhasználót vár dotenv fájlban (NAV_USERNAME, NAV_PASSWORD,
NAV_TAX_NUMBER, NAV_SIGN_KEY, NAV_EXCHANGE_KEY). Mindig a teszt-környezetet hívja.
NAV_E2E_ENV=/útvonal/.env.test vendor/bin/phpunit --group e2e # alapértelmezés: ~/.config/nav/.env.test
Licenc
MIT. Lásd: LICENSE.
Fejleszti és karbantartja az IntrAgile Kft.
Ez az oldal a csomag v1.0.3 verziójának README-jéből készült. Forrás a GitHubonúj lapon nyílik meg