Search by

tmonier / sylius-einvoice-transmission-plugin

thibmonier

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

Statistics

Installs: 441

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.0 2026-10-24 10:18 UTC

This package is auto-updated.

Last update: 2026-10-28 10:24:00 UTC


README

Version française

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 error and the message of the exception.
  • An adapter that answers a status outside the known set is refused: the line gets the status error with 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 with TransmissionNotRelaunchable, and nothing is sent;
  • if the document cannot be provided, the line goes back to error with 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 error or not_transmitted is 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 in not_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.