tmonier / sylius-einvoice-transmission-plugin
Transmission of Factur-X electronic invoices for Sylius 2: replaceable transmitter port, log of the sends with their statuses, idempotent listener of the Factur-X plugin event
Package info
github.com/thibmonier/sylius-einvoice-transmission-plugin
Type:sylius-plugin
pkg:composer/tmonier/sylius-einvoice-transmission-plugin
Requires
- php: ^8.2
- sylius/sylius: ^2.2
- symfony/clock: ^6.4 || ^7.4 || ^8.0
- tmonier/sylius-facturx-plugin: ^1.0
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpstan/phpstan-doctrine: ^2.0
- phpstan/phpstan-webmozart-assert: ^2.0
- phpunit/phpunit: ^10.5
- rector/rector: ^2.0
- sylius-labs/coding-standard: ^4.4
- sylius/test-application: ^2.0.0@alpha
- symfony/browser-kit: ^6.4 || ^7.4 || ^8.0
- symfony/css-selector: ^6.4 || ^7.4 || ^8.0
- symfony/debug-bundle: ^6.4 || ^7.4 || ^8.0
- symfony/dom-crawler: ^6.4 || ^7.4 || ^8.0
- symfony/dotenv: ^6.4 || ^7.4 || ^8.0
- symfony/http-client: ^6.4 || ^7.4 || ^8.0
- symfony/intl: ^6.4 || ^7.4 || ^8.0
- symfony/runtime: ^6.4 || ^7.4 || ^8.0
- symfony/web-profiler-bundle: ^6.4 || ^7.4 || ^8.0
- symfony/webpack-encore-bundle: ^2.2
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-28 10:24:00 UTC
README
Transmission of the Factur-X invoices generated by tmonier/sylius-facturx-plugin for Sylius 2: a replaceable port towards a platform, a log of the sends with their statuses, and an idempotent listener of the tmonier_facturx.invoice_generated event.
This is the first slice of the plugin (version 0.x). It ships the port, the log, the listener and a null adapter. It does not connect to any real platform: the adapters are yours to write, or those of a later release.
What it does
- When the Factur-X plugin generates an invoice, the listener hands the XML and the PDF to the adapter you configured.
- Each invoice has one line in the log (
tmonier_einvoice_transmission): invoice id and number, status, message, creation and update dates. - The listener is idempotent: an invoice that already has a line is never sent a second time, even if the event is replayed or handled by two processes at once (the invoice id is the primary key).
- A failing adapter never breaks the generation of the invoice: the exception is caught, the line gets the status
errorand the message of the exception. - An adapter that answers a status outside the known set is refused: the line gets the status
errorwith a message that names the unknown status, and the unknown value is never stored. - By default, the null adapter sends nothing and logs the invoice as
not_transmitted.
Requirements
- PHP 8.2 or later
- Sylius 2.2 or 2.3 (Symfony 6.4, 7.4 or 8.0)
tmonier/sylius-facturx-plugin^1.0, installed and configured
Installation
composer require tmonier/sylius-einvoice-transmission-plugin
Register the bundle in config/bundles.php:
return [ // ... Tmonier\SyliusEinvoiceTransmissionPlugin\TmonierSyliusEinvoiceTransmissionPlugin::class => ['all' => true], ];
Run the migration (the plugin registers its migrations by itself):
bin/console doctrine:migrations:migrate
Configuration
# config/packages/tmonier_sylius_einvoice_transmission.yaml tmonier_sylius_einvoice_transmission: # Id of your service that implements InvoiceTransmitterInterface (default: the null adapter) transmitter: App\Einvoice\MyPlatformTransmitter # Id of your service that implements InvoiceDocumentProviderInterface (optional, enables the relaunch) document_provider: App\Einvoice\MyInvoiceDocuments # Statuses your adapter may answer in addition to the four below (lower snake case) additional_statuses: []
Writing an adapter
use Tmonier\SyliusEinvoiceTransmissionPlugin\Status\TransmissionStatus; use Tmonier\SyliusEinvoiceTransmissionPlugin\Transmission\InvoiceDocument; use Tmonier\SyliusEinvoiceTransmissionPlugin\Transmission\InvoiceTransmitterInterface; use Tmonier\SyliusEinvoiceTransmissionPlugin\Transmission\TransmissionResult; final class MyPlatformTransmitter implements InvoiceTransmitterInterface { public function transmit(InvoiceDocument $document): TransmissionResult { // $document->invoiceId, ->invoiceNumber, ->xml, ->pdfContent, ->pdfFilename // ...send to your platform; throw on failure... return new TransmissionResult(TransmissionStatus::DEPOSITED, 'Reference 123'); } }
RecordingInvoiceTransmitter (namespace Testing) is a test double that keeps what it receives and answers a fixed result.
Relaunching a send in error
A send whose status is error can be sent again. Give the plugin a service that returns the generated invoice from its identifier (InvoiceDocumentProviderInterface::provide()), declare it as document_provider, then call RelaunchTransmissionHandler::relaunch($invoiceId):
- the line of the journal is reopened (same line, status
not_transmitted) by one conditional update, so two relaunches at once send once; - the adapter is called once and its answer is logged as for the first send;
- any other status (
deposited,rejected,not_transmitted) or an unknown invoice is refused withTransmissionNotRelaunchable, and nothing is sent; - if the document cannot be provided, the line goes back to
errorwith the message of the failure and can be relaunched again.
TransmissionJournalInterface gained reopen() in 0.2.0: a custom journal has to implement it.
Statuses
| Status | Meaning |
|---|---|
deposited |
The platform accepted the deposit of the invoice |
rejected |
The platform refused the invoice |
error |
The send failed (exception of the adapter, or unknown status answered) |
not_transmitted |
The invoice is logged but was not sent (null adapter, or send not answered yet) |
These statuses are a minimal set to be verified. The official lifecycle statuses of the French e-invoicing platforms have not been confirmed against an official source for this plugin, and the standard defines its own formats and statuses. The set is therefore kept small and can be extended with additional_statuses; no status is invented on behalf of a platform. Check the current official documents before relying on a status for a legal or tax purpose.
Limits of this slice
- No real platform: only the port, the log and the null adapter are provided.
- No automatic retry: a line in
errorornot_transmittedis not sent again by a replayed event; a send in error is relaunched explicitly (see above). A process that dies between the claim of the invoice and the answer of the adapter leaves the line innot_transmitted. - No admin screen, no directory lookup, no e-reporting.
- The plugin is a technical building block. It is not legal or tax advice and has no regulatory value by itself.
Development
The test application is configured by phpunit.xml.dist and by the environment, not by tests/TestApplication/.env* files. phpunit works without any variable once MySQL runs on port 3351; for the console commands, export them:
composer install docker compose up -d --wait mysql # port 3351 comes from compose.override.yml (see compose.override.dist.yml), or use your own DATABASE_URL export APP_ENV=test \ DATABASE_URL='mysql://root@127.0.0.1:3351/tmonier_sylius_einvoice_transmission_test' \ SYLIUS_TEST_APP_BUNDLES_PATH=tests/TestApplication/config/bundles.php \ SYLIUS_TEST_APP_CONFIGS_TO_IMPORT='@TmonierSyliusEinvoiceTransmissionPlugin/tests/TestApplication/config/config.yaml' php -d memory_limit=2G vendor/bin/console doctrine:database:create --if-not-exists php -d memory_limit=2G vendor/bin/console doctrine:migrations:migrate -n vendor/bin/phpunit vendor/bin/ecs check vendor/bin/phpstan analyse vendor/bin/rector process --dry-run
Real environment variables win over the defaults of phpunit.xml.dist (the CI sets its own DATABASE_URL).
License
MIT, see LICENSE.