MageOS RMA
mage-os/module-rma
Adds a complete Return Merchandise Authorization system to Magento 2, allowing customers to request returns from their account and admins to process RMAs with configurable statuses, reasons, and resolution workflows.
Build Tests
Code Quality
Tested on Magento 2.4.8-p4
Recent Test History
Each release is tested against the latest Magento version at that time.
Top Contributors
View LeaderboardShare This Module's Status
README
Loaded from GitHubMageOS_RMA
Return Merchandise Authorization (RMA) module for Magento / MageOS.
Installation
Via Composer
composer require mage-os/module-rma
Enable the module
bin/magento module:enable MageOS_RMA
bin/magento setup:upgrade
bin/magento cache:flush
The
setup:upgradecommand creates the database tables and inserts default data (status, reason, resolution type, item condition) via Data Patches.
Configuration
Module settings are located at Stores > Configuration > Sales > RMA - Return Management.
General
| Field | Type | Default | Description |
|---|---|---|---|
| Enable RMA | Yes/No | Yes | Enable or disable the RMA feature (scope: website) |
| Increment ID Prefix | Text | RMA- |
Prefix for the return increment ID (e.g. RMA-000001) |
| Return Period (Days) | Numeric | 30 |
Number of days after order placement within which a return can be requested |
Policy
| Field | Type | Default | Description |
|---|---|---|---|
| Auto-Approve Returns | Yes/No | No | When enabled, new return requests are automatically approved |
| Allowed Order Statuses | Multiselect | complete |
Only orders with these statuses can have a return request |
Email Notifications
| Field | Type | Default | Description |
|---|---|---|---|
| Email Sender | Select | General Contact |
Sender identity for RMA emails |
| New RMA Email Template (Customer) | Select | — | Email template sent to the customer when a new return is created |
| Status Change Email Template (Customer) | Select | — | Email template sent to the customer on return status change |
| New RMA Email Template (Admin) | Select | — | Email template sent to the admin when a new return is created |
| Admin Notification Email | Text (email) | — | Email address to receive admin notifications about returns |
Attachments
| Field | Type | Default | Description |
|---|---|---|---|
| Allowed File Extensions | Text | jpg,jpeg,png,gif,webp,mp4,mov,pdf,doc,docx,zip |
Comma-separated list of allowed file extensions |
| Maximum File Size (MB) | Numeric | 10 |
Maximum allowed file size in megabytes per single file |
| Maximum Files Per Upload | Numeric | 5 |
Maximum number of files allowed per RMA creation or comment |
Admin Area
Menu
Module entries are located under Sales > RMA:
- RMA Requests — main grid with all return requests
- Statuses — manage the return lifecycle statuses
- Reasons — manage return reasons
- Resolution Types — manage resolution types (refund, replacement, etc.)
- Item Conditions — manage item conditions (opened, sealed, etc.)
Creating an RMA from admin
- Go to Sales > RMA > RMA Requests, click Add New RMA
- Search and select an order in the Order field (only shows orders from websites with RMA enabled)
- Customer fields (name, email) and store are automatically filled from the order
- Select reason and resolution type
- In the Items to Return section, select the order items to include in the return, specifying quantity and condition for each
- Save — the system automatically generates an increment ID (e.g.
RMA-000001) and sends notification emails
Edit RMA
In edit mode, the form shows order and item information as read-only. You can modify status, reason, resolution type and admin notes.
Changing the status automatically sends a notification email to the customer.
Attachments
The RMA edit page displays a unified Attachments section above the comments timeline. This section shows all attachments associated with the RMA, regardless of whether they were uploaded at RMA creation or within a comment.
- Each attachment has a download link and a delete button
- Deleting an attachment from the unified section also removes it from the corresponding comment in the timeline (and vice versa)
- The section updates dynamically when new comments with attachments arrive via polling
Comments / Chat
The RMA edit page includes a Comments section that enables communication between admin and customer.
- Admin can write comments visible to the customer or internal notes (not visible to the customer) via the "Visible to Customer" checkbox
- Internal notes display an Internal Note badge in the timeline
- Admin can attach files to comments via drag & drop or file picker
- Comments update in real time via AJAX polling with progressive backoff (10s → 30s → 60s)
- Polling pauses when the browser tab is not visible and resumes when it becomes active again
- Messages can be sent with Ctrl+Enter in addition to the button
Frontend — Customer
My Returns
A My Returns link appears in the customer account sidebar. The page shows all of the customer's returns sorted by date descending, with pagination.
Table columns:
| Column | Description |
|---|---|
| RMA # | Return increment ID (e.g. RMA-000001) |
| Order # | Associated order increment ID |
| Status | Current return status (translated per store view) |
| Created At | Creation date |
| Action | View link to the detail page |
The Request Return button at the top leads to the creation form.
RMA Detail
The page displays return information and the items table:
- General information: increment ID, order, status, reason, preferred resolution, creation and update dates
- Items table: product name, SKU, requested quantity, item condition
- Attachments section: all attachments uploaded across the RMA lifecycle (creation and comments) displayed in a unified list with download links. The section is always present and updates dynamically when new comments with attachments arrive
- All labels (status, reason, resolution, condition) are translated according to the current store view
Customer Comments / Chat
Below the detail section there is a comments area that allows the customer to communicate with support:
- The customer only sees comments marked as visible (admin internal notes are not shown)
- Admin messages display a Support badge
- Customers can attach files to comments via drag & drop or file picker
- Attachments are also shown inline within each comment for context
- Same real-time polling mechanism as the admin side (backoff 10s → 30s → 60s, pauses on hidden tab)
- Submit with Ctrl+Enter or the Send button
Creating an RMA from the customer area
- From My Returns, click Request Return
- Select an order from the dropdown (only eligible orders are shown — see Eligibility section)
- On order change, available items are loaded via AJAX
- For each item: check the checkbox, specify quantity and condition
- Select reason and preferred resolution
- Optionally attach files via drag & drop or file picker (allowed extensions and size limits are configurable — see Attachments configuration)
- Click Submit Return Request
If arriving from the Request Return button on the order detail page, the order is pre-selected and the dropdown is disabled.
"Request Return" button on order detail
On the customer order detail page (Sales > My Orders > View Order), a Request Return button appears in the action bar if the order is eligible for a return. Clicking it leads to the creation form with the order pre-selected.
Frontend — Guest
Guests (orders without an account) can request a return from the guest order detail page:
- Access the guest order detail via Orders and Returns (order number + email/ZIP)
- If the order is eligible, the Request Return button appears
- The form is identical to the logged-in customer form, but without the order dropdown (the order is already determined)
- After submission, the return is created with
customer_id = null
Guests do not have a "My Returns" section — they can only create returns from the order detail page.
Order Eligibility for RMA
An order is eligible for a return request if all of these conditions are met:
-
RMA enabled — the module is enabled for the order's website (
isEnabled()) - Allowed status — the order status is among those configured in "Allowed Order Statuses"
-
Return period — the order date is within the period configured in "Return Period (Days)". If the period is
0, returns are always allowed (no time limit) - Available items — the order has at least one item with remaining returnable quantity (qty ordered − qty already requested in other RMAs > 0). Virtual items, downloadable items, and parent items of configurable/bundle products are excluded
The logic is centralized in the Service\OrderEligibility service with the following methods:
| Method | Description |
|---|---|
isOrderEligible(OrderInterface $order): bool |
Checks all 4 conditions |
getEligibleItems(OrderInterface $order): array |
Returns items with available quantity |
getCustomerEligibleOrders(int $customerId, int $storeId): Collection |
Customer orders matching conditions 1-3 |
Repositories and Service Contracts
Each entity exposes a repository with interfaces in Api/:
| Interface | Implementation | Methods |
|---|---|---|
RMARepositoryInterface |
Model\RMARepository |
get, getByIncrementId, save, delete, deleteById, getList |
StatusRepositoryInterface |
Model\StatusRepository |
get, save, delete, deleteById, getList |
ReasonRepositoryInterface |
Model\ReasonRepository |
get, save, delete, deleteById, getList |
ResolutionTypeRepositoryInterface |
Model\ResolutionTypeRepository |
get, save, delete, deleteById, getList |
ItemConditionRepositoryInterface |
Model\ItemConditionRepository |
get, save, delete, deleteById, getList |
ItemRepositoryInterface |
Model\ItemRepository |
get, save, delete, deleteById, getList |
CommentRepositoryInterface |
Model\CommentRepository |
get, save, delete, deleteById, getList |
All getList methods support SearchCriteriaInterface for filtering, sorting and pagination.
Business Events
The module dispatches custom events in RMARepository::save() to allow other modules to react to return lifecycle changes.
How dispatching works
-
Creation: when a new RMA is saved,
rma_created_afterfires -
Status change: when the status changes, two events fire in sequence:
-
rma_status_change_after(generic, fires on any transition) - A specific semantic event based on the new status (e.g.
rma_approved_after)
-
Events table
| Event | When it fires | Available data |
|---|---|---|
rma_created_after |
New RMA created | rma |
rma_status_change_after |
Any status change | rma, old_status_id, new_status_id |
rma_approved_after |
Status → Approved | rma, old_status_id, new_status_id |
rma_rejected_after |
Status → Rejected | rma, old_status_id, new_status_id |
rma_shipped_by_customer_after |
Status → Shipped by Customer | rma, old_status_id, new_status_id |
rma_received_after |
Status → Received by Admin | rma, old_status_id, new_status_id |
rma_canceled_after |
Status → Canceled by Customer | rma, old_status_id, new_status_id |
rma_resolved_after |
Status → Resolved | rma, old_status_id, new_status_id |
Example: Observer to book a carrier when a return is approved
etc/events.xml in your custom module:
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="rma_approved_after">
<observer name="my_module_book_carrier"
instance="MyVendor\MyModule\Observer\BookCarrierPickup"/>
</event>
</config>
Observer/BookCarrierPickup.php:
<?php
declare(strict_types=1);
namespace MyVendor\MyModule\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use MageOS\RMA\Api\Data\RMAInterface;
class BookCarrierPickup implements ObserverInterface
{
public function execute(Observer $observer): void
{
/** @var RMAInterface $rma */
$rma = $observer->getData('rma');
$oldStatusId = $observer->getData('old_status_id');
$newStatusId = $observer->getData('new_status_id');
// Your carrier booking logic here
// $rma->getCustomerEmail(), $rma->getOrderId(), etc.
}
}
Example: Generic observer to log all status changes
<event name="rma_status_change_after">
<observer name="my_module_log_status_change"
instance="MyVendor\MyModule\Observer\LogStatusChange"/>
</event>
<?php
declare(strict_types=1);
namespace MyVendor\MyModule\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Psr\Log\LoggerInterface;
class LogStatusChange implements ObserverInterface
{
public function __construct(
protected readonly LoggerInterface $logger
) {
}
public function execute(Observer $observer): void
{
$rma = $observer->getData('rma');
$this->logger->info('RMA status changed', [
'rma_id' => $rma->getEntityId(),
'increment_id' => $rma->getIncrementId(),
'old_status_id' => $observer->getData('old_status_id'),
'new_status_id' => $observer->getData('new_status_id'),
]);
}
}
REST API
All endpoints require an admin integration token (Authorization: Bearer <token>) and are protected by ACL resources.
RMA Entity
| Method | Endpoint | ACL | Description |
|---|---|---|---|
GET |
/V1/rma/:entityId |
MageOS_RMA::rma_manage |
Get RMA by ID |
GET |
/V1/rma/increment-id/:incrementId |
MageOS_RMA::rma_manage |
Get RMA by increment ID |
POST |
/V1/rma |
MageOS_RMA::rma_manage |
Create RMA |
PUT |
/V1/rma/:entityId |
MageOS_RMA::rma_manage |
Update RMA |
DELETE |
/V1/rma/:entityId |
MageOS_RMA::rma_manage |
Delete RMA |
GET |
/V1/rma/search |
MageOS_RMA::rma_manage |
Search RMAs (SearchCriteria) |
RMA Items
Items are scoped under their parent RMA.
| Method | Endpoint | ACL | Description |
|---|---|---|---|
GET |
/V1/rma/:rmaId/items |
MageOS_RMA::rma_manage |
List items for an RMA (SearchCriteria) |
POST |
/V1/rma/:rmaId/items |
MageOS_RMA::rma_manage |
Add item to an RMA |
DELETE |
/V1/rma/:rmaId/items/:itemId |
MageOS_RMA::rma_manage |
Remove item from an RMA |
RMA Comments
Comments are scoped under their parent RMA.
| Method | Endpoint | ACL | Description |
|---|---|---|---|
GET |
/V1/rma/:rmaId/comments |
MageOS_RMA::rma_manage |
List comments for an RMA (SearchCriteria) |
POST |
/V1/rma/:rmaId/comments |
MageOS_RMA::rma_manage |
Add comment to an RMA |
Lookup Entities
Each lookup entity (status, reason, resolution type, item condition) exposes full CRUD endpoints.
Statuses
| Method | Endpoint | ACL |
|---|---|---|
GET |
/V1/rma/status/:entityId |
MageOS_RMA::rma_status |
POST |
/V1/rma/status |
MageOS_RMA::rma_status |
PUT |
/V1/rma/status/:entityId |
MageOS_RMA::rma_status |
DELETE |
/V1/rma/status/:entityId |
MageOS_RMA::rma_status |
GET |
/V1/rma/status/search |
MageOS_RMA::rma_status |
Reasons
| Method | Endpoint | ACL |
|---|---|---|
GET |
/V1/rma/reason/:entityId |
MageOS_RMA::rma_reason |
POST |
/V1/rma/reason |
MageOS_RMA::rma_reason |
PUT |
/V1/rma/reason/:entityId |
MageOS_RMA::rma_reason |
DELETE |
/V1/rma/reason/:entityId |
MageOS_RMA::rma_reason |
GET |
/V1/rma/reason/search |
MageOS_RMA::rma_reason |
Resolution Types
| Method | Endpoint | ACL |
|---|---|---|
GET |
/V1/rma/resolution-type/:entityId |
MageOS_RMA::rma_resolution_type |
POST |
/V1/rma/resolution-type |
MageOS_RMA::rma_resolution_type |
PUT |
/V1/rma/resolution-type/:entityId |
MageOS_RMA::rma_resolution_type |
DELETE |
/V1/rma/resolution-type/:entityId |
MageOS_RMA::rma_resolution_type |
GET |
/V1/rma/resolution-type/search |
MageOS_RMA::rma_resolution_type |
Item Conditions
| Method | Endpoint | ACL |
|---|---|---|
GET |
/V1/rma/item-condition/:entityId |
MageOS_RMA::rma_item_condition |
POST |
/V1/rma/item-condition |
MageOS_RMA::rma_item_condition |
PUT |
/V1/rma/item-condition/:entityId |
MageOS_RMA::rma_item_condition |
DELETE |
/V1/rma/item-condition/:entityId |
MageOS_RMA::rma_item_condition |
GET |
/V1/rma/item-condition/search |
MageOS_RMA::rma_item_condition |
GraphQL API
The module provides GraphQL queries and mutations for headless frontend integration.
Queries
customerReturns
Returns a paginated list of returns for the logged-in customer. Requires customer token.
query {
customerReturns(pageSize: 10, currentPage: 1) {
items {
rma_id
increment_id
order_number
status { code label }
created_at
}
total_count
page_info { page_size current_page total_pages }
}
}
customerReturn
Returns a single return by ID for the logged-in customer. Requires customer token.
query {
customerReturn(rma_id: 1) {
rma_id
increment_id
order_number
status { id code label }
reason { id code label }
resolution_type { id code label }
items {
product_name
product_sku
qty_requested
condition { code label }
}
comments {
comment_id
author_type
author_name
comment
created_at
}
}
}
guestReturn
Returns a single return for a guest order, authenticated by order number and email.
query {
guestReturn(order_number: "000000001", email: "guest@example.com", rma_id: 1) {
rma_id
increment_id
status { code label }
items { product_name qty_requested }
}
}
returnComments
Returns a paginated list of customer-visible comments for a return. Requires customer token.
query {
returnComments(rma_id: 1, pageSize: 50, currentPage: 1) {
items {
comment_id
author_type
author_name
comment
created_at
}
total_count
}
}
Mutations
createCustomerReturn
Creates a return for a customer order. Requires customer token.
mutation {
createCustomerReturn(input: {
order_id: 1
reason_id: 1
resolution_type_id: 1
items: [
{ order_item_id: 1, qty_requested: 1, condition_id: 1 }
]
}) {
return {
rma_id
increment_id
status { code label }
}
}
}
createGuestReturn
Creates a return for a guest order, authenticated by order number and email.
mutation {
createGuestReturn(input: {
order_number: "000000001"
email: "guest@example.com"
reason_id: 1
resolution_type_id: 1
items: [
{ order_item_id: 1, qty_requested: 1, condition_id: 1 }
]
}) {
return {
rma_id
increment_id
status { code label }
}
}
}
addReturnComment
Adds a comment to a return. Requires customer token.
mutation {
addReturnComment(input: {
rma_id: 1
comment: "When will my refund be processed?"
}) {
comment_id
author_type
author_name
comment
created_at
}
}
Authentication
| Operation | Auth |
|---|---|
customerReturns, customerReturn, returnComments, addReturnComment |
Customer token (Authorization: Bearer <customer_token>) |
createCustomerReturn |
Customer token — ownership of the order is verified |
guestReturn, createGuestReturn |
No token — authenticated by order_number + email |
Types
All lookup fields (status, reason, resolution_type, condition) return a ReturnLookupValue with id, code and label. The label is store-localized based on the current store view header (Store).
Hyvä Theme Compatibility
This module is Hyvä-ready out of the box. It includes both Luma (default) and Hyvä templates within the same module — no separate compatibility module is needed.
How it works
The module ships hyva_ prefixed layout XML files alongside the standard Luma layout files. When running under a Hyvä theme, Magento automatically processes these hyva_* handles after the regular handles. The Hyvä layouts remove the Luma blocks and replace them with blocks pointing to Alpine.js + Tailwind CSS templates.
What's included
| Luma layout | Hyvä layout | Purpose |
|---|---|---|
customer_account.xml |
hyva_customer_account.xml |
"My Returns" sidebar link |
rma_customer_history.xml |
hyva_rma_customer_history.xml |
Returns list page |
rma_customer_view.xml |
hyva_rma_customer_view.xml |
Return detail + comments |
rma_customer_create.xml |
hyva_rma_customer_create.xml |
Create return form |
rma_guest_create.xml |
hyva_rma_guest_create.xml |
Guest return form |
sales_order_view.xml |
hyva_sales_order_view.xml |
"Request Return" button (customer) |
sales_guest_view.xml |
hyva_sales_guest_view.xml |
"Request Return" button (guest) |
Templates
Hyvä templates are located under view/frontend/templates/hyva/ and use:
- Alpine.js for all interactive behavior (AJAX item loading, comments polling, form validation)
- Tailwind CSS utility classes for styling
-
Native
fetch()instead of jQuery$.ajax() -
hyva.getCookie('form_key')for CSRF token retrieval instead of jQuery Cookie
The comments system implements the same smart polling mechanism as the Luma version (10s → 30s → 60s backoff, Visibility API pause/resume) using plain Alpine.js.
Tailwind Build
Hyvä automatically discovers and purges Tailwind classes from module templates. After installing or updating this module, rebuild Tailwind from your Hyvä theme:
cd app/design/frontend/<Vendor>/<theme>/web/tailwind
npm run build
Status Codes
Status codes are defined as constants in Model\RMA\StatusCodes:
| Constant | Value |
|---|---|
NEW_REQUEST |
new_request |
NEED_DETAILS |
need_details |
APPROVED |
approved |
REJECTED |
rejected |
SHIPPED_BY_CUSTOMER |
shipped_by_customer |
RECEIVED_BY_ADMIN |
received_by_admin |
CANCELED_BY_CUSTOMER |
canceled_by_customer |
RESOLVED |
resolved |
This content is fetched directly from the module's GitHub repository. We are not the authors of this content and take no responsibility for its accuracy, completeness, or any consequences arising from its use.