zoparga/laravel-szamlazzhu
Laravel package for integrating Szamlazz.hu, the Hungarian online invoicing service. Create and manage invoices, configure seller and buyer data, and issue invoices from your Laravel app through a simple API wrapper.
composer require zoparga/laravel-szamlazzhu. The service provider is auto-discovered; Szamlazz is registered as a facade.php artisan vendor:publish --tag=szamlazzhu-config (check the README for the exact tag — the package ships a single config/szamlazzhu.php). Most keys map to env vars, so set SZAMLAZZHU_AGENT_KEY and the invoice/receipt agent keys in .env rather than editing the file.számla) and receipt (nyugta) agents, and separate keys for the live system vs. the test/sandbox agent. Create a test agent in the Szamlazz.hu admin panel before writing any code — every invoice you generate with the live key is a real, legally-issued document.config/szamlazzhu.php (to see which credential keys exist), then the package README for the facade method signatures, then the Szamlazz.hu XML API documentation, since the package is a thin transport over that XML schema. The vendor source under vendor/zoparga/laravel-szamlazzhu/src is small and readable when the README is ambiguous.use Szamlazz;
$invoice = Szamlazz::createInvoice([...]); // header + line items
$pdf = Szamlazz::getPdf($invoice->getInvoiceNumber());
Storage::disk('invoices')->put("invoices/{$invoice->getInvoiceNumber()}.pdf", $pdf);
Confirm you can see the invoice in the Szamlazz.hu web UI before wiring it into a checkout flow. That end-to-end check is the fastest way to validate your agent key, tax number, and payload structure in one go.
Wrap the facade in a domain action, never call it from a controller. Create a single App\Actions\IssueInvoice (or App\Services\Billing\SzamlazzBiller) that takes your Order model, builds the XML payload, calls the facade, and returns a result DTO. You want exactly one place to change when Szamlazz.hu's schema or the package's array keys shift, and exactly one place to add logging.
Map your domain to the payload in a dedicated builder. Keep a InvoicePayloadBuilder that turns Order + OrderItem[] into the package's array/XML structure: net unit price, quantity, VAT rate code, currency, payment method, and the buyer's tax number. Isolating this makes it unit-testable without any HTTP at all, which is where most of your test coverage should live.
Treat issuance as two steps. Creating the invoice and fetching the PDF are separate round trips. Persist the invoice number on your orders table in the same request that issues, then fetch and store the PDF — either immediately or in a follow-up job. If the PDF fetch fails you have a legitimately issued invoice referenced by number and can re-fetch later; if you treat them as one atomic step you'll be tempted to retry the creation call and issue a duplicate.
// after createInvoice()
$order->update([
'invoice_number' => $invoice->getInvoiceNumber(),
'invoice_issued_at' => now(),
]);
FetchInvoicePdf::dispatch($order)->delay(now()->addSeconds(10));
Queue issuance, but guard against duplicates. Invoice creation belongs in a job (network timeouts, Szamlazz.hu outages), with $tries, exponential backoff, and a ShouldBeUnique or a DB-level invoice_number IS NULL guard so a retried job can't issue a second invoice. Never issue inside a DB transaction that could roll back after the call succeeds — you'll end up with an orphan invoice and a retried order.
Lean on the 1.8 error messages. This release fills in previously unmapped Szamlazz.hu API error strings, so error handling can now branch on message content rather than a generic failure. Wrap the facade calls in a try/catch that converts package exceptions into your own InvoiceFailedException carrying the raw request/response, then either surface a safe message to the customer or escalate to an operator. The messages are Hungarian and API-specific, so log them verbatim and translate only for user-facing copy.
Cancellations have their own flow. A storno/credit note is not "delete the invoice." It references the original invoice number explicitly. Model it as a separate action that stores both the original and the reversing document numbers on the order, and never allow a storno more than once per invoice.
Multi-tenant setups need runtime config. If each tenant has its own Szamlazz.hu agent, don't rely on a single env key. Resolve credentials per tenant and set them at call time (via a bound config repository or a per-call client), and cache the resolved config per tenant to avoid re-reading on every request. Be careful here: if you mutate global config with config()->set(), you must reset it in queued workers between jobs, or tenant A's agent key leaks into tenant B's invoice.
Testing. Separate the builder tests (pure array comparison, no HTTP) from the transport tests. For transport, use the package's HTTP abstraction — if it goes through the Laravel HTTP client, Http::fake() works; if it uses Guzzle directly, inject a mock handler. Snapshot the generated XML with a fixture so a dependency bump that changes a field name fails loudly in CI rather than silently producing invalid invoices in production.
.env. Assert on app()->environment() or, better, use a distinct config key set per environment so a staging deploy can't reach the live agent.createInvoice() gives you the document, you'll get an object with metadata but no bytes. Fetch separately.config:cache breaks env reads at runtime. Any code reading env('SZAMLAZZHU_...') directly will return null in a cached environment. Read through config() only.createInvoice() again. Keep the order's external reference/idempotency field stable across retries.str_contains() on them in business logic for anything important — match on the error code if the package exposes one, and treat message text as logging material.szamlazzhu log channel with limited retention; but do capture it, because upstream support will ask for it.How can I help you explore Laravel packages today?