orangecat / module-returns
Customer-facing RMA and return request management for Magento 2 B2B stores
Package info
github.com/olivertar/m2_returns
Type:magento2-module
pkg:composer/orangecat/module-returns
Requires
- php: >=8.1
- magento/framework: *
- orangecat/core: *
README
Customer-facing RMA and return request management for Magento 2 B2B stores.
Module: Orangecat_Returns
Version: 1.0.0
License: OSL-3.0
Author: Oliverio Gombert olivertar@gmail.com
Table of Contents
- Overview
- Theme Compatibility
- Requirements
- Installation
- What Gets Installed
- Return Eligibility Rules
- Configuration
- Store Admin Guide
- Buyer Guide (Frontend)
- Developer Guide
- REST API
- Frontend Routes Reference
- DevOps & Integrator Notes
Overview
Orangecat_Returns adds a full return-request (RMA) workflow to Magento 2. Customers submit return requests from their account area; admins review, update status, assign a resolution, and add internal notes. Every status change triggers an automatic email to the customer.
The module handles:
- Per-product returnability toggle via the
is_returnableproduct attribute - Configurable return window (days) measured from order creation or latest shipment date
- Optional partial returns — customers can select individual items and quantities
- Four configurable lookup catalogs: Statuses, Reasons, Conditions, Resolutions — all with per-store label translations
- Increment ID generation in
RMA000000001format - Automated email notifications to admins on new requests and to customers on status changes
- Full Hyvä and Luma theme support; Breeze Evolution CSS included
Position in the Orangecat B2B Dependency Chain
Orangecat_Returns has no dependency on other Orangecat modules. It stands alone and can be deployed alongside any combination of the Orangecat B2B suite.
Orangecat_Core (via composer: orangecat/core)
Orangecat_Returns ← this module (independent)
Return Eligibility Rules
A return request can only be created when all of the following are true:
- The module is enabled in configuration.
- The order is in
completeorclosedstate. - The return window has not expired (configurable;
0= no limit). - At least one item in the order has
is_returnable = Yeson its product. - Available quantity for each selected item is greater than zero (all previous requests count, even rejected ones).
Theme Compatibility
| Theme | Status | Notes |
|---|---|---|
| Luma | Supported | Standard .phtml templates. Less styles via view/frontend/web/css/source/_module.less. |
| Hyvä | Supported | Dedicated *_hyva.phtml templates with Tailwind CSS. Loaded via hyva_* layout handles. Module CSS injected via hyva_default.xml. |
| Breeze Evolution | Partial | No dedicated Breeze templates; falls back to Luma .phtml. Breeze-compatible Less styles provided in view/frontend/web/css/breeze/_default.less. |
To add proper Breeze template support: create view/frontend/templates/account/returns_list_breeze.phtml, return_view_breeze.phtml, and the equivalent create templates, then wire them in breeze_returns_* layout handles.
Requirements
| Dependency | Version / Notes |
|---|---|
| PHP | 8.1+ |
| Magento | 2.4.x |
Orangecat_Core |
composer: orangecat/core |
Magento_Catalog |
core |
Magento_Sales |
core |
Magento_Customer |
core |
Magento_Eav |
core — used by AddIsReturnableAttribute data patch |
Magento_Email |
core |
Magento_Ui |
core |
Magento_Backend |
core |
Installation
This module is a git submodule of the B2B SDK. To add it:
git submodule add git@github.com:olivertar/m2_returns.git app/code/Orangecat/Returns git submodule update --init app/code/Orangecat/Returns
Then, inside the PHP container (reward shell):
bin/magento module:enable Orangecat_Returns bin/magento setup:upgrade bin/magento setup:di:compile bin/magento setup:static-content:deploy -f bin/magento cache:flush
What Gets Installed
Database Tables
return_status
The configurable status catalog.
| Column | Type | Notes |
|---|---|---|
status_id |
int unsigned, PK, auto-increment | |
code |
varchar(50), NOT NULL | Unique machine code (e.g. pending). |
label |
varchar(255), NOT NULL | Default display label. |
sort_order |
int | Display order in dropdowns. |
is_active |
smallint | 1 = active, 0 = hidden. |
return_status_store
Per-store label translations for statuses.
| Column | Type | Notes |
|---|---|---|
status_id |
int unsigned, PK+FK | Cascades on delete. |
store_id |
smallint unsigned, PK+FK | Cascades on delete. |
label |
varchar(255), NOT NULL | Translated label for this store. |
return_reason
The configurable reason catalog.
| Column | Type | Notes |
|---|---|---|
reason_id |
int unsigned, PK, auto-increment | |
label |
varchar(255), NOT NULL | |
is_other |
smallint | 1 = renders a free-text textarea on the form. |
sort_order |
int | |
is_active |
smallint |
return_reason_store
Per-store label translations for reasons. Same structure as return_status_store.
return_condition
The configurable item-condition catalog.
| Column | Type | Notes |
|---|---|---|
condition_id |
int unsigned, PK, auto-increment | |
label |
varchar(255), NOT NULL | |
sort_order |
int | |
is_active |
smallint |
return_condition_store
Per-store label translations for conditions. Same structure as return_status_store.
return_resolution
The configurable resolution catalog — the outcome assigned by admin when processing a request (e.g. Refund, Exchange).
| Column | Type | Notes |
|---|---|---|
resolution_id |
int unsigned, PK, auto-increment | |
label |
varchar(255), NOT NULL | |
sort_order |
int | |
is_active |
smallint |
return_resolution_store
Per-store label translations for resolutions. Same structure as return_status_store.
return_request
The return request header — one record per submitted RMA.
| Column | Type | Notes |
|---|---|---|
request_id |
int unsigned, PK, auto-increment | |
increment_id |
varchar(50), NOT NULL | Human-readable ID, e.g. RMA000000001. |
order_id |
int unsigned, NOT NULL, FK | References sales_order.entity_id. Cascades on delete. |
order_increment_id |
varchar(50), NOT NULL | Snapshot of the order increment ID. |
customer_id |
int unsigned, nullable | FK to customer. Nullable for future guest support. |
customer_email |
varchar(255), NOT NULL | Snapshot of the customer email at submission time. |
store_id |
smallint unsigned, NOT NULL | Store the order was placed on. |
status |
varchar(50), NOT NULL, default pending |
Current status code. |
resolution |
varchar(255), nullable | Resolution label assigned by admin. |
admin_notes |
text, nullable | Internal notes — not visible to the customer. |
customer_comments |
text, nullable | Customer's freeform comment submitted with the request. |
created_at |
timestamp | Set on INSERT. |
updated_at |
timestamp | Updated on every save. |
Indexes: customer_id, order_id, status.
return_request_item
One record per line item in a return request.
| Column | Type | Notes |
|---|---|---|
item_id |
int unsigned, PK, auto-increment | |
request_id |
int unsigned, NOT NULL, FK | References return_request.request_id. Cascades on delete. |
order_item_id |
int unsigned, NOT NULL | References the original sales_order_item. |
product_id |
int unsigned, NOT NULL | Product ID at submission time. |
product_name |
varchar(255), NOT NULL | Snapshot of the product name. |
product_sku |
varchar(255), NOT NULL | Snapshot of the product SKU. |
qty_requested |
decimal(12,4), NOT NULL | Quantity the customer wants to return. |
reason |
varchar(255), NOT NULL | Reason label stored as text (snapshot). |
reason_other |
text, nullable | Free-text filled when reason is_other = 1. |
condition_label |
varchar(255), nullable | Item condition label stored as text (snapshot). |
Index: request_id.
EAV Attributes
| Attribute Code | Entity | Type | Input | Scope | Default | Notes |
|---|---|---|---|---|---|---|
is_returnable |
catalog_product |
int | boolean | Global | 0 (No) |
Appears in product edit under the Returns attribute group. Applies to simple, configurable, virtual, bundle, grouped products. |
Important: Products with
is_returnable = No(the default) are silently excluded from all return forms. You must explicitly setis_returnable = Yeson each product you want to allow returns for. For configurable products, the attribute is checked on the child simple product selected in the order.
Data Patches
| Class | What It Installs |
|---|---|
InstallDefaultStatuses |
10 statuses: Pending, Authorized, Partially Authorized, Denied, Return Received, Return Partially Received, Approved, Rejected, Processed and Closed, Closed. |
InstallDefaultReasons |
6 reasons: Wrong Item, Defective Product, Not As Described, Changed My Mind, Arrived Too Late, Other (is_other=1). |
InstallDefaultConditions |
3 conditions: Unopened, Opened, Damaged. |
InstallDefaultResolutions |
3 resolutions: Exchange, Refund, Store Credit. |
AddIsReturnableAttribute |
Adds the is_returnable boolean attribute to catalog_product. |
Configuration
Navigate to Stores > Configuration > Orangecat > Returns.
General Configuration
| Label | Config path | Default | Description |
|---|---|---|---|
| Enable Returns Module | returns/general/enabled |
Yes | Master toggle. Disables all frontend routes and hides the "My Returns" account link when off. |
| Allow Partial Returns | returns/general/allow_partial |
Yes | When disabled, the customer must return the full order; item selection checkboxes are hidden. |
| Return Window (days) | returns/general/return_days_limit |
30 | Number of days after the reference date during which a return may be submitted. Set to 0 for no limit. |
| Calculate Return Period From | returns/general/return_period_start |
Order Creation Date | order = order created_at; shipment = latest shipment created_at. When set to shipment, orders with no shipments cannot be returned. |
| Return Address | returns/general/return_address |
— | Shown to customers on the return form and confirmation pages. Store-level scope. |
| Return Instructions | returns/general/return_instructions |
— | Shown to customers alongside the return address. Store-level scope. |
| Admin Notification Emails | returns/general/notification_emails |
— | Comma-separated list of email addresses notified when a new return request is submitted. |
returns/general/enabled
returns/general/allow_partial
returns/general/return_days_limit
returns/general/return_period_start
returns/general/return_address
returns/general/return_instructions
returns/general/notification_emails
Store Admin Guide
Navigating to Returns
All returns management is under Orangecat B2B > Returns in the admin menu.
Manage Return Requests
Path: Orangecat B2B > Returns > Manage Returns
The grid displays all submitted return requests with columns for Increment ID, Order, Customer Email, Status, Store, and Created date. Use the column filters and search to locate specific requests.
Viewing a request:
- Click a row or use Edit in the Actions column.
- The detail page shows the request header (status, resolution, admin notes) and the Items tab listing every line item with its reason, condition, and requested quantity.
Processing a request:
- Open the return request edit page.
- Change Status to reflect the current stage (e.g.
Authorized,Denied). - Optionally set a Resolution (e.g.
Refund). - Add Admin Notes for internal tracking — these are not emailed to the customer.
- Click Save. If the status changed, the customer receives an automatic status-update email.
Admins can only update
status,resolution, andadmin_notes. Item details are immutable after submission.
Managing Statuses
Path: Orangecat B2B > Returns > Statuses
Create, edit, or deactivate return statuses. Each status has a machine code (must be unique), a display label, a sort_order, and an is_active flag. Per-store label translations are managed on the status edit form.
Managing Reasons
Path: Orangecat B2B > Returns > Reasons
Each reason can optionally have Is Other = Yes, which causes a free-text textarea to appear on the frontend return form when that reason is selected.
Managing Conditions
Path: Orangecat B2B > Returns > Conditions
Item conditions reported by the customer (e.g. Unopened, Damaged). Supports per-store label translations.
Managing Resolutions
Path: Orangecat B2B > Returns > Resolutions
The set of outcomes admin can assign when processing a request (e.g. Exchange, Refund, Store Credit).
Enabling Returns on Products
Path: Catalog > Products > [Edit product] > Returns tab
Set Is Returnable to Yes on each product that should be eligible for returns. The attribute defaults to No — products not explicitly enabled are silently excluded from return forms.
Configuration
Path: Orangecat B2B > Returns > Settings (or Stores > Configuration > Orangecat > Returns)
See the Configuration section for a full field reference.
Buyer Guide (Frontend)
My Returns (Account Area)
Logged-in customers see a My Returns link in their account navigation (after Orders). The page lists all their submitted return requests with Increment ID, linked Order, Status, and submission date.
URL: /returns/account/index
Submitting a New Return
- Navigate to My Returns and click Create New Return, or go directly to
/returns/create/search. - Enter your Order ID or Order Increment ID to locate the order.
- If the order is eligible, you are taken to the return form at
/returns/create/form?order_id=<id>. - For each returnable item in the order:
- Check the item (when partial returns are enabled).
- Enter the quantity to return (must not exceed the available balance).
- Select a Reason from the dropdown; if "Other" is chosen, fill in the free-text field.
- Select the Condition of the item.
- Optionally add a Comment (general notes for the admin).
- Submit the form. On success, you are redirected to the request detail page showing the new
RMAxxxxxxxxxincrement ID.
Orders that are not in complete or closed state, or that are outside the return window, are rejected with an explanatory error message.
Viewing a Return Request
URL: /returns/account/view?id=<request_id>
Shows the request header (status, resolution if assigned, submission date, customer comments) and a table of requested items with their reasons, conditions, and quantities.
Developer Guide
Module Structure
Orangecat/Returns/
├── Api/ # Service contracts
│ ├── Data/ # Data interfaces (ReturnRequest, Item, Status, Reason, Condition, Resolution)
│ └── *RepositoryInterface.php # One repository interface per entity
├── Block/
│ ├── Account/ # ReturnsList, ReturnView, Link (customer account)
│ ├── Adminhtml/Returns/ # Admin edit/tabs/form blocks
│ └── Create/ # Form, Search blocks (frontend create flow)
├── Controller/
│ ├── Account/ # Index (list), View (detail)
│ ├── Adminhtml/ # CRUD controllers for Returns, Status, Reason, Condition, Resolution
│ └── Create/ # Search, Form, Post (submit)
├── Helper/
│ └── Data.php # canReturn(), isProductReturnable(), getRequestedQty()
├── Model/
│ ├── Config.php # Typed config accessors
│ ├── Email/Sender.php # Email dispatch for both templates
│ ├── Config/Source/ # Dropdown sources for system.xml and forms
│ ├── ResourceModel/ # One resource model + collection per entity
│ └── ReturnRequest.php # Main model; also Status, Reason, Condition, Resolution, Item
├── Observer/
│ ├── SendNewRequestNotification.php
│ └── SendStatusUpdateEmail.php
├── Setup/Patch/Data/ # 5 data patches (statuses, reasons, conditions, resolutions, EAV attribute)
├── Ui/Component/Listing/Column/ # Actions columns for all five admin grids
├── view/
│ ├── adminhtml/ # UI component XML, layout XML, templates
│ └── frontend/
│ ├── email/ # Two email templates
│ ├── layout/ # Luma + hyva_ layout handles
│ ├── templates/ # account/* and create/* — both Luma and Hyvä variants
│ └── web/css/ # source/_module.less (Luma), hyva/module.css, breeze/_default.less
└── etc/
├── db_schema.xml
├── di.xml / adminhtml/di.xml
├── events.xml
├── email_templates.xml
├── acl.xml
├── adminhtml/{menu,routes,system}.xml
└── frontend/routes.xml
Key Classes
Service Contracts
| Interface | Implementation | Description |
|---|---|---|
ReturnRequestRepositoryInterface |
ReturnRequestRepository |
save(), getById(), getList(), delete(), deleteById(), getItemsByRequestId(). Dispatches return_request_created and return_request_status_changed events automatically on save. |
StatusRepositoryInterface |
StatusRepository |
CRUD for return statuses. |
ReasonRepositoryInterface |
ReasonRepository |
CRUD for return reasons. |
ConditionRepositoryInterface |
ConditionRepository |
CRUD for item conditions. |
ResolutionRepositoryInterface |
ResolutionRepository |
CRUD for resolutions. |
Notable Models
| Class | Purpose |
|---|---|
Model\Config |
Typed accessors for all returns/general/* config values. Inject instead of calling ScopeConfigInterface directly. |
Helper\Data |
canReturn(OrderInterface) — full eligibility check. isProductReturnable(OrderItemInterface) — resolves to the child simple for configurables. getRequestedQty(OrderItemInterface) — sums all past requests (all statuses). |
Model\Email\Sender |
Wraps TransportBuilder for both notification templates. |
Increment ID Format
ReturnRequestRepository::save() sets the increment ID after the first INSERT using sprintf('RMA%09d', $requestId), producing values like RMA000000001.
Observers
| Class | Event | Action |
|---|---|---|
Observer\SendNewRequestNotification |
return_request_created |
Sends returns_new_request_admin email to all configured admin notification addresses. |
Observer\SendStatusUpdateEmail |
return_request_status_changed |
Sends returns_status_update_customer email to the customer when status changes. |
Both observers swallow exceptions silently to prevent email failures from breaking the save flow.
Plugins
This module registers no plugins.
Email Templates
| Template ID | File | Trigger |
|---|---|---|
returns_new_request_admin |
view/frontend/email/returns_new_request_admin.html |
New return request submitted (return_request_created event). Sent to admin notification addresses. |
returns_status_update_customer |
view/frontend/email/returns_status_update_customer.html |
Return request status updated (return_request_status_changed event). Sent to the customer. |
ACL Resources
| Resource ID | Title | Location |
|---|---|---|
Orangecat_Returns::returns |
Returns | Under Magento_Backend::admin |
Orangecat_Returns::returns_manage |
Manage Return Requests | Under returns |
Orangecat_Returns::status |
Manage Statuses | Under returns |
Orangecat_Returns::reason |
Manage Reasons | Under returns |
Orangecat_Returns::condition |
Manage Conditions | Under returns |
Orangecat_Returns::resolution |
Manage Resolutions | Under returns |
Orangecat_Returns::config |
Returns Configuration | Under Magento_Config::config |
Adding Custom Logic
- Override eligibility: Add a plugin on
Helper\Data::canReturn()or extendModel\Configto inject additional rules (e.g., block returns for specific customer groups or SKUs). - React to state changes: Observe
return_request_createdorreturn_request_status_changedevents — both pass theReturnRequestInterfaceinstance in event data. - Custom lookup values: Add records directly to
return_status,return_reason,return_condition, orreturn_resolutionvia a data patch that depends on the correspondingInstallDefault*patch.
REST API
This module does not expose any REST API endpoints. No webapi.xml is defined.
Frontend Routes Reference
Frontend front name: returns.
| Route | Controller | Access |
|---|---|---|
GET /returns/account/index |
Controller\Account\Index |
Logged-in customers |
GET /returns/account/view |
Controller\Account\View |
Logged-in customers (?id=<request_id>) |
GET /returns/create/search |
Controller\Create\Search |
Logged-in customers |
GET /returns/create/form |
Controller\Create\Form |
Logged-in customers (?order_id=<id>) |
POST /returns/create/post |
Controller\Create\Post |
Logged-in customers |
All customer-facing controllers extend Magento\Customer\Controller\AbstractAccount and redirect unauthenticated visitors to the login page. All controllers redirect to /customer/account when the module is disabled.
DevOps & Integrator Notes
Deployment Checklist
# Run inside reward shell bin/magento module:enable Orangecat_Returns bin/magento setup:upgrade # runs DB schema + data patches bin/magento setup:di:compile bin/magento setup:static-content:deploy -f bin/magento cache:flush
Minimum ACL for Integration Token
An integration used to manage returns programmatically needs:
Orangecat_Returns::returns_manage— read/update return requestsOrangecat_Returns::status— read/manage statusesOrangecat_Returns::reason— read/manage reasonsOrangecat_Returns::condition— read/manage conditionsOrangecat_Returns::resolution— read/manage resolutions
Disabling Without Uninstalling
bin/magento module:disable Orangecat_Returns bin/magento setup:upgrade bin/magento cache:flush
When disabled: all frontend routes return a redirect to /customer/account; the admin menu disappears; existing data in all return_* tables is preserved.
Data Integrity Notes
- Deleting a
sales_orderrecord cascades toreturn_requestand then toreturn_request_item(FK withON DELETE CASCADE). - Deleting a status, reason, condition, or resolution from the catalog tables does not cascade to existing request records. Item reasons and conditions are stored as text snapshots at submission time; status codes are stored as raw strings. Removing a catalog entry will not break existing requests but will remove it from dropdowns for new ones.
return_status.codehas a unique constraint. Duplicate codes from custom patches will throw a DB-level exception duringsetup:upgrade.- The
is_returnableproduct attribute defaults to0(No). Products without an explicitYesvalue are excluded from all return forms — verify this is set before go-live.