sylius/refund-plugin
Sylius Refund Plugin adds refund management to Sylius e-commerce stores. Administrators can create and process order and payment refunds, track refund states and items, and integrate refund workflows with Sylius resources and admin UI.
Read this first: sylius/refund-plugin is not a Laravel package. It is a Sylius (Symfony) bundle — no service provider, no config/refund.php, no Eloquent models. If you found it while searching Packagist from a Laravel project, stop: there is no supported way to install it into Laravel without dragging in the entire Sylius/Symfony stack. Two realistic paths:
composer create-project sylius/sylius-standard) and install the plugin there. This is the intended use.For path 1, minimal install:
composer require sylius/refund-plugin
Then, in the Sylius app:
Sylius\RefundPlugin\RefundPlugin in config/bundles.php.config/routes.yaml (sylius_refund_admin, sylius_refund_shop — only load the ones you need).config/packages/_sylius_refund.yaml) and run bin/console doctrine:migrations:migrate.src/Resources/config/app/ or include them from your _sylius.yaml. Without this the "Refunds" panel does not appear on the order screen.Where to look first, in order: src/Resources/config/app/ (what needs wiring), src/Command/ + src/CommandHandler/ (the actual API surface), src/Refund/RefundPaymentMethodResolver.php (how "offline" refunds are decided), and the admin templates for the refund flow.
First use case: open a paid order in admin → Refunds → create a refund from order items/shipments → complete it → a credit memo is generated. If that works, the plumbing is correct.
Programmatic refunds via the message bus. The public API is commands, not services. From your own controller or console command:
$bus->dispatch(new RefundUnits(
$orderNumber,
[new RefundUnit($orderItemUnitId, RefundType::ORDER_ITEM_UNIT, $quantity, $amount)],
$paymentMethodId,
'Damaged in transit'
));
$bus->dispatch(new CompleteRefund($refundId));
Creating and completing are separate steps; treat "create" as a draft document and "complete" as the irreversible step that generates the credit memo.
Offline refunds are the default, gateway refunds are yours to write. The plugin records that money should be returned and produces a credit memo — it does not call Stripe/PayPal. For automatic PSP refunds, decorate the RefundPayment handler or listen for the completion event and call your gateway there, then store the transaction reference on your extended refund entity. Implement RefundPaymentMethodInterface (plus its supports() method) to expose a "manual refund" channel in the admin dropdown — most teams add one such method meaning "paid out by bank transfer" and reconcile later.
Extend the entities, don't fork them. Use Sylius' resource override mechanism in _sylius.yaml (classes:, resolving_target_entity) and Doctrine mappedSuperclass to add fields like psp_reference. Fork the bundle only as a last resort — you will be maintaining it against every Sylius minor release.
Customize the credit memo by overriding the Twig template and, if the layout differs materially, decorating CreditMemoGenerator. The PDF adapter (dompdf/snappy) is configured app-side; pick it early because it is the most common source of install pain.
Subscribe to lifecycle events (RefundCreated, RefundCompleted, credit-memo-created, plus the command-handler middleware) for notifications, ERP sync, and ledger writes. In Symfony this is the EventDispatcher; do not assume Laravel-style model events exist on the Refund entity — they do not.
Headless / API. Sylius 1.12+ exposes order resources through API Platform, but the refund endpoints are admin-UI oriented. For a mobile app or SPA, write a thin custom resource that dispatches the same commands — that keeps the state machine as the single source of truth.
composer.json constraints before upgrading Sylius.composer require. Missing route imports or missing resource config produce a plugin that "installs fine" but shows no Refunds tab. If the UI is absent, you skipped a config import.$amount = 1050 means 10.50. Mixing floats in from your own code is the fastest way to produce an over-refund or a credit memo off by 100×. Validate against the remaining refundable amount in your own code — the admin UI guards it, your custom controller probably won't.RefundUnits twice from a retried queue job creates two refunds. Check payment state and existing refunds before dispatching, or wrap dispatch in a unique-constrained job.bin/console debug:messenger to confirm handlers are registered, bin/console debug:container Sylius\\RefundPlugin for services, and the Sylius admin grid (shows refund state) as your first health check. Log channel noise is high — filter by sylius_refund.KernelTestCase with the test database and assert on Refund state rather than on emitted events where possible.How can I help you explore Laravel packages today?