PHPackages                             madcoders/sylius-rma-plugin - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. madcoders/sylius-rma-plugin

ActiveSylius-plugin[Utility &amp; Helpers](/categories/utility)

madcoders/sylius-rma-plugin
===========================

RMA Plugin

1.1.0(3w ago)41.2k[2 issues](https://github.com/mad-coders/sylius-rma-plugin/issues)[1 PRs](https://github.com/mad-coders/sylius-rma-plugin/pulls)EUPL-1.2PHPPHP ^8.2

Since Nov 17Pushed 1w ago1 watchersCompare

[ Source](https://github.com/mad-coders/sylius-rma-plugin)[ Packagist](https://packagist.org/packages/madcoders/sylius-rma-plugin)[ RSS](/packages/madcoders-sylius-rma-plugin/feed)WikiDiscussions 1.3 Synced 3w ago

READMEChangelog (7)Dependencies (60)Versions (7)Used By (0)

[Madcoders](https://www.madcoders.co) Sylius RMA Plugin
=======================================================

[](#madcoders-sylius-rma-plugin)

[![Latest Version](https://camo.githubusercontent.com/ee7791327f16beda9ce2f00b98b718b5e1c80463f2b853c8822141e3291c948e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d6164636f646572732f73796c6975732d726d612d706c7567696e2e737667)](https://packagist.org/packages/madcoders/sylius-rma-plugin)[![CI](https://github.com/mad-coders/sylius-rma-plugin/actions/workflows/ci.yml/badge.svg)](https://github.com/mad-coders/sylius-rma-plugin/actions/workflows/ci.yml)[![PHP Version](https://camo.githubusercontent.com/5ed7cc971a0ae276672d0d250a522b6e53140ba8a88d17bbaae7a567ba063cb2/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6d6164636f646572732f73796c6975732d726d612d706c7567696e2e737667)](composer.json)[![License](https://camo.githubusercontent.com/72a7b0ef8cb7ce31c35ea9123e069ce191c9a7da5cdc2132d8956c47e0443b7d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4555504c2d2d312e322d626c75652e737667)](LICENSE)

Sylius RMA (Return Merchandise Authorization) plugin by Madcoders lets customers create a return form and submit a return request from a delivered order.

Features
--------

[](#features)

- return form for both guest and signed-in customers
- customers select the items and quantities to return from a delivered order
- pre-shipment withdrawal: a not-yet-shipped order can be withdrawn (cancelled) instead of returned - instantly when unpaid, or via admin approval with item selection (partial withdrawals) when paid - see [Returns state machine](#returns-state-machine)
- customers choose a return reason for the request
- customers are notified by e-mail at each step of the process
- optional PDF return form (opt-in, off by default - see [below](#optional-enable-the-return-form-pdf))
- merchant-defined return reasons, optionally limited by time since shipment
- merchant-defined terms and conditions the customer must accept before submitting the form
- return management area in the Sylius admin

Requirements
------------

[](#requirements)

VersionPHP8.2Sylius1.12Symfony6.4Installation
------------

[](#installation)

1. Add as dependency in `composer.json`

```
composer require madcoders/sylius-rma-plugin
```

2. Enable plugin in `config/bundles.php`:

```
Madcoders\SyliusRmaPlugin\MadcodersSyliusRmaPlugin::class => ['all' => true],
```

3. Import required config in `config/packages/_sylius.yaml` file:

```
imports:
    - { resource: "@MadcodersSyliusRmaPlugin/Resources/config/config.yml" }
```

4. Import routes `config/routes.yaml` file:

```
madcoders_sylius_rma_plugin:
    resource: "@MadcodersSyliusRmaPlugin/Resources/config/routing.yml"
```

5. Run migrations:

```
php bin/console doctrine:migrations:migrate
```

Configuration
-------------

[](#configuration)

All settings live under the `madcoders_rma` key (`config/packages/madcoders_rma.yaml`) and are optional; the defaults below match the plugin's out-of-the-box behaviour.

SettingEnv varTypeDefaultEffect`return_form_pdf_enabled`-bool`false`Generate the return-form PDF (email attachment + print/download links); requires wkhtmltopdf. See [below](#optional-enable-the-return-form-pdf).`allow_unpaid_withdrawal``MADCODERS_RMA_ALLOW_UNPAID_WITHDRAWAL`bool`true`Offer instant withdrawal of unpaid, not-yet-shipped orders (cancels the Sylius order). When `false`, unpaid orders are not offered withdrawal. See [below](#optional-instant-withdrawal-of-unpaid-orders).`require_additional_information``MADCODERS_RMA_REQUIRE_ADDITIONAL_INFORMATION`bool`false`Show and require the return form's "Additional information" section (bank account number, account holder name, bank name / BIC-SWIFT) for refund handling. When `false`, the section is hidden and not required. See [below](#optional-require-additional-information-on-the-return-form).`resources.*`-mapSylius defaultsStandard Sylius ResourceBundle overrides (model / interface / controller / factory / repository / form) for the plugin's entities.Store data managed in the **Sylius admin** rather than config files: the return address per channel, return reasons (with optional time-since-shipment deadlines), and the consents a customer must accept.

### Optional: enable the return-form PDF

[](#optional-enable-the-return-form-pdf)

PDF generation (the confirmation-email attachment and the print/download links) is **off by default** and requires a working [wkhtmltopdf](https://wkhtmltopdf.org/) binary. To enable it:

```
# config/packages/madcoders_rma.yaml
madcoders_rma:
    return_form_pdf_enabled: true
```

See [ADR 0011](docs/adr-log/0011-return-form-pdf-feature-flag.md).

### Optional: instant withdrawal of unpaid orders

[](#optional-instant-withdrawal-of-unpaid-orders)

When a not-yet-shipped order is **unpaid**, it is withdrawn instantly (the Sylius order is cancelled and the return resolves straight to `withdrawn`, with no admin step). This is **on by default**. Paid orders are always withdrawable via admin approval regardless of this flag.

Toggle it with the `MADCODERS_RMA_ALLOW_UNPAID_WITHDRAWAL` environment variable:

```
# .env
MADCODERS_RMA_ALLOW_UNPAID_WITHDRAWAL=false
```

or override the parameter directly:

```
# config/packages/madcoders_rma.yaml
madcoders_rma:
    allow_unpaid_withdrawal: false
```

When disabled, an unpaid order is not offered the withdrawal flow at all.

### Optional: require additional information on the return form

[](#optional-require-additional-information-on-the-return-form)

The return form can collect the bank details needed to handle a refund: **bank account number**(validated as an IBAN), **account holder name**, and **bank name / BIC-SWIFT**. This "Additional information" section is **off by default** - the section is hidden and none of its fields are required, so a customer can submit a return without bank details.

Enable it with the `MADCODERS_RMA_REQUIRE_ADDITIONAL_INFORMATION` environment variable:

```
# .env
MADCODERS_RMA_REQUIRE_ADDITIONAL_INFORMATION=true
```

or override the parameter directly:

```
# config/packages/madcoders_rma.yaml
madcoders_rma:
    require_additional_information: true
```

When enabled, the section is rendered on the return form and all three fields are required. The values are persisted on the `OrderReturn` and shown (when present) in the admin and shop account return views. This flag does not affect the withdrawal flow, which always collects the bank account number regardless of the setting.

### Optional: mark products as non-returnable

[](#optional-mark-products-as-non-returnable)

Some products cannot be taken back (perishables, hygiene/sealed goods, made-to-order items, gift cards). Return eligibility is normally decided per order; this adds an **item-level** rule so such products are excluded from the return flow even on an otherwise returnable order.

Once enabled (see below), the admin product form gains a **"Non-returnable"** checkbox. With it ticked:

- a variant of that product is never offered for return and is never persisted onto an `OrderReturn`;
- an order whose items are **all** non-returnable shows "nothing to return" (the start-return button is hidden), exactly like an ineligible order;
- the flag removes the line regardless of remaining quantity.

Out of the box every product is returnable. To enable the flag, have your Sylius `Product` model implement [`NonReturnableProductInterface`](src/Entity/NonReturnableProductInterface.php) and apply [`NonReturnableProductTrait`](src/Entity/NonReturnableProductTrait.php) (the trait supplies the `non_returnable` column and accessors):

```
// src/Entity/Product/Product.php
use Doctrine\ORM\Mapping as ORM;
use Madcoders\SyliusRmaPlugin\Entity\NonReturnableProductInterface;
use Madcoders\SyliusRmaPlugin\Entity\NonReturnableProductTrait;
use Sylius\Component\Core\Model\Product as BaseProduct;

#[ORM\Entity]
#[ORM\Table(name: 'sylius_product')]
class Product extends BaseProduct implements NonReturnableProductInterface
{
    use NonReturnableProductTrait;
}
```

Then run the plugin migration (it adds the `non_returnable` column to `sylius_product`). The checkbox is added to the admin product form automatically (a form-type extension, rendered via the `sylius.admin.product.tab_details` template event) once the model implements the interface. To source the flag from somewhere other than the product entity, replace [`ProductReturnabilityCheckerInterface`](#customizations).

Returns state machine
---------------------

[](#returns-state-machine)

Every return form is an `OrderReturn` entity driven by a single [winzou state machine](src/Resources/config/config.yml) (graph `return_status`, property `orderReturnStatus`). A form is created in `draft` and then moves through the graph depending on whether the customer is filing a **return** or **withdrawing**(cancelling) a pre-shipment order. The same graph also carries the admin-side resolution of a withdrawal request.

States: `draft`, `new`, `completed`, `canceled`, `withdrawal_request`, `withdrawn`.

### Full graph

[](#full-graph)

 ```
stateDiagram-v2
    [*] --> draft: form created

    draft --> new: new
    new --> completed: complete

    draft --> canceled: cancel
    new --> canceled: cancel

    draft --> withdrawn: withdraw
    draft --> withdrawal_request: request_withdrawal
    withdrawal_request --> withdrawn: withdraw
    withdrawal_request --> new: fallback_to_return

    completed --> [*]
    canceled --> [*]
    withdrawn --> [*]
```

      Loading ### Standard return flow

[](#standard-return-flow)

A customer fills in the return form for a delivered order. On submit the `new` transition moves the form out of `draft`; an admin then either completes or cancels it.

 ```
stateDiagram-v2
    [*] --> draft: customer starts return form
    draft --> new: new (form submitted)
    new --> completed: complete (admin)
    new --> canceled: cancel (admin)
    draft --> canceled: cancel
    completed --> [*]
    canceled --> [*]
```

      Loading ### Withdrawal (pre-shipment) flow

[](#withdrawal-pre-shipment-flow)

A withdrawal always ends in the terminal `withdrawn` state. Whether it gets there instantly (with the whole Sylius order cancelled) or via admin approval is decided by two checkers:

- [`WithdrawalEligibilityChecker::isWithdrawable()`](src/Services/Withdrawal/WithdrawalEligibilityChecker.php)
    - is the withdrawal flow offered at all? (placed, not shipped, not a cart; unpaid only when the `allow_unpaid_withdrawal` flag is on).
- [`InstantCancellationEligibilityChecker::isEligible()`](src/Services/Withdrawal/InstantCancellationEligibilityChecker.php)
    - true when the order is **not paid**, so it can be withdrawn instantly with nothing to refund.

When instant-eligible (unpaid), the customer confirms in one click and the form fast-forwards straight to `withdrawn` via the `withdraw` transition, cancelling the whole Sylius order.

When paid/authorized, the customer instead sees the **same item-selection screen as a standard return** ([`WithdrawalReturnFormType`](src/Form/Type/WithdrawalReturnFormType.php) over `Return/view.html.twig`) and can choose which items/quantities to withdraw - a **partial withdrawal**is allowed. Submitting raises a `withdrawal_request` that an admin resolves: confirming it (`withdraw` -&gt; `withdrawn`) records the withdrawal but **does not** cancel the Sylius order, because the request may be partial - the refund and any order cancellation stay manual admin actions; or handling it as a normal return (`fallback_to_return` -&gt; `new`). Either way the withdrawn quantities are claimed and cannot be returned again (`MaxQtyCalculator` counts every non-`draft` return).

 ```
stateDiagram-v2
    [*] --> draft: customer requests withdrawal
    state instant_eligible
    draft --> instant_eligible
    instant_eligible --> withdrawn: withdraw\n(unpaid, whole order cancelled)
    instant_eligible --> withdrawal_request: request_withdrawal\n(paid, item selection, needs approval)
    withdrawal_request --> withdrawn: withdraw (admin confirm, order NOT cancelled - manual refund)
    withdrawal_request --> new: fallback_to_return (admin, handle as return)
    withdrawn --> [*]
```

      Loading ### Transitions and notifications

[](#transitions-and-notifications)

Several transitions fire `after` callbacks (changelog updates and customer e-mails), configured in [`config.yml`](src/Resources/config/config.yml). The `withdraw` transition uses winzou `from`-filtered callbacks so the instant (customer) and admin-approved cases send different notifications:

TransitionFromToAfter callback`new``draft``new`-`complete``new``completed`changelog update`cancel``draft`, `new``canceled`changelog update`request_withdrawal``draft``withdrawal_request`withdrawal-requested e-mail`withdraw``draft``withdrawn`instant withdrawal e-mail (customer)`withdraw``withdrawal_request``withdrawn`resolution (confirmed) e-mail (admin)`fallback_to_return``withdrawal_request``new`resolution (fallback) e-mailCustomizations
--------------

[](#customizations)

Which RMA path an order is offered - return, withdrawal, or instant withdrawal - is decided by small single-method **eligibility checkers**. Each one is bound to an interface (with a default service alias) and is consumed everywhere through that interface (controllers, the [`rma_order_can_start_rma()`](#returns-state-machine) Twig function, the reason provider), so you can change a rule for the whole plugin by pointing the alias at your own implementation.

Interface (`Madcoders\SyliusRmaPlugin\Services\...`)MethodDecidesDefault service id (`madcoders.sylius_rma_plugin.services...`)`ReturnEligibilityCheckerInterface``isReturnable(OrderInterface)`whether a (post-shipment) order can be returned`.return_eligibility_checker``Withdrawal\WithdrawalEligibilityCheckerInterface``isWithdrawable(OrderInterface)`whether a (pre-shipment) order is offered withdrawal at all`.withdrawal.eligibility_checker``Withdrawal\InstantCancellationEligibilityCheckerInterface``isEligible(OrderInterface)`within withdrawal, instant (unpaid) vs admin approval (paid)`.withdrawal.instant_cancellation_eligibility_checker``Reason\ReturnReasonEligibilityCheckerInterface``isEligible(OrderInterface, OrderReturnReasonInterface)`whether a given return reason is offered (deadline since shipment)`.reason.return_reason_eligibility_checker``ProductReturnabilityCheckerInterface``isReturnable(ProductVariantInterface)`whether a single ordered variant may be added to a return (item-level)`.product_returnability_checker`To replace one, implement its interface and alias it in your application:

```
# config/services.yaml
services:
    App\Rma\MyWithdrawalEligibilityChecker: ~

    # take over the rule everywhere it is used
    Madcoders\SyliusRmaPlugin\Services\Withdrawal\WithdrawalEligibilityCheckerInterface:
        alias: App\Rma\MyWithdrawalEligibilityChecker
```

Prefer to keep the default behaviour and only add to it? Decorate the default service instead:

```
services:
    App\Rma\MyWithdrawalEligibilityChecker:
        decorates: madcoders.sylius_rma_plugin.services.withdrawal.eligibility_checker
        arguments: ['@.inner']
```

Note that the default `WithdrawalEligibilityChecker` receives the [`allow_unpaid_withdrawal`](#configuration) flag as a constructor argument; a full replacement is responsible for honouring that flag itself if it still applies.

Development
-----------

[](#development)

Requires PHP 8.2, Composer, Docker (for the database) and Node/Yarn (for the test application assets). Run `make help` to list every available command.

### Quick start

[](#quick-start)

```
make setup        # composer install + start MySQL (docker) + build assets + create schema
make test         # PHPUnit + non-JS Behat
make static       # PHPStan + ECS
```

`make setup` is a one-shot bootstrap. It is equivalent to:

```
make install      # composer install
make docker-up    # start the MySQL 8 container on host port 3307
make frontend     # yarn install + encore build + assets:install
make backend      # create the test database and schema
```

### Docker

[](#docker)

The bundled `docker-compose.yml` provides the services the test suite needs:

```
make docker-up        # MySQL 8 only (host port 3307, to avoid a local MySQL on 3306)
make docker-up-all    # MySQL 8 + headless Chrome (for the @javascript Behat suite)
make docker-down      # stop and remove the containers
```

The test application reads `DATABASE_URL=mysql://root:rma@127.0.0.1:3307/...` from `tests/Application/.env`.

### Tests

[](#tests)

```
make phpunit          # unit / component tests
make behat            # non-JavaScript Behat suite
make behat-js         # JavaScript Behat suite (needs `make docker-up-all` + a running server)
```

### Static analysis &amp; code style

[](#static-analysis--code-style)

```
make static           # phpstan + ecs (no changes)
make fix              # auto-fix code style with ECS
```

PHPStan runs against a committed baseline (`phpstan-baseline.neon`) so only new issues fail the build.

### Pre-commit hook

[](#pre-commit-hook)

A git hook auto-fixes code style and verifies static analysis and unit tests before each commit. Install it once per clone:

```
make install-hooks    # sets git core.hooksPath to .githooks
```

On every `git commit` it runs `make pre-commit`, which:

1. runs `ecs --fix` on staged PHP files and re-stages the result,
2. runs PHPStan, ECS and PHPUnit, aborting the commit if any of them fail.

Bypass it in an emergency with `git commit --no-verify`.

### Commit messages &amp; changelog

[](#commit-messages--changelog)

Commits follow [Conventional Commits](https://www.conventionalcommits.org/)(see [ADR 0009](docs/adr-log/0009-conventional-commits.md)); `make install-hooks` also installs the `.gitmessage` template. Notable changes are recorded in [CHANGELOG.md](CHANGELOG.md) under `[Unreleased]`.

- See also [How to contribute](docs/CONTRIBUTING.md)

License
-------

[](#license)

This library is under the [EUPL 1.2](LICENSE) license.

Credits
-------

[](#credits)

[![madcoders logo](docs/img/madcoders-logo-slogan.png)](docs/img/madcoders-logo-slogan.png)

Developed by [MADCODERS](https://madcoders.co)
Architects of this package:

- [Piotr Lewandowski](https://github.com/plewandowski)
- [Leonid Moshko](https://github.com/LeoMoshko)

[![Buy Me A Coffee](https://camo.githubusercontent.com/0cf29a542375e1a46e84d8bf5805a4e5c0a6ee98b6547ccdc0c55eed49d99c69/68747470733a2f2f63646e2e6275796d6561636f666665652e636f6d2f627574746f6e732f76322f64656661756c742d79656c6c6f772e706e67)](https://www.buymeacoffee.com/madcoders)

###  Health Score

49

—

FairBetter than 94% of packages

Maintenance86

Actively maintained with recent releases

Popularity19

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity67

Established project with proven stability

 Bus Factor1

Top contributor holds 70.7% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~416 days

Total

5

Last Release

25d ago

PHP version history (2 changes)1.0.0PHP ^7.3

1.1.0PHP ^8.2

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/412343?v=4)[madcoders](/maintainers/madcoders)[@MADCoders](https://github.com/MADCoders)

---

Top Contributors

[![plewandowski](https://avatars.githubusercontent.com/u/2155836?v=4)](https://github.com/plewandowski "plewandowski (273 commits)")[![LeoMoshko](https://avatars.githubusercontent.com/u/9072581?v=4)](https://github.com/LeoMoshko "LeoMoshko (113 commits)")

---

Tags

rmasyliussylius-pluginsyliussylius-plugin

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Psalm

Type Coverage Yes

### Embed Badge

![Health badge](/badges/madcoders-sylius-rma-plugin/health.svg)

```
[![Health](https://phpackages.com/badges/madcoders-sylius-rma-plugin/health.svg)](https://phpackages.com/packages/madcoders-sylius-rma-plugin)
```

###  Alternatives

[sylius/refund-plugin

Plugin provides basic refunds functionality for Sylius application.

701.8M20](/packages/sylius-refund-plugin)[sylius/invoicing-plugin

Invoicing plugin for Sylius.

891.1M2](/packages/sylius-invoicing-plugin)[stefandoorn/sitemap-plugin

Sitemap Plugin for Sylius

841.1M1](/packages/stefandoorn-sitemap-plugin)[monsieurbiz/sylius-rich-editor-plugin

A Rich Editor plugin for Sylius.

75416.2k6](/packages/monsieurbiz-sylius-rich-editor-plugin)[odiseoteam/sylius-vendor-plugin

Vendor plugin for Sylius. Add Vendor (Brand) to your products

6068.4k1](/packages/odiseoteam-sylius-vendor-plugin)[synolia/sylius-scheduler-command-plugin

Scheduler Command Plugin.

35389.6k](/packages/synolia-sylius-scheduler-command-plugin)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
