Overview
Add a first-party returns management module so customers and guests can request returns from an order, exchange messages with the merchant, and have the resolution feed into the existing credit memo / order pipeline. No SaaS dependency, no external services, sits on the same building blocks as invoices, shipments, and credit memos.
Initial release: 1.0.0.
Design principles
- Flat tables, no EAV. RMA is a sales document and follows the same convention as
sales_flat_*.
- Statuses, reasons, resolutions, item conditions are enum-style code constants. Reasons / resolutions / item conditions are also editable via a multi-line
system.xml field for merchants who want to tweak them. No admin CRUD grids for taxonomy.
- Per-product return rules through two new product attributes, not a separate rule engine.
- Customer-facing URLs live under
/sales/order/rma/* and guest URLs under /sales/guest/rma/*, matching the existing auth split for order views.
- Resolution hooks (refund, replacement) prefill a draft credit memo or draft order. Admin still posts manually. Human in the loop.
Module layout
app/code/core/Maho/Rma/ with Block/, Helper/, Model/, controllers/, etc/, data/, sql/.
Declared in app/etc/modules/Maho_Rma.xml. Locale CSV at app/locale/en_US/Maho_Rma.csv.
etc/config.xml:
- Module version
1.0.0
- Model / block / helper / resource aliases under
rma
- Default config values
- Email template definitions
etc/system.xml: settings group under Sales:
- Enable RMA
- Increment ID prefix
- Default return window (days)
- Allowed order statuses for return
- Default reasons (multi-line)
- Default resolutions (multi-line)
- Default item conditions (multi-line)
- Attachment allowed extensions, max file size, max files per RMA
etc/adminhtml.xml: ACL under admin/sales/rma.
Schema (install-1.0.0.php)
Four tables, no maho_ prefix:
rma (rma_id, increment_id, order_id, customer_id, customer_email, store_id, status, resolution, customer_comment, created_at, updated_at)
rma_item (rma_item_id, rma_id, order_item_id, qty_requested, qty_approved, qty_received, reason, condition)
rma_status_history (history_id, rma_id, status, comment, is_visible_on_front, is_customer_notified, created_by, created_at)
rma_attachment (attachment_id, history_id, file_name, file_path, file_size, mime_type)
FKs to sales_flat_order, sales_flat_order_item, customer_entity, core_store with the same on-delete semantics those tables use elsewhere.
Product attributes (data-install-1.0.0.php)
is_returnable (yes/no, default yes), added to the default attribute set
return_period_days (int, nullable, blank inherits global default)
State machine
8 states, defined as Maho_Rma_Model_Rma constants:
`pending`, `awaiting_info`, `approved`, `received`, `closed_refunded`, `closed_replaced`, `closed_rejected`, `closed_cancelled`
Resolution constants on Maho_Rma_Model_Resolution: `refund`, `replace`, `store_credit`, `repair`.
Transitions are validated in _beforeSave against a transition map. Invalid transitions throw Mage_Core_Exception.
Eligibility
Maho_Rma_Model_Eligibility returns the set of order items that can still be returned:
- Order in an allowed status (config)
- Within the return window (per-product
return_period_days else global default)
- Product
is_returnable = yes
- Qty not already covered by open or closed RMAs
The same helper powers the customer-facing form, the guest form, and the "Request return" button injection on order view pages.
Admin
- Grid at
/admin/rma/index with filters on status, resolution, store, customer email, order increment, date
- Edit at
/admin/rma/edit/{id}, tabs: Info, Items, History/Comments, Attachments
- Comment form with
visible to customer and notify customer checkboxes
- Mass actions: change status, send notification, cancel
- ACL:
Maho_Rma::rma, Maho_Rma::manage, Maho_Rma::comment, Maho_Rma::resolve
_setForcedFormKeyActions(['save', 'delete', 'massStatus', 'massCancel'])
Customer frontend (routes via #[Maho\Config\Route])
GET /sales/order/rma/history
GET /sales/order/rma/create/{orderId} (form)
POST /sales/order/rma/create/{orderId} (submit)
GET /sales/order/rma/view/{id}
POST /sales/order/rma/comment/{id}
Plus template injection:
Mage_Sales order view: "Request return" button when eligibility returns non-empty items
- Customer account left nav: "My returns" link
- Comment submission uses
mahoFetch(), no polling
Guest frontend
GET /sales/guest/rma/create/{orderHash}
POST /sales/guest/rma/create/{orderHash}
GET /sales/guest/rma/view/{id}
POST /sales/guest/rma/comment/{id}
Auth via the same hash/email pair the existing guest order lookup uses.
Template injection on sales/guest/view for the same "Request return" button.
Email notifications
Three templates, dispatched via Mage_Core_Model_Email_Template:
rma_new_admin: admin gets the new RMA
rma_status_update_customer: status changed
rma_comment_customer: admin posted a customer-visible comment
Triggered from observers on maho_rma_save_after and maho_rma_status_history_save_after.
Resolution hooks
Observer on maho_rma_status_change:
- New status
received + resolution refund: build a draft Mage_Sales_Model_Order_Creditmemo with approved qty / condition prefilled, store its id on the RMA. Admin posts manually.
- New status
received + resolution replace: prefill a draft order (admin reviews and submits).
store_credit and repair: flag-only in 1.0.0; wired later when store credit lands.
PDF return slip
Maho_Rma_Block_Pdf_Slip extending Mage_Core_Block_Pdf via DomPdf. Available from the RMA view page once status is approved. Includes RMA increment id, items, qty, reason, return address (from store config), barcode of the increment id.
Events exposed
maho_rma_save_before, maho_rma_save_after
maho_rma_status_change (carries old + new status)
maho_rma_status_history_save_after
maho_rma_attachment_save_after
Tests (Pest)
MahoBackendTestCase:
- State transition validation
- Eligibility calculation (window, is_returnable, qty already covered)
- Credit memo prefill on resolution=refund + status=received
- ACL on admin actions
- Mass actions
MahoFrontendTestCase:
- Customer create flow
- Guest create flow (hash auth)
- Comment visibility filter (
is_visible_on_front)
- Attachment size and extension validation
- "Request return" button visibility against eligibility
Documentation
Add an RMA section to AGENTS.md in the same shape as the Routing and Date sections, covering:
- Eligibility helper
- State constants
- Resolution constants
- Event names
- Per-product attributes
Suggested PR ordering
- Skeleton + schema + state machine + eligibility helper. No user-visible surface yet but the model is solid.
- Admin grid, edit, ACL, mass actions. Admin can create RMAs on customers' behalf.
- Customer frontend.
- Guest frontend.
- Email notifications.
- Resolution hooks (credit memo / replacement order prefill).
- PDF return slip.
- Documentation.
Tests land alongside the relevant PR, not at the end.
Overview
Add a first-party returns management module so customers and guests can request returns from an order, exchange messages with the merchant, and have the resolution feed into the existing credit memo / order pipeline. No SaaS dependency, no external services, sits on the same building blocks as invoices, shipments, and credit memos.
Initial release: 1.0.0.
Design principles
sales_flat_*.system.xmlfield for merchants who want to tweak them. No admin CRUD grids for taxonomy./sales/order/rma/*and guest URLs under/sales/guest/rma/*, matching the existing auth split for order views.Module layout
app/code/core/Maho/Rma/withBlock/,Helper/,Model/,controllers/,etc/,data/,sql/.Declared in
app/etc/modules/Maho_Rma.xml. Locale CSV atapp/locale/en_US/Maho_Rma.csv.etc/config.xml:1.0.0rmaetc/system.xml: settings group under Sales:etc/adminhtml.xml: ACL underadmin/sales/rma.Schema (install-1.0.0.php)
Four tables, no
maho_prefix:rma(rma_id, increment_id, order_id, customer_id, customer_email, store_id, status, resolution, customer_comment, created_at, updated_at)rma_item(rma_item_id, rma_id, order_item_id, qty_requested, qty_approved, qty_received, reason, condition)rma_status_history(history_id, rma_id, status, comment, is_visible_on_front, is_customer_notified, created_by, created_at)rma_attachment(attachment_id, history_id, file_name, file_path, file_size, mime_type)FKs to
sales_flat_order,sales_flat_order_item,customer_entity,core_storewith the same on-delete semantics those tables use elsewhere.Product attributes (data-install-1.0.0.php)
is_returnable(yes/no, default yes), added to the default attribute setreturn_period_days(int, nullable, blank inherits global default)State machine
8 states, defined as
Maho_Rma_Model_Rmaconstants:`pending`, `awaiting_info`, `approved`, `received`, `closed_refunded`, `closed_replaced`, `closed_rejected`, `closed_cancelled`
Resolution constants on
Maho_Rma_Model_Resolution: `refund`, `replace`, `store_credit`, `repair`.Transitions are validated in
_beforeSaveagainst a transition map. Invalid transitions throwMage_Core_Exception.Eligibility
Maho_Rma_Model_Eligibilityreturns the set of order items that can still be returned:return_period_dayselse global default)is_returnable = yesThe same helper powers the customer-facing form, the guest form, and the "Request return" button injection on order view pages.
Admin
/admin/rma/indexwith filters on status, resolution, store, customer email, order increment, date/admin/rma/edit/{id}, tabs: Info, Items, History/Comments, Attachmentsvisible to customerandnotify customercheckboxesMaho_Rma::rma,Maho_Rma::manage,Maho_Rma::comment,Maho_Rma::resolve_setForcedFormKeyActions(['save', 'delete', 'massStatus', 'massCancel'])Customer frontend (routes via
#[Maho\Config\Route])GET /sales/order/rma/historyGET /sales/order/rma/create/{orderId}(form)POST /sales/order/rma/create/{orderId}(submit)GET /sales/order/rma/view/{id}POST /sales/order/rma/comment/{id}Plus template injection:
Mage_Salesorder view: "Request return" button when eligibility returns non-empty itemsmahoFetch(), no pollingGuest frontend
GET /sales/guest/rma/create/{orderHash}POST /sales/guest/rma/create/{orderHash}GET /sales/guest/rma/view/{id}POST /sales/guest/rma/comment/{id}Auth via the same hash/email pair the existing guest order lookup uses.
Template injection on
sales/guest/viewfor the same "Request return" button.Email notifications
Three templates, dispatched via
Mage_Core_Model_Email_Template:rma_new_admin: admin gets the new RMArma_status_update_customer: status changedrma_comment_customer: admin posted a customer-visible commentTriggered from observers on
maho_rma_save_afterandmaho_rma_status_history_save_after.Resolution hooks
Observer on
maho_rma_status_change:received+ resolutionrefund: build a draftMage_Sales_Model_Order_Creditmemowith approved qty / condition prefilled, store its id on the RMA. Admin posts manually.received+ resolutionreplace: prefill a draft order (admin reviews and submits).store_creditandrepair: flag-only in 1.0.0; wired later when store credit lands.PDF return slip
Maho_Rma_Block_Pdf_SlipextendingMage_Core_Block_Pdfvia DomPdf. Available from the RMA view page once status isapproved. Includes RMA increment id, items, qty, reason, return address (from store config), barcode of the increment id.Events exposed
maho_rma_save_before,maho_rma_save_aftermaho_rma_status_change(carries old + new status)maho_rma_status_history_save_aftermaho_rma_attachment_save_afterTests (Pest)
MahoBackendTestCase:MahoFrontendTestCase:is_visible_on_front)Documentation
Add an RMA section to
AGENTS.mdin the same shape as the Routing and Date sections, covering:Suggested PR ordering
Tests land alongside the relevant PR, not at the end.