swiftmade/lhv-connect
Laravel client for LHV Connect, the bank's API for corporate payments and account data. It wraps authentication, payment initiation, and statement retrieval in an expressive, configurable package, so you can integrate LHV banking into your PHP application with minimal boilerplate.
swiftmade/lhv-connect wraps the LHV Connect (LHV Pank corporate banking) API — payments, account statements, balances — behind a Laravel-friendly client. Version 0.2.0 is a framework-compatibility release (Laravel 12 & 13), so the integration surface is unchanged from 0.1.x.
Minimal setup:
composer require swiftmade/lhv-connectphp artisan vendor:publish --tag=lhv-connect-config (check php artisan vendor:publish output if the tag differs in your version)..env (typical keys, confirm exact names in config/lhv-connect.php):LHV_CONNECT_BASE_URL=https://sandbox.lhv.ee/connect/v1
LHV_CONNECT_CLIENT_ID=your-org-id
LHV_CONNECT_CERT_PATH=/var/www/app/storage/app/private/lhv/cert.pem
LHV_CONNECT_KEY_PATH=/var/www/app/storage/app/private/lhv/key.pem
LHV_CONNECT_KEY_PASSWORD=secret
Where to look first: config/lhv-connect.php (all knobs live here), then the package's client/service class and any src/Messages or src/Requests directory — the shape of those classes tells you whether you build typed DTOs or raw arrays. Read the package's own tests; for banking packages they are usually the best documentation of expected payloads.
Isolate the package behind your own service. Bind a BankingService interface in a service provider and let the package be one implementation. Banks rotate endpoints, XML schema versions, and certificate requirements; you do not want LhvConnect::send() scattered across forty controllers when that happens.
$this->app->bind(PaymentGateway::class, LhvPaymentGateway::class);
Certificate storage. Keep PEMs in storage/app/private/lhv/ (never in the repo, never in public/), mount them from a secret manager in production, and give the filesystem user read-only permissions. Sandbox and production certificates are different — model them as two named connections in config rather than one mutable env var.
Payments: build, validate, then send. Construct the payment (creditor IBAN, amount as a string, currency, reference, end-to-end ID), validate locally against the bank's XSD if the package exposes that, then submit. Persist the request payload and the bank's response in your own table keyed by your internal payment ID — the bank's message ID alone is not enough to reconcile.
Statements: fetch, persist, parse. Pull camt.053 XML on a schedule, store the raw XML, then parse into transactions rows with a unique constraint on (bank_account, bank_reference, booking_date). Reprocessing is common (overlapping date windows, retries), so the unique index is your idempotency guard, not a firstOrCreate scattered in the parser.
Status polling. LHV Connect payment status is asynchronous in practice — a submitted payment is accepted, then settles later. Run a queued job on a schedule that polls pending payments older than N minutes and transitions them. Cap the polling window (e.g. 7 days) so a stuck payment does not poll forever.
Testing. Fake the HTTP layer (Http::fake() with captured sandbox XML fixtures) rather than mocking the package's classes — that keeps your tests valid across patch releases. Keep one opt-in integration test tagged @group sandbox that hits the real sandbox with a €0.01 payment.
Config in CI. Bank credentials are not available in CI. Make the connection lazily resolved so php artisan config:cache, route listing, and unit tests never touch the certificate files.
guzzlehttp/guzzle, XML libs) that do change behaviour. Run your sandbox test after upgrading, not just the unit suite.curl --cert ... --key ... before blaming the package.notAfter date in config/DB and add a scheduled check with a 30-day warning. This is the single most common production outage for bank integrations.0.1 + 0.2 problems in a payment are unacceptable. Cast at the boundary, never before.config:cache and env(). If you add your own logic, read config values via config(), not env(). Direct env() calls inside package-adjacent code return null in production and work fine locally — a classic "works on my machine".Europe/Tallinn business dates and ISO-8601 timestamps in the XML. Laravel's default UTC app timezone silently shifts dates and can push a payment into the wrong value date."swiftmade/lhv-connect": "~0.2.0" rather than ^0.2.How can I help you explore Laravel packages today?