artaza/module-rma

Returns (RMA) for Magento Open Source, which has no native RMA: the customer requests a return from their account, and an external system pulls it and pushes the decision back over REST.

Maintainers

Package info

github.com/martinartaza/magento_rma

Homepage

Type:magento2-module

pkg:composer/artaza/module-rma

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-08-14 15:12 UTC

This package is auto-updated.

Last update: 2026-08-15 02:33:16 UTC


README

Returns (RMA) for Magento Open Source, which has no native RMA — that feature only ships with Adobe Commerce.

The customer requests a return from their account (or, as a guest, from the order lookup), and the request is exposed over REST so an external system can pull it, decide, and push the outcome back. The storefront then shows the customer where their return stands.

This module decides nothing. It records the request, mirrors a status, and displays it. Every real decision — accept or reject, whether the goods go back to sellable stock, whether a credit note is issued — belongs to whatever system you plug in. That keeps the money and the stock in one place instead of split across two.

It ships ERP-agnostic: nothing here knows about any particular back office. It was built for the Magento 2 Connector for Odoo 19, but the contract is plain REST and two dispatched events, so anything can drive it.

How it works

Two entry points, and each one dispatches an event so you can hook in without touching this module.

flowchart TB
    C(["Customer — logged-in or guest"])

    subgraph MAG["Magento · Artaza_Rma"]
        direction TB
        F["Storefront form<br/>Controller/Order · Controller/Guest"]
        M["RmaManagement<br/>createRequest · updateStatus"]
        DB[("artaza_rma<br/>artaza_rma_item")]
        A["My Returns · admin grid<br/>badge + message"]
        F --> M --> DB --> A
    end

    E["External system (ERP)<br/>decides"]

    C -->|"1 · requests a return"| F
    M -.->|"2 · artaza_rma_request_created"| E
    DB -->|"3 · GET /V1/rma — cursor"| E
    E -->|"4 · POST /V1/rma/{id}/status"| M
    M -.->|"5 · artaza_rma_status_updated"| E
    A -->|"6 · sees where it stands"| C
Loading

The loop, in order

sequenceDiagram
    participant C as Customer
    participant M as Magento (Artaza_Rma)
    participant E as External system

    C->>M: Requests a return (items + qty + reason)
    Note over M: items rebuilt SERVER-SIDE from the order<br/>only the quantity comes from the customer
    M->>M: status = requested, dispatch<br/>artaza_rma_request_created

    loop cursor pull
        E->>M: GET /V1/rma?updated_at >= cursor (ASC)
        M-->>E: returns updated since the cursor
    end

    Note over E: the human decides:<br/>accept · reject · inspect · resolve
    E->>M: POST /V1/rma/42/status<br/>{status, adminMessage, creditAmount, couponCode}
    M->>M: save + dispatch artaza_rma_status_updated
    M-->>C: badge + message in My Returns
Loading

Status machine

requested is the only status born in Magento. Every other transition arrives from outside — this module records and displays them, it never decides.

stateDiagram-v2
    [*] --> requested: customer asks in Magento
    requested --> accepted: authorised
    requested --> rejected: refused, with a reason
    accepted --> in_transit: customer ships it back
    in_transit --> inspection: received
    inspection --> approved: passed
    inspection --> fraud: seal broken / tampered
    approved --> resolved_exchange: replacement sent
    approved --> resolved_credit: credit + coupon
    fraud --> returned: item sent back
    fraud --> held: kept in quarantine
    rejected --> [*]
    resolved_exchange --> [*]
    resolved_credit --> [*]
    returned --> [*]
    held --> [*]
Loading

A closed list. requested is the only one born in Magento; everything else arrives from outside.

Status Meaning
requested The customer opened the return
accepted Return authorised
rejected Refused — carries the reason shown to the customer (terminal)
in_transit The customer shipped the goods back
inspection Received, under review
approved Inspection passed
fraud Seal broken or tampered with
resolved_exchange Replacement delivered (terminal)
resolved_credit Credit issued, optionally with a coupon code (terminal)
returned The same item was sent back to the customer (terminal)
held Kept in quarantine (terminal)

What you get

Storefront

  • A Request a return button on the order view, for logged-in customers and guests. Guest authorisation uses Magento's own mechanism (Sales\Helper\Guest::loadValidOrder), so no new way in is invented.
  • A My Returns section in the customer account: list, detail, current status and the message the operator wrote. Status is shown as a coloured badge.
  • The items are rebuilt server-side from the order; only the quantity comes from the customer.

Admin

  • A returns grid and a detail view.

REST

Endpoint Purpose
GET /V1/rma?searchCriteria… Pull returns, typically by an updated_at cursor
POST /V1/rma/{rmaId}/status Push the decision back: status, message, credit amount, coupon

Both are guarded by the ACL resource Artaza_Rma::manage.

Eventsartaza_rma_request_created and artaza_rma_status_updated, so another module can react (send a mail, notify the ERP) without patching this one.

Install

composer require artaza/module-rma
bin/magento module:enable Artaza_Rma
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f
bin/magento cache:flush

setup:upgrade creates artaza_rma and artaza_rma_item (the child table cascades on delete, and updated_at is indexed so a cursor pull stays cheap).

To let an external system in: System ▸ Extensions ▸ Integrations, create one, grant Artaza_Rma::manage, activate it and use its access token as Authorization: Bearer <token>. After changing the ACL, reauthorize the integration or its calls keep coming back 401.

Driving it

Pull what changed since your cursor:

GET /rest/all/V1/rma?searchCriteria[filter_groups][0][filters][0][field]=updated_at
                    &searchCriteria[filter_groups][0][filters][0][value]=2026-08-01 00:00:00
                    &searchCriteria[filter_groups][0][filters][0][condition_type]=gteq
                    &searchCriteria[sortOrders][0][field]=updated_at
                    &searchCriteria[sortOrders][0][direction]=ASC

Sort ascending by updated_at: that is what makes the cursor self-healing — a run that dies half-way resumes from the last record it actually absorbed instead of skipping ahead.

Push the decision back:

POST /rest/all/V1/rma/42/status

{
  "status": "resolved_credit",
  "resolution": "credit",
  "adminMessage": "Approved. Store credit available for your next purchase.",
  "creditAmount": 43558.79,
  "couponCode": "RMA42-XY7Q",
  "odooReference": "NC-B 0001-00000123"
}

Everything except status is optional. adminMessage is what the customer reads, so a rejection should always carry one. Re-sending the same status is idempotent — the natural key is the RMA's increment_id.

Theme notes

The storefront templates target Hyvä. On a Luma-based theme the controllers, the model and the REST API work unchanged, but you will want to restyle the templates. Module styles live in view/frontend/web/css/module.css, not in the theme's build, so nothing is lost on a static-content:deploy.

Requirements

  • Magento 2.4.x (Open Source or Commerce)
  • PHP 8.1+

License

OSL-3.0 — the same license as the Magento core.

Sebastian Artaza · artaza.net · martin.artaza@gmail.com