vimatech/laravel-einvoicing
Composer 安装命令:
composer require vimatech/laravel-einvoicing
包简介
Generate compliant structured e-invoices (Peppol BIS 3.0 UBL, EN 16931 CII) natively and dispatch them through pluggable networks. Zero third-party runtime dependencies.
README 文档
README
Generate compliant structured e-invoices natively and dispatch them through pluggable networks (Peppol access points, French PDPs), with per-country routing — for Laravel 11, 12 and 13.
Zero third-party runtime dependencies. Every document is built with PHP's own
ext-dom; every network call uses Laravel's own HTTP client. Nohorstoeko/zugferd, no UBL libraries, no PDF libraries. This is a deliberate security & maintenance policy.
Features
- Native generators — Peppol BIS Billing 3.0 (UBL invoice + credit note) and EN 16931 CII,
emitted directly with
DOMDocument. Factur-X (PDF/A-3) is stubbed for a later isolated module. - Neutral domain model — a single
CanonicalInvoiceDTO; no format or vendor concept ever leaks into your application. - Pluggable networks —
PeppolDriver,FrPdpDriver,NullDriver,FakeDriver, plus your own. - Per-country routing — map destination countries to networks, with a fallback and a per-tenant override hook.
- Native validation — mandatory-field and arithmetic checks (EN 16931 subset) fail fast with actionable messages before anything is rendered or transmitted.
- Lifecycle events —
EInvoiceGenerated,EInvoiceDispatched,EInvoiceDelivered,EInvoiceRejected,EInvoiceReceived.
Requirements
- PHP 8.3+
- Laravel 11, 12 or 13
- Extensions:
ext-dom,ext-xmlwriter,ext-libxml,ext-mbstring
Installation
composer require vimatech/laravel-einvoicing
Publish the config (optional):
php artisan vendor:publish --tag=einvoicing-config
Quick start
1. Build a canonical invoice
The CanonicalInvoice is the only model you ever construct. It is format- and network-agnostic.
use Vimatech\EInvoicing\Dtos\{CanonicalInvoice, Party, LineItem, TaxBreakdown}; $invoice = new CanonicalInvoice( number: 'INV-2024-0001', issueDate: new DateTimeImmutable('2024-01-15'), currency: 'EUR', seller: new Party( name: 'Acme Trading Ltd.', countryCode: 'BE', endpointId: '0208:0123456789', // Peppol electronic address (BT-34) endpointScheme: '0208', // EAS scheme id vatId: 'BE0123456789', legalRegistrationId: '0123456789', legalRegistrationScheme: '0208', street: 'Main street 1', city: 'Brussels', postalZone: '1050', ), buyer: new Party( name: 'Globex NV', countryCode: 'BE', endpointId: '0208:9876543210', endpointScheme: '0208', vatId: 'BE9876543210', street: 'Market square 9', city: 'Antwerp', postalZone: '2000', ), lines: [ new LineItem( id: '1', name: 'Laptop computer', quantity: 5.0, netPrice: 200.0, lineExtensionAmount: 1000.0, taxCategory: 'S', taxPercent: 21.0, ), ], taxBreakdowns: [ new TaxBreakdown(category: 'S', percent: 21.0, taxableAmount: 1000.0, taxAmount: 210.0), ], dueDate: new DateTimeImmutable('2024-02-14'), buyerReference: 'PO-98765', );
Document totals (line extension, tax exclusive/inclusive, payable) are derived from the lines and the VAT breakdown — you do not pass them in.
2. Generate a UBL (Peppol BIS 3.0) document
use Vimatech\EInvoicing\Facades\EInvoice; use Vimatech\EInvoicing\Enums\Format; $document = EInvoice::format(Format::Ubl)->generate($invoice); $document->contents; // the XML string $document->mimeType; // application/xml $document->profile; // urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 $document->save('/path/to/INV-2024-0001.xml');
A credit note is the same call with typeCode: CanonicalInvoice::TYPE_CREDIT_NOTE — the
generator switches to the CreditNote root and CreditedQuantity automatically. Generate CII
with Format::Cii.
If a mandatory field is missing or the arithmetic does not balance, generation throws
InvalidInvoice, which carries the full list of violations:
use Vimatech\EInvoicing\Exceptions\InvalidInvoice; try { EInvoice::format(Format::Ubl)->generate($invoice); } catch (InvalidInvoice $e) { $e->violations(); // ['BT-1: invoice number is required', ...] }
3. Send through a network
send() renders the document, routes it by the buyer's country, transmits it, and fires the
lifecycle events:
$result = EInvoice::send($invoice); // Format defaults to config('einvoicing.default_format') $result->status; // LifecycleStatus::Delivered | Submitted | Rejected | ... $result->messageId; // poll later with fetchStatus() $result->successful(); // bool
Force a format or a specific network:
EInvoice::send($invoice, Format::Ubl, networkKey: 'peppol');
Resolve a network yourself:
EInvoice::route('BE'); // network responsible for Belgium EInvoice::network('peppol'); // a network by key $status = EInvoice::network('peppol')->fetchStatus($result->messageId);
4. Receive inbound documents
foreach (EInvoice::receive('peppol') as $inbound) { $inbound->contents; // raw XML $inbound->senderId; // sender electronic address } // each inbound document also fires an EInvoiceReceived event
Configuration
config/einvoicing.php declares the available networks, the country → network routing
table, and the default format. Credentials come from the environment.
'networks' => [ 'peppol' => [ 'driver' => 'peppol', 'base_url' => env('PEPPOL_BASE_URL'), 'token' => env('PEPPOL_API_TOKEN'), 'paths' => ['send' => '/documents', 'status' => '/documents/{id}/status', 'inbound' => '/inbound'], 'status_map' => [], // map partner status strings -> LifecycleStatus values ], 'fr_pdp' => ['driver' => 'fr_pdp', 'base_url' => env('FR_PDP_BASE_URL'), 'token' => env('FR_PDP_API_TOKEN')], 'sandbox' => ['driver' => 'null'], ], 'routing' => [ 'FR' => 'fr_pdp', 'BE' => 'peppol', 'NL' => 'peppol', // ... ], 'fallback' => env('EINVOICING_FALLBACK_NETWORK'), // null => unmatched countries throw
Adapting a built-in driver to your partner
PeppolDriver and FrPdpDriver speak a small, neutral REST shape. Point them at your access
point / PDP by overriding paths and, where the vocabulary differs, status_map — no code change:
'peppol' => [ 'driver' => 'peppol', 'base_url' => env('PEPPOL_BASE_URL'), 'token' => env('PEPPOL_API_TOKEN'), 'paths' => [ 'send' => '/v2/outbound', 'status' => '/v2/messages/{id}', 'inbound' => '/v2/inbound', ], 'status_map' => [ 'in_progress' => 'in_transit', 'done' => 'delivered', ], ],
Adding a country
Add a row to the routing map pointing at any configured network key:
'routing' => [ 'ES' => 'peppol', 'IT' => 'peppol', ],
Unmatched countries throw UnsupportedCountry unless a fallback network is set.
Per-tenant routing override
Register a resolver (e.g. in a service provider) to override routing per tenant or per invoice.
Returning a network key wins over the static map; returning null defers to it:
use Vimatech\EInvoicing\Facades\EInvoice; EInvoice::router()->overrideUsing(function (string $country, ?CanonicalInvoice $invoice) { return tenant()->prefersDirectPeppol() ? 'peppol' : null; });
Adding a driver
Implement EInvoiceNetwork (or extend AbstractHttpDriver for a REST partner) — keep every vendor
concept inside the driver:
namespace App\EInvoicing; use Vimatech\EInvoicing\Contracts\EInvoiceNetwork; use Vimatech\EInvoicing\Dtos\{CanonicalInvoice, DispatchResult, GeneratedDocument, NetworkCapabilities}; use Vimatech\EInvoicing\Enums\{Format, LifecycleStatus}; final class AcmeDriver implements EInvoiceNetwork { public function __construct(private array $config, private string $key) {} public function key(): string { return $this->key; } public function send(GeneratedDocument $document, CanonicalInvoice $invoice): DispatchResult { // ... call your partner, then normalise the response ... return new DispatchResult(LifecycleStatus::Submitted, $this->key, messageId: '...'); } public function fetchStatus(string $messageId): DispatchResult { /* ... */ } public function receive(): array { return []; } public function capabilities(): NetworkCapabilities { return new NetworkCapabilities($this->key, formats: [Format::Ubl]); } }
Reference it by class in config (it is resolved from the container):
'networks' => [ 'acme' => ['driver' => App\EInvoicing\AcmeDriver::class, /* ... */], ],
Or register a factory at runtime:
app(\Vimatech\EInvoicing\Networks\NetworkManager::class) ->extend('acme', fn (array $config, string $key) => new App\EInvoicing\AcmeDriver($config, $key));
Testing with the FakeDriver
A dependency-free FakeDriver is shipped for your test suite. Swap any network for it and
assert against what was sent — no HTTP, no credentials:
use Vimatech\EInvoicing\Facades\EInvoice; use Vimatech\EInvoicing\Enums\LifecycleStatus; it('sends the invoice to Peppol', function () { $fake = EInvoice::fake('peppol'); EInvoice::send($invoice); $fake->assertSent(fn ($invoice) => $invoice->number === 'INV-2024-0001'); $fake->assertSentCount(1); expect($fake->lastSent()->number)->toBe('INV-2024-0001'); }); it('handles a rejection', function () { EInvoice::fake('peppol')->alwaysReturn(LifecycleStatus::Rejected); expect(EInvoice::send($invoice)->rejected())->toBeTrue(); });
FakeDriver also supports pushInbound() for receive() flows, the inspection API
(sent(), sentCount(), hasSent(), lastSent()) and the assert* helpers.
Conformance & testing notes
- Profiles emitted: Peppol BIS Billing 3.0 —
CustomizationIDurn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0,ProfileIDurn:fdc:peppol.eu:2017:poacc:billing:01:1.0. CII carries the EN 16931 guidelineurn:cen.eu:en16931:2017. - Scope: only the subset of business terms required for the supported fields is emitted, in the UBL 2.1 / CII sequence order. Document-level allowances/charges and multi-currency VAT accounting are intentionally out of scope for this release.
- Golden files: UBL output is asserted byte-for-byte against committed golden samples
(
tests/fixtures/peppol/) shaped after the official Peppol BIS 3.0 examples, plus structural XPath assertions for identifiers and balancing totals. - Recommended external validation: before production, run generated XML through the official Peppol BIS / EN 16931 validator or the CEN Schematron. Native validation here is a fast pre-flight, not a substitute for the Schematron.
Run the suite:
composer test # Pest + orchestra/testbench composer analyse # PHPStan level max composer lint # Laravel Pint
Architecture
CanonicalInvoice ──► FormatGenerator ──► GeneratedDocument
│ (Ubl / Cii / FacturX)
│
└──► EInvoiceRouter ──► EInvoiceNetwork ──► DispatchResult
(by country) (Peppol / FrPdp / Null / Fake / yours)
Dtos/— readonly value objects (the neutral model).Formats/— nativeDOMDocumentgenerators + validation.Networks/— drivers + the config-drivenNetworkManager.Routing/—EInvoiceRouter(country map + fallback + override hook).Events/,Exceptions/,Facades/— the glue.
Contributing
Contributions are welcome.
Please ensure:
- Tests pass (
composer test) - PHPStan passes (
composer analyse) - Code style is formatted with Pint (
composer format)
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our Security Policy for reporting vulnerabilities.
License
The MIT License (MIT). Please see License File for more information.
Credits
Built and maintained by Vimatech. Created by Adel Zemzemi.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 0
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-26