mage-os / module-rma
mage-os/module-rma
Return Merchandise Authorization
MageOS_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 | No | 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 managed as a sub-resource of the parent RMA, following the same pattern as Magento core (e.g. OrderInterface / OrderItemRepositoryInterface). The GET /V1/rma/:entityId endpoint returns the RMA header only — to retrieve items, use the dedicated endpoints below.
| 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 managed as a sub-resource of the parent RMA, same as items above.
| 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: "[email protected]", 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: "[email protected]"
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 |
Changelog
[2.3.2] - 2026-06-18
Fixed
- Shortened long DB constraint names to comply with database limitations
Added
- Romanian translations for UI, emails, and configuration
[2.3.1] - 2026-06-05
Fixed
- Fix unqualified column reference in customer RMA list (#36)
[2.3.0] - 2026-05-10
Changed
- Enable RMA system configuration default switched from
YestoNoat installation — RMA feature must now be explicitly enabled by the admin after install (#34) - README updated to reflect the new
Enable RMAdefault value (#34)
[2.2.1] - 2026-04-27
Fixed
- [Code Quality] Raw SQL
getConnection()->fetchOne()inRMARepository::save()for old status detection replaced with repository pattern$this->get()->getStatusId()(#22) - [Code Quality] N+1 query in customer RMA history —
ListRma::getOrderIncrementId()callingorderRepository->get()per row replaced with single LEFT JOIN onsales_ordervia new opt-inCollection::joinSalesOrder()method (#24) - [Compatibility] PHP 8.5: nullable getter return values cast to
(int)/(string)before use as array keys in 5 admin DataProviders (RMA,Status,Reason,ResolutionType,ItemCondition), 3 UI listing columns (RmaActions,StatusActions,GenericEntityActions), andReturnDataProviderGraphQL lookup — prevents null-as-array-key warnings on PHP 8.5+
[2.2.0] - 2026-04-21
Fixed
- [Security] Trailing slash gap on absolute attachment paths in
AttachmentServiceallowed potential path-traversal edge cases (#32)
Added
- Unit test coverage for
AbstractRepository,RMARepository,StatusResolver,OrderEligibility,RmaSubmitService,AttachmentService, attachment upload, and customer attachment download (#32)
[2.1.0] - 2026-04-15
Fixed
- [Security] Allowed file extensions config switched from free-form comma list to multiselect with server-side mimetype validation in
AttachmentService(#30)
Added
Model/Config/Source/AllowedExtensions.phpsource model backing the new multiselect config (#30)
[2.0.1] - 2026-04-09
Fixed
NoSuchEntityExceptionresolved in wrong namespace inBlock/Customer/Rma/ListRma.php— catch block never matchedSearchCriteriaBuilder::addSortOrder()called with two strings instead ofSortOrderobject inAbstractRmaManagement— runtime error when sorting items/comments via management layer
[2.0.0] - 2026-04-08
Breaking Changes
RmaSubmitService::saveItems()now requiresOrderInterface $orderas third parameterAbstractRmaManagementnow injectsSearchCriteriaBuilderFactoryinstead ofSearchCriteriaBuilder(affectsRmaItemManagementandRmaCommentManagementconstructors)
Fixed
- [Security] Order item IDs not validated against submitted order (#6)
- [Security] No quantity validation against available qty — customers could request more than ordered (#7)
- [Data Integrity] RMA creation not transactional — partial saves on failure (#10)
- [Data Integrity] Store view deletion cascading to RMA records — changed FK to SET NULL (#11)
- [Data Integrity]
db_schema_whitelist.jsonmissingrma_attachmenttable and indexes forreason_id/resolution_type_id(#12) - [Infrastructure]
AutoApproveRmaobserver causing redundant double-save — removed, service already handles it (#15) - [Code Quality]
SearchCriteriaBuildershared instance mutated across calls inAbstractRmaManagement(#23) - [Code Quality] Frontend Luma templates using inline styles — extracted to LESS classes (#25)
- [API]
getStoreLabels()return typearraynot Web API serializable — typed asstring[](#21) - [i18n] 65 missing translation strings added to all 4 locale CSVs (#27)
Added
@apiannotation on all 27 interfaces (#16)- Sequence dependencies in
module.xmlforMagento_Sales,Magento_Webapi,Magento_GraphQl(#13) - Composer
requireformagento/module-sales,magento/module-webapi,magento/module-graph-ql(#13) - Indexes on
rma_entity.reason_idandrma_entity.resolution_type_id(#12) - Sub-resource pattern documented in README for items and comments endpoints (#19)
Removed
Observer/AutoApproveRma.phpand its event binding inevents.xml(#15)
[1.3.1] - 2026-03-09
Fixed
- JSON responses returning objects instead of arrays due to
array_mappreserving entity ID keys from$collection->getItems()— caused admin comment polling to silently fail and Hyvä comment save to throw TypeError onforEach - Comment save controllers now wrap everything in a catch-all
Exceptionhandler to guarantee JSON response even on unexpected errors - Comment attachments not appearing in real-time —
toArray()was called beforesaveFromJson(), so attachments were not yet in DB when queried - Guest RMA creation not passing
attachmentsJsontocreateRma()— caused silent data loss for guest attachments
Added
- Accepted file types info (extensions, max size, max files) displayed in comment upload dropzones (Luma, Hyvä, Admin)
[1.3.0] - 2026-03-08
Changed
Controller Deduplication (~500 lines removed)
- New abstract controllers:
AbstractLookupSave,AbstractLookupDelete,AbstractLookupInlineEdit,AbstractLookupMassDelete,AbstractLookupEdit - 20 concrete lookup controllers (Status, Reason, ResolutionType, ItemCondition × 5 actions) reduced to minimal subclasses
Security
- Path traversal protection in
AttachmentService::getAbsolutePath()— validates resolved path starts with base attachments directory - Filename sanitization in
moveFromTmpAndSave()viabasename() - File size validation before and after upload in
uploadToTmp() - Inline edit mass assignment protection via field whitelists in abstract controller
Code Quality
foreachloops replaced witharray_map/array_filteracross 6 locationsAbstractHelperremoved fromModuleConfig— now injectsScopeConfigInterfacedirectly- Unused
LoggerInterfaceremoved fromAttachmentService json_encode/json_decodereplaced withMagento\Framework\Serialize\Serializer\Json- Download logic deduplicated into
AttachmentService::createDownloadResponse() - Status label resolution moved inside
Sender::sendCustomerStatusChangeEmail() - Double order load eliminated in
ReturnDataProvider - Comment save error handling split: comment save and attachment save in separate try/catch blocks
- Hyvä Alpine
submitComment()now has proper error handling (catch + else branches) - FQCN inline references replaced with
useimports - Coding style fixes: spacing, import ordering, redundant annotations removed
$block->escape*()migrated to$escaper->escape*()in all 13 PHTML templates- FK validation added for status_id, reason_id, resolution_type_id in RMA save controllers
- FilterGroup OR semantics preserved in
AbstractRmaManagement::buildScopedSearchCriteria()
Added
@throwsannotations on all methods that can throw (direct or indirect)HttpPostActionInterfaceonAbstractLookupInlineEdit- Magic number constants
BYTES_PER_KB,BYTES_PER_MBinAttachmentService - New i18n strings for error messages (4 languages)
[1.2.0] - 2026-03-04
Removed
- 5
NewActioncontrollers (Rma, Status, Reason, ResolutionType, ItemCondition) — redundantforward('edit'), listing buttons now point directly to*/*/edit - 3 entity-specific Actions columns (ReasonActions, ResolutionTypeActions, ItemConditionActions) — replaced by
GenericEntityActionswith di.xml virtual types
Added
GenericEntityActionsUI component column with parameterizededitUrlPath,deleteUrlPath,entityLabel— reused via 3 virtual types in di.xml
Fixed
$storeIdundefined inService\Email\Sender::sendRmaEmail()— now correctly resolved from$rma->getStoreId()
Changed
- All constants switched to unqualified
constwith explicit type hints (const string,const array) across the entire module CleanupCommand::getClosedStatusIds()condensed from foreach toarray_mapStatusCommand::renderItems()inlined intermediate$conditionLabelvariable- Removed 2 unused imports (
ItemInterfacein OrderItems block,SearchResultsInterfacein AbstractRmaManagement)
[1.1.0] - 2026-02-22
Added
File Attachments
- File upload support at RMA creation (drag & drop or file picker) for both customer and guest
- File upload support in comments (admin and customer)
- Unified Attachments section on RMA detail page showing all attachments regardless of source (creation or comment)
- Attachments remain visible inline within each comment for context
- Dynamic update of unified section when new comments with attachments arrive via AJAX polling
- Admin can delete attachments from both the unified section and comment timeline (synchronized)
- Dedicated download controller with authorization checks (customer ownership, admin ACL)
- Temporary upload endpoint with server-side validation (extension, file size)
- Configurable allowed file extensions (default: jpg, jpeg, png, gif, webp, mp4, mov, pdf, doc, docx, zip)
- Configurable maximum file size per file (default: 10 MB)
- Configurable maximum files per upload (default: 5)
- Configuration available at Stores > Configuration > Sales > RMA - Return Management > Attachments
- Full Hyvä support with Alpine.js upload widget and dynamic unified section
- Shared JS utilities (
rma-utils.js,rma-file-upload.js) for Luma frontend and admin - Attachment translations for it_IT, fr_FR, de_DE, es_ES (24 new strings per language)
AttachmentServicewithsaveFromJson(),moveFromTmpAndSave(),getByRmaId(),toArray(),formatFileSize()AttachmentConfigTraitfor shared configuration getters across blocks- Database table
rma_attachmentwith fields: entity_id, rma_entity_id, comment_id (nullable), file_name, file_path, file_size
[1.0.2] - 2026-02-20
Fixed
- Added PHP version constraint to composer.json to reflect requirements
[1.0.1] - 2026-02-15
Fixed
- Admin mass delete not working on all grids (RMA, Status, Reason, Resolution Type, Item Condition) — added missing
$_idFieldNameto all collections soDocument::getId()resolves correctly inFilter::getFilterIds() - Multi-store order eligibility —
OrderEligibility::getCustomerEligibleOrders()now filters by all store IDs within the same website instead of exact store ID
[1.0.0] - 2026-02-13
Added
Core RMA Management
- Full RMA entity with auto-generated increment IDs (configurable prefix)
- RMA items linked to original order items with quantity tracking (requested, approved, returned)
- Chat-like comments system with real-time polling (smart backoff: 10s, 30s, 60s) and Page Visibility API
- Configurable return eligibility period (days) and allowed order statuses
Lookup Entities
- Status management with protected system statuses (
new_request,approved,rejected,items_received,closed) - Reason management (default: Defective, Wrong Item, Changed Mind, Not as Described)
- Resolution Type management (default: Refund, Replacement, Store Credit)
- Item Condition management (default: Unopened, Opened, Damaged)
- Multi-store label support for all lookup entities
Admin Panel
- RMA request grid with filtering, sorting, inline editing, and mass delete
- RMA edit form with status management and admin comments
- RMA creation form with order search, item selection, and quantity picker
- Full CRUD grids and forms for Status, Reason, Resolution Type, and Item Condition
- Inline editing support on all lookup grids
- ACL resources:
rma_manage,rma_status,rma_reason,rma_resolution_type,rma_item_condition
Customer Frontend
- "My Returns" section in customer account with paginated history
- RMA detail view with item list, status, and chat comments
- RMA creation form with dynamic order item loading (AJAX)
- "Request Return" link on order view page
Guest Support
- Guest RMA creation via order number + email lookup
- "Request Return" link on guest order view page
REST API
- Full CRUD endpoints for RMA entity (
/V1/rma) - RMA search by increment ID (
/V1/rma/increment-id/:incrementId) - RMA item management (
/V1/rma/:rmaId/items) - RMA comment management (
/V1/rma/:rmaId/comments) - Full CRUD + search for Status, Reason, Resolution Type, and Item Condition
- 30+ REST API endpoints total
GraphQL API
customerReturnsquery with paginationcustomerReturnquery for single RMA detailguestReturnquery for guest order returnsreturnCommentsquery for RMA chat historycreateCustomerReturnmutationcreateGuestReturnmutationaddReturnCommentmutation
Email Notifications
- Customer email on new RMA creation
- Customer email on RMA status change
- Admin email on new RMA creation
- Configurable sender identity and admin notification address
CLI Commands
bin/magento rma:list- List RMA requests with filtersbin/magento rma:status- Show RMA status detailsbin/magento rma:cleanup- Clean up old closed RMAs
System Configuration
- Enable/disable RMA module
- Increment ID prefix
- Return period (days)
- Auto-approve toggle
- Allowed order statuses for returns
- Email sender, templates, and admin notification address
Internationalization
- Italian (it_IT)
- French (fr_FR)
- German (de_DE)
- Spanish (es_ES)
Hyva Theme Compatibility
- Alpine.js + Tailwind CSS templates for all customer-facing pages
- Hyva-prefixed layout XML files with automatic template discovery
- Smart comment polling with
hyva.getCookie('form_key')for CSRF
Architecture
- Service layer:
OrderEligibility,RmaSubmitService,GuestOrderService,LabelResolver,CommentFormatter - Abstract base repository (
AbstractRepository) for all lookup entity repositories - Abstract admin controller (
AbstractLookupController) for lookup entity management - Generic admin button blocks (
GenericBackButton,GenericSaveButton,GenericDeleteButton) with di.xml virtual types - Event-driven architecture:
rma_created_after,rma_status_change_after, semantic status events - Data patches for default statuses, reasons, resolution types, and item conditions
- Shared JS in
view/base/web/js/for admin and frontend comment systems
| Version | Stability | QA Status | Compatibility | Released |
|---|---|---|---|---|
| 2.3.2 | stable | Fail | Magento 2.4.8-2.4.9 Details | 2026-06-18 13:50:13 |
| 2.3.1 | stable | Fail | Magento 2.4.8-2.4.9 Details | 2026-06-05 08:08:04 |
| 2.3.0 | stable | Not tested | Not yet tested Details | 2026-05-10 16:05:17 |
| 2.2.1 | stable | Not tested | Not yet tested Details | 2026-04-27 08:59:10 |
| 2.2.0 | stable | Not tested | Not yet tested Details | 2026-04-21 12:45:33 |
| 2.1.0 | stable | Not tested | Not yet tested Details | 2026-04-15 14:24:16 |
| 2.0.1 | stable | Not tested | Not yet tested Details | 2026-04-09 08:56:22 |
| 2.0.0 | stable | Not tested | Not yet tested Details | 2026-04-08 08:12:29 |
| 1.3.1 | stable | Not tested | Not yet tested Details | 2026-03-09 00:20:54 |
| 1.3.0 | stable | Not tested | Not yet tested Details | 2026-03-07 16:39:45 |
| 1.2.0 | stable | Not tested | Not yet tested Details | 2026-03-04 22:12:38 |
| 1.1.0 | stable | Not tested | Not yet tested Details | 2026-02-22 18:26:10 |
| 1.0.2 | stable | Not tested | Not yet tested Details | 2026-02-20 14:02:21 |
| 1.0.1 | stable | Not tested | Not yet tested Details | 2026-02-15 19:30:22 |
| 1.0.0 | stable | Not tested | Not yet tested Details | 2026-02-13 18:07:47 |
Requires 5
| Package | Constraint |
|---|---|
| magento/framework | * |
| magento/module-graph-ql | * |
| magento/module-sales | * |
| magento/module-webapi | * |
| php | >=8.3 |
Compatibility
Each Magento release line is installed on its supported PHP versions, then the module is built (DI compilation + static-content deploy) and its unit and integration suites are run. The matrix shows the lines and PHP versions the module is confirmed to install and run on. Code-quality results further down (phpstan, phpcs, …) are reported separately and never affect compatibility.
Code Quality
Advisory checks against the module's source. Static analysis runs once across the whole module; PHPStan re-runs per Magento + PHP version because resolvable symbols differ between releases. These NEVER affect the Compatibility badge — a phpcs finding can't make a module incompatible.
Static analysis
Coding standards (phpcs), mess detection (phpmd), copy-pasted code (cpd), PHP cross-version compatibility, composer.json validity. Each runs once for the whole module.
| Tool | Status | Findings | Summary |
|---|---|---|---|
| PHPCS | Fail | 328 | 8 errors, 320 warnings (ruleset: Magento2) |
| PHPMD | Warning | 64 | 64 rule violations (UnusedFormalParameter:43, MissingImport:11, TooManyPublicMethods:4, EmptyCatchBlock:3, NPathComplexity:2) |
| Cpd | Warning | 4 | 4 duplicated chunks spanning 276 total lines (min-lines=5, min-tokens=70) |
| Composer validate | Info | 4 | valid; 4 advisory notes (composer validate --strict) |
PHPStan
Type-checks the module's PHP against a real Magento install at the configured gate level. Re-runs per Magento and PHP version because resolvable symbols differ between releases. Cell → details modal.
Tests
Unit and integration suites, run for each applicable Magento and PHP version. A test failure speaks to the module's behaviour, not its compatibility with a Magento line, so it is reported here separately and never reddens the compatibility matrix.
Unit tests
Integration tests
| Magento | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 |
|---|---|---|---|---|
| 2.4.7 | N/A | N/A | ||
| 2.4.8 | N/A | N/A | ||
| 2.4.9 | N/A | N/A |
Security
Security checks run directly against the module: an audit of its declared dependencies for known vulnerabilities (composer audit) and a scan of its source for malware and web-shell signatures. Each runs once. A malware detection fails the version outright.
More from mage-os
View vendorGCP event sinks for mage-os/mageos-async-events
Combine the power of LLM and domain knowledge to improve admin experience though a chatbot UI.
Turn an existing module into recurring revenue.
If you already maintain a Magento 2 module on GitHub or GitLab, listing it on Packagento takes about five minutes. We mirror your tags, handle distribution signing, and route paid licenses through Stripe Connect, so you can keep shipping the way you already do.