setono/sylius-quickpay-refund-bridge-plugin
A Sylius plugin that bridges QuickPay refunds into your store's payment workflow, letting you process and manage QuickPay refunds from within the Sylius admin and order management interface.
This plugin is a bridge: it does not provide the QuickPay payment method itself, it only connects Sylius' refund workflow (from sylius/refund-plugin) to QuickPay's refund API. Before touching the bridge, make sure the two halves it connects are already working.
sylius/refund-plugin is installed, migrations are run (sylius_refund_* tables present), and the "Refunds" admin section works. Try a refund against a manual/offline payment first.composer require setono/sylius-quickpay-refund-bridge-plugin
config/bundles.php:
Setono\SyliusQuickpayRefundBridgePlugin\SetonoSyliusQuickpayRefundBridgePlugin::class => ['all' => true],
Where to look first when reading the code: find the service that implements the Refund plugin's refunder/processor interface (or the event listener on UnitsRefunded / RefundPaymentGenerated) — that class is the entire bridge and tells you exactly which QuickPay client method is called and with what amount.
The bridge is event/state-machine driven, not UI driven. The canonical flow is: admin submits a refund → sylius/refund-plugin persists RefundPayment entities and fires an event → this bundle catches it → resolves the QuickPay payment → calls refund on the QuickPay client. There is no separate "Refund via QuickPay" button; you reuse Sylius' existing refund UI.
Order-level vs unit-level refunds. Sylius' refund plugin supports refunding individual order units and shipping, and it aggregates them into a RefundPayment. Test both:
QuickPay tracks refunded amount per payment, so sequential partials are where mismatches usually show up.
Match payment to order explicitly. QuickPay's dashboard keys on your order ID / transaction ID. Make sure your Sylius order number or a stable payment reference is what you send to QuickPay, not Sylius' internal payment ID — otherwise reconciliation in QuickPay is painful. Verify what the bundle sends by logging the outgoing request once.
Idempotency via a refund key. Any real integration must send a unique idempotency key per Sylius refund (e.g. the RefundPayment id or its UUID). Sylius admin double-clicks and state-machine re-transitions are common. If the bundle doesn't populate one, wrap the call yourself or guard with a "refund already requested" flag on the payment.
Integrate with your own post-refund hooks, don't fork the bundle. Listen to Sylius' refund events (UnitsRefunded, RefundPaymentGenerated, refund state transitions) in your app to do things the bridge intentionally doesn't: send a customer email, write an audit log row, tag the order. The bridge should stay a thin transport layer.
Wrap in a decorator for observability. Decorate the bridge's refunder/processor service and log (request payload, QuickPay response, HTTP status) around it. You will need this the first time a refund "worked in Sylius but not in QuickPay."
Testing pattern. Use QuickPay's test merchant (their test cards auto-approve) plus a Sylius functional test that:
Mock the QuickPay client at the HTTP boundary — don't hit the live test API in CI.
Dormancy / maintenance risk. Last release April 2022, 2 stars, effectively no dependents. Assume it is not tracking newer Sylius or QuickPay client versions. Pin symfony/*, sylius/*, and the refund plugin versions, and read the composer constraints before upgrading Sylius. If your Sylius is v1.12+, budget time to verify the bundle still compiles and the service wiring is intact.
It is only as good as the base QuickPay plugin. The bridge needs the QuickPay payment's transaction id (payment->getDetails() or a stored reference) to refund against. If your QuickPay plugin version stores state differently, the bridge may resolve null and fail silently or throw. Confirm the field it reads exists on your live payment objects before relying on it.
Amounts and currency units. QuickPay's API expects amounts in the currency's minor unit (integer, e.g. 10000 = 100.00 DKK). Sylius also stores integers in minor units, but mixing them (dividing by 100 "for readability") produces off-by-100 refunds. Grep the bundle for the amount construction and confirm it matches the QuickPay client's expectation; also confirm currency is forwarded (QuickPay refunds may need the payment currency).
Silent no-op is the common failure. If the bridge can't find a matching QuickPay payment or transaction, it may skip the API call rather than erroring. Always reconcile: after a Sylius refund, check the QuickPay dashboard. Add a log line or a RefundPayment note that records the QuickPay refund id returned — that is your proof the call happened.
State machine ordering. Sylius' refund plugin can move refund payments to "completed" independently of whether the gateway call succeeded. Don't assume a "completed" Sylius refund means QuickPay was refunded. Model a failure path (retry button / manual reconcile) in your admin.
Config quirks. There's typically little to no bundle config — check DependencyInjection/Configuration.php and the Resources/config/services.yaml for the exact service ids and whether any env var (API key, merchant) is expected. The QuickPay API credentials belong to the base payment method config (GatewayConfig), not this bundle; don't duplicate them.
Extension points are limited by design. The sane extension points are (a) decorating the refunder/processor service, (b) listening to Sylius refund events, (c) wrapping the QuickPay client. Avoid patching vendor code; if the bridge's idempotency or amount handling is wrong for your accounting, copy the tiny class into your app and wire it yourself rather than maintaining a fork.
Debugging checklist when a refund doesn't reach QuickPay:
debug:container?UnitsRefunded vs RefundPaymentGenerated? Is the listener subscribed to the one your flow emits?How can I help you explore Laravel packages today?