Skip to main content

Sync Sylius order payment state to an invoice

· 2 min read
Stephan Hochdörfer
Head of IT Business Operations

In a recent Sylius project, we realized that the order payment state is not synchronized with the invoice for the order.

In the project, we have installed the official InvoicingPlugin from Sylius. Once an order gets placed, the invoice gets created. However, the payment state is not yet set if you use an external payment provider. Sadly, the plugin does not offer a way to "update" the invoice once the payment is made.

Solving the issue was not too hard at all. The Invoicing plugin issues an OrderPaymentPaid event after completing the order. The event is used to send out an email with the invoice attached. The easiest way to solve our issues was to subscribe to the event and update the record in the database:

<?php

declare(strict_types=1);

namespace App\EventListener;

use Doctrine\ORM\EntityManagerInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Sylius\InvoicingPlugin\Doctrine\ORM\InvoiceRepositoryInterface;
use Sylius\InvoicingPlugin\Entity\Invoice;
use Sylius\InvoicingPlugin\Entity\InvoiceInterface;
use Sylius\InvoicingPlugin\Event\OrderPaymentPaid;

final class OrderPaymentPaidListener
{
public function __construct(
private InvoiceRepositoryInterface $invoiceRepository,
private OrderRepositoryInterface $orderRepository,
private EntityManagerInterface $entityManager,
) {
}

public function __invoke(OrderPaymentPaid $event): void
{
/** @var OrderInterface $order */
$order = $this->orderRepository->findOneByNumber($event->orderNumber());
if ($order === null) {
return;
}

/** @var InvoiceInterface|null $invoice */
$invoice = $this->invoiceRepository->findOneByOrder($order);
if ($invoice === null) {
return;
}

if ($invoice->paymentState() === InvoiceInterface::PAYMENT_STATE_COMPLETED) {
return;
}

try {
$this->entityManager->createQueryBuilder()
->update(Invoice::class, 'i')
->set('i.paymentState', ':paymentState')
->where('i.id = :id')
->setParameter('paymentState', InvoiceInterface::PAYMENT_STATE_COMPLETED)
->setParameter('id', $invoice->id())
->getQuery()
->execute();
} catch(\Exception $e) {
}
}
}

The logic is straightforward. The order object is fetched via the order number passed with the event. For the order, the matching invoice is read from the database. Finally, the record in the database gets updated to the "completed" state.

In addition to the PHP code above, the following service configuration in config/services.yaml is needed:

App\EventListener\OrderPaymentPaidListener:
class: App\EventListener\OrderPaymentPaidListener
arguments:
- "@sylius_invoicing_plugin.repository.invoice"
- "@sylius.repository.order"
- "@sylius_invoicing_plugin.manager.invoice_sequence"
tags:
- { name: 'messenger.message_handler', bus: 'sylius.event_bus' }

UPDATE: My friend Łukasz Chruściel wondered why I am updating the invoice state directly in the database. And I realized I forgot this part of the explanation. Long story short: The Sylius Invoice bundle is built in a way that invoices can be created and not modified. This makes sense as you want to avoid editing existing invoices for legal reasons. Sadly it includes the payment state and that's why I had to use the database query directly.