mage-os / module-rma

mage-os/module-rma

Return Merchandise Authorization

  • Samuele Martini
  • Davide Lunardon
magento2-module Compatibility: 2.4.8-2.4.9 Code Quality: Fail Tests: N/A Security: Pass MIT

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:upgrade command 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

  1. Go to Sales > RMA > RMA Requests, click Add New RMA
  2. Search and select an order in the Order field (only shows orders from websites with RMA enabled)
  3. Customer fields (name, email) and store are automatically filled from the order
  4. Select reason and resolution type
  5. In the Items to Return section, select the order items to include in the return, specifying quantity and condition for each
  6. 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

  1. From My Returns, click Request Return
  2. Select an order from the dropdown (only eligible orders are shown — see Eligibility section)
  3. On order change, available items are loaded via AJAX
  4. For each item: check the checkbox, specify quantity and condition
  5. Select reason and preferred resolution
  6. Optionally attach files via drag & drop or file picker (allowed extensions and size limits are configurable — see Attachments configuration)
  7. 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:

  1. Access the guest order detail via Orders and Returns (order number + email/ZIP)
  2. If the order is eligible, the Request Return button appears
  3. The form is identical to the logged-in customer form, but without the order dropdown (the order is already determined)
  4. 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:

  1. RMA enabled — the module is enabled for the order's website (isEnabled())
  2. Allowed status — the order status is among those configured in "Allowed Order Statuses"
  3. 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)
  4. 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

  1. Creation: when a new RMA is saved, rma_created_after fires
  2. 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 Yes to No at installation — RMA feature must now be explicitly enabled by the admin after install (#34)
  • README updated to reflect the new Enable RMA default value (#34)

[2.2.1] - 2026-04-27

Fixed

  • [Code Quality] Raw SQL getConnection()->fetchOne() in RMARepository::save() for old status detection replaced with repository pattern $this->get()->getStatusId() (#22)
  • [Code Quality] N+1 query in customer RMA history — ListRma::getOrderIncrementId() calling orderRepository->get() per row replaced with single LEFT JOIN on sales_order via new opt-in Collection::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), and ReturnDataProvider GraphQL 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 AttachmentService allowed 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.php source model backing the new multiselect config (#30)

[2.0.1] - 2026-04-09

Fixed

  • NoSuchEntityException resolved in wrong namespace in Block/Customer/Rma/ListRma.php — catch block never matched
  • SearchCriteriaBuilder::addSortOrder() called with two strings instead of SortOrder object in AbstractRmaManagement — runtime error when sorting items/comments via management layer

[2.0.0] - 2026-04-08

Breaking Changes

  • RmaSubmitService::saveItems() now requires OrderInterface $order as third parameter
  • AbstractRmaManagement now injects SearchCriteriaBuilderFactory instead of SearchCriteriaBuilder (affects RmaItemManagement and RmaCommentManagement constructors)

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.json missing rma_attachment table and indexes for reason_id/resolution_type_id (#12)
  • [Infrastructure] AutoApproveRma observer causing redundant double-save — removed, service already handles it (#15)
  • [Code Quality] SearchCriteriaBuilder shared instance mutated across calls in AbstractRmaManagement (#23)
  • [Code Quality] Frontend Luma templates using inline styles — extracted to LESS classes (#25)
  • [API] getStoreLabels() return type array not Web API serializable — typed as string[] (#21)
  • [i18n] 65 missing translation strings added to all 4 locale CSVs (#27)

Added

  • @api annotation on all 27 interfaces (#16)
  • Sequence dependencies in module.xml for Magento_Sales, Magento_Webapi, Magento_GraphQl (#13)
  • Composer require for magento/module-sales, magento/module-webapi, magento/module-graph-ql (#13)
  • Indexes on rma_entity.reason_id and rma_entity.resolution_type_id (#12)
  • Sub-resource pattern documented in README for items and comments endpoints (#19)

Removed

  • Observer/AutoApproveRma.php and its event binding in events.xml (#15)

[1.3.1] - 2026-03-09

Fixed

  • JSON responses returning objects instead of arrays due to array_map preserving entity ID keys from $collection->getItems() — caused admin comment polling to silently fail and Hyvä comment save to throw TypeError on forEach
  • Comment save controllers now wrap everything in a catch-all Exception handler to guarantee JSON response even on unexpected errors
  • Comment attachments not appearing in real-time — toArray() was called before saveFromJson(), so attachments were not yet in DB when queried
  • Guest RMA creation not passing attachmentsJson to createRma() — 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() via basename()
  • File size validation before and after upload in uploadToTmp()
  • Inline edit mass assignment protection via field whitelists in abstract controller

Code Quality

  • foreach loops replaced with array_map/array_filter across 6 locations
  • AbstractHelper removed from ModuleConfig — now injects ScopeConfigInterface directly
  • Unused LoggerInterface removed from AttachmentService
  • json_encode/json_decode replaced with Magento\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 use imports
  • 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

  • @throws annotations on all methods that can throw (direct or indirect)
  • HttpPostActionInterface on AbstractLookupInlineEdit
  • Magic number constants BYTES_PER_KB, BYTES_PER_MB in AttachmentService
  • New i18n strings for error messages (4 languages)

[1.2.0] - 2026-03-04

Removed

  • 5 NewAction controllers (Rma, Status, Reason, ResolutionType, ItemCondition) — redundant forward('edit'), listing buttons now point directly to */*/edit
  • 3 entity-specific Actions columns (ReasonActions, ResolutionTypeActions, ItemConditionActions) — replaced by GenericEntityActions with di.xml virtual types

Added

  • GenericEntityActions UI component column with parameterized editUrlPath, deleteUrlPath, entityLabel — reused via 3 virtual types in di.xml

Fixed

  • $storeId undefined in Service\Email\Sender::sendRmaEmail() — now correctly resolved from $rma->getStoreId()

Changed

  • All constants switched to unqualified const with explicit type hints (const string, const array) across the entire module
  • CleanupCommand::getClosedStatusIds() condensed from foreach to array_map
  • StatusCommand::renderItems() inlined intermediate $conditionLabel variable
  • Removed 2 unused imports (ItemInterface in OrderItems block, SearchResultsInterface in 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)
  • AttachmentService with saveFromJson(), moveFromTmpAndSave(), getByRmaId(), toArray(), formatFileSize()
  • AttachmentConfigTrait for shared configuration getters across blocks
  • Database table rma_attachment with 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 $_idFieldName to all collections so Document::getId() resolves correctly in Filter::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

  • customerReturns query with pagination
  • customerReturn query for single RMA detail
  • guestReturn query for guest order returns
  • returnComments query for RMA chat history
  • createCustomerReturn mutation
  • createGuestReturn mutation
  • addReturnComment mutation

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 filters
  • bin/magento rma:status - Show RMA status details
  • bin/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
Versions
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.

Compatibility matrix (Magento × PHP)
Magento PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5
2.4.7 not tested Pass
2.4.8 Pass Pass
2.4.9 Pass Pass

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.

Static analysis results
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.

PHPStan results by Magento and PHP version
Magento PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5
2.4.7 N/A 45
2.4.8 45 45
2.4.9 45 45

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

Unit tests results by Magento and PHP version
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

Integration tests

Integration tests results by Magento and PHP version
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.

Security results
Tool Status Findings Summary
Composer audit Pass 0
Malware scan Pass 0
License
MIT
Authors

More from mage-os

View vendor
Make it pay

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.