mage-os / module-rma
Return Merchandise Authorization
Requires
- php: >=8.3
- magento/framework: *
README
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 |