Magento RMA: Retourenmanagement ohne Enterprise-Feature aufbauen
AI generated
M2
di.xml
Magento 2 · RMA · Retouren · Service Contracts
Magento RMA: Retourenmanagement ohne Enterprise-Lizenz
Custom Entity, Statusmaschine und Repository selbst gebaut

Magento Open Source liefert kein natives RMA-Modul, das gibt es nur in Adobe Commerce. Wer Retouren trotzdem sauber verwalten will, baut sich ein eigenes RMA-Modul mit Declarative Schema, Service Contracts, einer echten Statusmaschine und automatischer Gutschrift-Erstellung, statt Retourenmanagement über Custom Attributes und E-Mails zu improvisieren.

18 Min. Lesezeit db_schema.xml · Repository · State Machine · Plugin Magento 2.4.x · PHP 8.4 · Hyvä

1. Die RMA-Lücke in Magento Open Source

Wer in Magento Open Source (Community Edition) nach einem RMA-Modul sucht, findet nichts. Return Merchandise Authorization, also der komplette Workflow von Rückgabeantrag über Genehmigung bis zur Gutschrift, ist ausschließlich Adobe Commerce vorbehalten. Der Magento_Rma-Modulordner existiert im Enterprise-Kern, aber nicht in Open Source, und ein Downgrade der Lizenz zieht diese Funktion sofort. Für viele Shopbetreiber ist das der Moment, in dem klar wird: Retourenmanagement muss entweder komplett manuell per E-Mail und Excel-Liste laufen, oder es entsteht als eigenes Modul.

Der naive erste Reflex ist oft, ein Custom Attribute an der Order oder am Shipment anzuhängen, das den Retourenstatus als Freitext trägt. Das funktioniert für einen Prototyp, bricht aber sofort, sobald mehrere Retourenpositionen zu einer Bestellung gehören, sobald Teilretouren möglich sein sollen oder sobald ein zweites Team im Support parallel an derselben Retoure arbeitet. Ein sauberes RMA-System braucht eine eigene Entität mit eigenem Lebenszyklus, nicht ein angeflanschtes Attribut an einem fremden Objekt.

Dieser Artikel beschreibt den kompletten Bauplan für ein eigenständiges RMA-Modul: eine Custom Entity über db_schema.xml, Service Contracts nach Magento-Konvention, eine echte Statusmaschine für Retourenanfragen, ein Formular im Kundenkonto, eine Verwaltungsoberfläche und die Verknüpfung zur automatischen Gutschrifterstellung. Alles mit PHP 8.4, Constructor Property Promotion und dem Repository-Pattern, das Magento selbst für seine nativen Entitäten verwendet.

2. Custom Entity via Declarative Schema

Der erste Baustein für ein RMA-Modul ist eine eigene Datenbanktabelle für Retourenanfragen. Statt eines InstallSchema-Scripts nutzt man in Magento 2.4 konsequent db_schema.xml, das deklarative Schema. Der Vorteil gegenüber Install-Scripts: Magento berechnet den notwendigen ALTER- oder CREATE-Befehl selbst aus dem Diff zwischen deklariertem und tatsächlichem Zustand, und Whitelist-Dateien dokumentieren jede Schemaänderung versioniert mit.

Die Tabelle mironsoft_rma_request braucht mindestens eine Referenz auf die Bestellung, eine Referenz auf das Shipment (für die spätere Verknüpfung mit den tatsächlich versendeten Artikeln), ein Statusfeld, ein Feld für den Rückgabegrund und Zeitstempel für jede Statusänderung. Wichtig ist ein Fremdschlüssel auf sales_order_entity, damit referenzielle Integrität auf Datenbankebene sichergestellt ist, statt sie nur in der Anwendungsschicht zu prüfen.

Eine zweite Tabelle mironsoft_rma_request_item bildet die Zeilenebene ab, weil eine Retourenanfrage in der Regel mehrere Produktpositionen mit unterschiedlichen Mengen umfasst. Diese Trennung in Kopf- und Positionsdaten folgt demselben Muster wie sales_order und sales_order_item in Magento selbst, und genau das macht das Modul für andere Entwickler im Team sofort verständlich, weil sie das Pattern schon kennen.


<?xml version="1.0"?>
<!-- File: app/code/Mironsoft/Rma/etc/db_schema.xml -->
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
    <table name="mironsoft_rma_request" resource="default" engine="innodb"
           comment="RMA Return Request">
        <column xsi:type="int" name="entity_id" unsigned="true" nullable="false"
                identity="true" comment="Entity ID"/>
        <column xsi:type="int" name="order_id" unsigned="true" nullable="false"
                comment="Sales Order ID"/>
        <column xsi:type="int" name="shipment_id" unsigned="true" nullable="true"
                comment="Sales Shipment ID"/>
        <column xsi:type="varchar" name="status" nullable="false" length="32"
                default="requested" comment="RMA Status"/>
        <column xsi:type="text" name="reason" nullable="true" comment="Return Reason"/>
        <column xsi:type="int" name="creditmemo_id" unsigned="true" nullable="true"
                comment="Linked Creditmemo ID"/>
        <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false"
                default="CURRENT_TIMESTAMP" comment="Created At"/>
        <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false"
                default="CURRENT_TIMESTAMP" comment="Updated At"/>
        <constraint xsi:type="primary" referenceId="PRIMARY">
            <column name="entity_id"/>
        </constraint>
        <constraint xsi:type="foreign" referenceId="MIRONSOFT_RMA_REQUEST_ORDER_ID_SALES_ORDER_ENTITY_ID"
                    table="mironsoft_rma_request" column="order_id"
                    referenceTable="sales_order" referenceColumn="entity_id"
                    onDelete="CASCADE"/>
        <index referenceId="MIRONSOFT_RMA_REQUEST_STATUS" indexType="btree">
            <column name="status"/>
        </index>
    </table>
</schema>

3. Service Contracts: Api/Data und Repository

Ein RMA-Modul, das direkt mit Collections oder rohen SQL-Queries arbeitet, ist von Anfang an schwer testbar und schlecht erweiterbar. Der Magento-Standardweg sind Service Contracts: ein Api/Data/RmaRequestInterface für die Datenstruktur, ein Api/RmaRequestRepositoryInterface für CRUD-Operationen und Suchkriterien, und konkrete Implementierungen im Model-Namespace. Andere Module, ein zukünftiges GraphQL-Resolver-Modul oder ein REST-Endpunkt können sich dann gegen dieselbe stabile Schnittstelle programmieren, ohne das interne Datenmodell zu kennen.

Die Repository-Implementierung nutzt Constructor Property Promotion aus PHP 8.4, um Boilerplate-Zuweisungen zu vermeiden. Für Lesezugriffe mit Filtern kommt SearchCriteriaInterface zum Einsatz statt eigener Ad-hoc-Methoden wie getByOrderIdAndStatus(), weil das Repository sonst mit jedem neuen Filterkriterium wächst. Ein CollectionProcessorInterface übersetzt die Suchkriterien in die konkrete Resource-Collection-Abfrage.

Wichtig ist außerdem, dass der Zustandswechsel selbst nicht im Repository liegt. Das Repository speichert und lädt Daten, es entscheidet nicht, ob ein Übergang von requested zu approved erlaubt ist. Diese Verantwortung gehört in ein eigenes State-Machine-Objekt, das im nächsten Abschnitt beschrieben wird. Diese Trennung hält jede Klasse klein und einzeln testbar.


<?php

declare(strict_types=1);

namespace Mironsoft\Rma\Model;

use Mironsoft\Rma\Api\Data\RmaRequestInterface;
use Mironsoft\Rma\Api\Data\RmaRequestInterfaceFactory;
use Mironsoft\Rma\Api\RmaRequestRepositoryInterface;
use Mironsoft\Rma\Model\ResourceModel\RmaRequest as RmaRequestResource;
use Mironsoft\Rma\Model\ResourceModel\RmaRequest\CollectionFactory;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterfaceFactory;
use Magento\Framework\Api\SearchResults\CollectionProcessorInterface;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;

/**
 * Repository implementation for RMA return requests.
 * Handles persistence only, state transitions live in RmaStateMachine.
 */
class RmaRequestRepository implements RmaRequestRepositoryInterface
{
    /**
     * @param RmaRequestResource $resource Resource model for direct persistence
     * @param RmaRequestInterfaceFactory $requestFactory Factory for new entity instances
     * @param CollectionFactory $collectionFactory Factory for search result collections
     * @param SearchResultsInterfaceFactory $searchResultsFactory Factory for wrapping search results
     * @param CollectionProcessorInterface $collectionProcessor Translates SearchCriteria into collection filters
     */
    public function __construct(
        private readonly RmaRequestResource $resource,
        private readonly RmaRequestInterfaceFactory $requestFactory,
        private readonly CollectionFactory $collectionFactory,
        private readonly SearchResultsInterfaceFactory $searchResultsFactory,
        private readonly CollectionProcessorInterface $collectionProcessor
    ) {
    }

    /**
     * Persists a return request entity.
     *
     * @param RmaRequestInterface $request Return request to persist
     * @return RmaRequestInterface Saved entity with entity_id populated
     * @throws CouldNotSaveException When persistence fails
     */
    public function save(RmaRequestInterface $request): RmaRequestInterface
    {
        try {
            $this->resource->save($request);
        } catch (\Exception $exception) {
            throw new CouldNotSaveException(__('Could not save the return request.'), $exception);
        }

        return $request;
    }

    /**
     * Loads a return request by its entity id.
     *
     * @param int $entityId Primary key of the return request
     * @return RmaRequestInterface Loaded entity
     * @throws NoSuchEntityException When no entity exists for the given id
     */
    public function getById(int $entityId): RmaRequestInterface
    {
        $request = $this->requestFactory->create();
        $this->resource->load($request, $entityId);

        if (!$request->getEntityId()) {
            throw new NoSuchEntityException(__('Return request with id "%1" does not exist.', $entityId));
        }

        return $request;
    }

    /**
     * Loads a filtered, paginated list of return requests.
     *
     * @param SearchCriteriaInterface $searchCriteria Filter, sort and pagination criteria
     * @return \Magento\Framework\Api\SearchResultsInterface Search result wrapper
     */
    public function getList(SearchCriteriaInterface $searchCriteria): \Magento\Framework\Api\SearchResultsInterface
    {
        $collection = $this->collectionFactory->create();
        $this->collectionProcessor->process($searchCriteria, $collection);

        $searchResults = $this->searchResultsFactory->create();
        $searchResults->setSearchCriteria($searchCriteria);
        $searchResults->setItems($collection->getItems());
        $searchResults->setTotalCount($collection->getSize());

        return $searchResults;
    }
}

4. Die Statusmaschine für Retourenanfragen

Der Kern jedes belastbaren RMA-Systems ist eine explizite Statusmaschine mit genau definierten Zuständen und erlaubten Übergängen: requested, approved, rejected, received und refunded. Eine Retourenanfrage startet immer bei requested, sobald der Kunde sie einreicht. Von dort erlaubt die Statusmaschine nur den Übergang zu approved oder rejected, niemals direkt zu received oder refunded. Diese Beschränkung mag auf den ersten Blick pedantisch wirken, verhindert aber genau die Inkonsistenzen, die bei einem einfachen Freitext-Statusfeld schleichend entstehen.

Technisch wird die Statusmaschine als eigene Klasse implementiert, die eine Transitionstabelle als Konstante hält und vor jedem Statuswechsel prüft, ob der Zielzustand vom aktuellen Zustand aus erreichbar ist. Ein ungültiger Übergang, etwa von rejected zu refunded, wirft eine LocalizedException, statt den Status stillschweigend zu überschreiben. Das ist der entscheidende Unterschied zwischen einer echten Statusmaschine und einem Status-Setter, der jeden beliebigen String akzeptiert.

Von approved aus geht die Anfrage zu received, sobald die Ware im Lager physisch eingetroffen und geprüft wurde. Erst von received aus ist der Übergang zu refunded möglich, und genau dieser Übergang löst im Hintergrund die Gutschrifterstellung aus, die in Abschnitt 7 beschrieben wird. Jeder Statuswechsel wird zusätzlich mit einem Zeitstempel und optional einem Kommentarfeld protokolliert, damit der komplette Verlauf einer Retoure im Nachhinein nachvollziehbar bleibt, etwa bei Rückfragen im Support.

5. Rückgabeantrag im Kundenkonto

Der Kunde muss eine Retoure selbst anstoßen können, ohne den Support anzuschreiben. Dafür entsteht im Kundenkonto ein neuer Bereich, umgesetzt als Hyvä-Section mit Alpine.js für die interaktiven Teile wie Mengenauswahl pro Position und Grundauswahl für den Rückgabegrund. Die Liste der zurückgebbaren Positionen wird serverseitig aus dem Shipment des Kunden ermittelt, nicht aus der kompletten Bestellung, denn nur tatsächlich versendete Artikel können zurückgeschickt werden.

Ein Alternativansatz zum manuellen Formular ist ein Observer auf sales_order_shipment_save_after, der bei bestimmten Produkttypen oder Kategorien automatisch ein Retourenfenster im Kundenkonto freischaltet, sobald ein Versand erfolgt ist. Das ist sinnvoll, wenn ein Rückgaberecht zeitlich an das Versanddatum gekoppelt ist, etwa 14 Tage ab Zustellung, denn dann kann das Formular serverseitig prüfen, ob dieses Fenster noch offen ist, statt sich auf clientseitiges JavaScript zu verlassen.

Das Formular selbst validiert serverseitig zwingend, dass die angeforderte Rückgabemenge pro Position die versendete Menge nicht übersteigt, und dass für dieselbe Bestellposition nicht mehrfach parallel offene Retourenanfragen existieren. Diese Validierung gehört in ein eigenes ValidatorInterface, nicht in den Controller, damit dieselbe Prüfung auch von einem späteren REST- oder GraphQL-Endpunkt wiederverwendet werden kann, ohne Code zu duplizieren.

6. Retouren im Adminbereich verwalten

Für das Support-Team braucht es eine Übersicht aller offenen Retourenanfragen im Adminbereich. Ein UI-Component-Grid auf Basis von mironsoft_rma_request als Datenquelle zeigt Bestellnummer, Kunde, Status und Erstellungsdatum, mit Filtern nach Status und Zeitraum. Jede Zeile verlinkt auf eine Detailansicht mit den einzelnen Retourenpositionen, dem Rückgabegrund und den verfügbaren Aktionsbuttons für den nächsten erlaubten Statuswechsel.

Die Aktionsbuttons im Grid rufen nicht direkt das Repository auf, sondern einen dedizierten Controller, der die Statusmaschine aus Abschnitt 4 nutzt. So ist ausgeschlossen, dass ein Admin-User über die Oberfläche einen Zustand erzwingt, der aus fachlicher Sicht ungültig ist, etwa das direkte Genehmigen einer bereits abgelehnten Retoure. Der Controller prüft zusätzlich die passende ACL-Ressource, damit nur berechtigte Rollen Statuswechsel auslösen können.

Für Shops, die stärker auf Hyvä statt klassisches Luma-Admin-UI setzen, lässt sich dieselbe Übersicht auch als eigener Bereich im Kundenkonto für B2B-Firmenadministratoren spiegeln, mit denselben Repository- und Statusmaschinen-Klassen im Hintergrund. Die Wiederverwendung derselben Service-Contract-Schicht für Admin- und Frontend-Ansicht ist genau der Vorteil, den die Trennung in Interfaces und Implementierungen von Anfang an bringt.

7. Von der Wareneingangsprüfung zur Gutschrift

Sobald das Lagerteam den physischen Wareneingang bestätigt und die Retourenanfrage auf received gesetzt wird, ist der nächste fachliche Schritt die Gutschrifterstellung. Ein Plugin auf die Statusmaschine, genauer ein after-Plugin auf die Methode, die den Übergang zu refunded ausführt, ruft Magento\Sales\Api\CreditmemoManagementInterface auf, um für die betroffenen Bestellpositionen eine Gutschrift zu erzeugen. Ein Plugin ist hier bewusst der Preference vorgezogen, weil die Kernlogik der Statusmaschine unverändert bleibt und die Gutschrift-Erstellung als zusätzliches, austauschbares Verhalten daneben existiert.

Wichtig ist, dass die Gutschriftmenge exakt den genehmigten und tatsächlich eingegangenen Positionen entspricht, nicht der ursprünglich beantragten Menge. Ein Kunde kann fünf Artikel anmelden, aber nur drei tatsächlich zurückschicken, und nur für diese drei darf die Gutschrift entstehen. Die Referenz aus creditmemo_id in der mironsoft_rma_request-Tabelle verknüpft die Retoure dauerhaft mit der erzeugten Gutschrift, sodass sich der komplette Vorgang von der Anfrage bis zur Rückzahlung durchgängig nachvollziehen lässt.

Für Zahlungsarten mit externen Payment-Gateways muss das Plugin zusätzlich prüfen, ob eine Online-Rückerstattung technisch möglich ist, oder ob eine manuelle Offline-Gutschrift nötig ist. Diese Entscheidung sollte nicht hartcodiert sein, sondern über die vom jeweiligen Payment-Modul bereits bereitgestellte canRefund()-Prüfung laufen, damit das RMA-Modul nicht für jede Zahlungsart eine eigene Sonderbehandlung braucht.

8. Benachrichtigungen bei jedem Statuswechsel

Ein Kunde, der eine Retoure einreicht und danach nichts mehr hört, ruft beim Support an, das kostet Zeit und Vertrauen. Für jeden der fünf Statuswechsel wird deshalb eine eigene E-Mail-Vorlage über etc/email_templates.xml registriert und ein passender Observer auf ein modul-eigenes Event, zum Beispiel mironsoft_rma_status_changed, das die Statusmaschine nach jedem erfolgreichen Übergang dispatcht.

Die Templates nutzen Magentos Template-Variablen-System, sodass Bestellnummer, Kundenname, Retourenpositionen und der neue Status dynamisch eingefügt werden, statt Texte im PHP-Code zusammenzubauen. Das hält die Trennung zwischen Versandlogik und Textinhalt sauber und erlaubt dem Marketing-Team, Formulierungen im Admin-Bereich unter Marketing > E-Mail-Vorlagen anzupassen, ohne Deploy.

Der Observer selbst bleibt bewusst dünn: Er lädt die passende Vorlage anhand des neuen Status, sammelt die Template-Variablen aus der Retourenanfrage und übergibt beides an TransportBuilder. Die eigentliche Entscheidung, welche Vorlage zu welchem Status gehört, liegt in einer Konfigurationsklasse, nicht in einem langen switch-Block im Observer, damit ein neuer Status später ergänzt werden kann, ohne den Observer selbst anzufassen.


<?xml version="1.0"?>
<!-- File: app/code/Mironsoft/Rma/etc/email_templates.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Email:etc/email_templates.xsd">
    <template id="rma_status_requested"
              label="RMA: Return Request Received"
              file="rma_status_requested.html"
              type="html"
              module="Mironsoft_Rma"
              area="frontend"/>
    <template id="rma_status_approved"
              label="RMA: Return Request Approved"
              file="rma_status_approved.html"
              type="html"
              module="Mironsoft_Rma"
              area="frontend"/>
    <template id="rma_status_rejected"
              label="RMA: Return Request Rejected"
              file="rma_status_rejected.html"
              type="html"
              module="Mironsoft_Rma"
              area="frontend"/>
    <template id="rma_status_received"
              label="RMA: Goods Received"
              file="rma_status_received.html"
              type="html"
              module="Mironsoft_Rma"
              area="frontend"/>
    <template id="rma_status_refunded"
              label="RMA: Refund Issued"
              file="rma_status_refunded.html"
              type="html"
              module="Mironsoft_Rma"
              area="frontend"/>
</config>

Für die Event-Verkabelung und die Plugin-Registrierung reicht ein kompaktes di.xml, das den Observer an das Event bindet und das Gutschrift-Plugin an die Statusmaschine hängt. Beide Verkabelungen bleiben deklarativ und lassen sich in einem zweiten Modul überschreiben, falls ein anderes Team eigene Benachrichtigungslogik ergänzen will, ohne den Kern des RMA-Moduls zu verändern.


<?xml version="1.0"?>
<!-- File: app/code/Mironsoft/Rma/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Mironsoft\Rma\Api\Data\RmaRequestInterface"
                type="Mironsoft\Rma\Model\RmaRequest"/>
    <preference for="Mironsoft\Rma\Api\RmaRequestRepositoryInterface"
                type="Mironsoft\Rma\Model\RmaRequestRepository"/>

    <type name="Mironsoft\Rma\Model\RmaStateMachine">
        <plugin name="mironsoft_rma_create_creditmemo_on_refund"
                type="Mironsoft\Rma\Plugin\CreateCreditmemoOnRefundPlugin"
                sortOrder="10"/>
    </type>

    <type name="Magento\Framework\Event\Config\Data">
        <arguments>
            <argument name="events" xsi:type="array">
                <item name="mironsoft_rma_status_changed" xsi:type="array">
                    <item name="mironsoft_rma_send_status_email" xsi:type="array">
                        <item name="instance" xsi:type="string">Mironsoft\Rma\Observer\SendStatusChangeEmailObserver</item>
                    </item>
                </item>
            </argument>
        </arguments>
    </type>
</config>

9. RMA-Entscheidungen im Vergleich

Bei jedem eigenen RMA-Modul gibt es Weggabelungen, an denen der schnelle, naive Ansatz und das langfristig wartbare Pattern deutlich auseinanderlaufen. Die folgende Übersicht fasst die wichtigsten Entscheidungen zusammen, die über die gesamte Lebensdauer des Retourenmanagements Bestand haben sollten.

Entscheidung Naiver Ansatz Empfohlenes Pattern Vorteil
Status verwalten Freitext-Attribut an Order/Shipment Eigene Statusmaschine mit Transitionstabelle Ungültige Übergänge werden verhindert, nicht nur geloggt
Datenzugriff Direkte SQL-Queries oder rohe Collections Repository mit Service Contracts Testbar, austauschbar, API-fähig
Gutschrift auslösen Manuell im Admin nach E-Mail vom Support Plugin auf Statusübergang zu refunded Konsistent, ohne manuellen Zwischenschritt
Kundenbenachrichtigung Hartcodierte E-Mail-Texte im Observer email_templates.xml mit Template-Variablen Anpassbar im Admin, ohne Deploy
Schema-Änderungen InstallSchema/UpgradeSchema-Scripts db_schema.xml, deklarativ Versionierter Diff, keine manuellen Migrationen

Der rote Faden hinter allen fünf Zeilen ist derselbe: Retourenmanagement ist kein Feature, das man mit einem Attribut und einem Cronjob nachrüstet, sondern ein eigenständiger Geschäftsprozess mit klaren Zuständen, Verantwortlichkeiten und Nachvollziehbarkeit. Wer diese Struktur von Anfang an einzieht, spart sich spätere Refactorings, wenn Teilretouren, Mehrfachpositionen oder B2B-Sonderfälle dazukommen.

10. Zusammenfassung

Ein eigenes RMA-System für Magento Open Source ist kein Ersatz-Hack für ein fehlendes Enterprise-Feature, sondern kann bei sauberer Umsetzung sogar besser zum eigenen Geschäftsprozess passen als eine generische Adobe-Commerce-Lösung. Declarative Schema für die Custom Entity, Service Contracts für den Datenzugriff, eine explizite Statusmaschine für die fünf Zustände requested, approved, rejected, received und refunded, ein Plugin für die automatische Gutschrifterstellung und Template-basierte E-Mails für jeden Statuswechsel bilden zusammen ein RMA-Modul, das genauso robust ist wie native Magento-Funktionalität.

Der entscheidende Unterschied zu einem improvisierten Retourenmanagement mit Freitext-Attributen liegt in der Nachvollziehbarkeit: Jeder Statuswechsel ist geprüft, protokolliert und löst genau die richtigen Folgeaktionen aus, von der Kundenbenachrichtigung bis zur Gutschrift. Wer diese Struktur beim ersten Aufbau des Moduls konsequent einzieht, muss sie nicht nachträglich refaktorieren, wenn das Retourenvolumen wächst oder neue Anforderungen wie Teilretouren oder B2B-Freigabeprozesse dazukommen.

Magento RMA und Retourenmanagement, das Wichtigste auf einen Blick

Keine native Lösung

Magento Open Source hat kein RMA-Modul, das gibt es nur in Adobe Commerce. Ein eigenes Modul ist der einzige Weg ohne Lizenzupgrade.

Statusmaschine statt Freitext

Fünf definierte Zustände mit geprüften Übergängen verhindern inkonsistente Retourendaten und ungültige Statuswechsel.

Service Contracts & Repository

Api/Data-Interfaces und Repository-Pattern machen das Modul testbar und für REST, GraphQL und Admin-UI gleichzeitig nutzbar.

Automatisierte Gutschrift

Ein Plugin auf den Übergang zu refunded erzeugt die Gutschrift automatisch, exakt für die tatsächlich eingegangenen Positionen.

11. FAQ: Magento RMA und Retourenmanagement

1Gibt es RMA in Magento Open Source?
Nein, RMA gehört ausschließlich zu Adobe Commerce. Für Retourenmanagement in Open Source braucht es ein eigenes Modul.
2Reicht ein Custom Attribute für den Status?
Nein, es prüft keine Übergänge, unterstützt keine Teilretouren und lässt sich schlecht mit einer Gutschrift verknüpfen. Eine eigene Entität mit Statusmaschine ist nötig.
3Welche Status durchläuft eine Retoure?
requested, approved, rejected, received, refunded. Nur bestimmte Übergänge sind erlaubt, etwa nie direkt von requested zu refunded.
4Wie wird die Entität angelegt?
Über db_schema.xml, das deklarative Schema. Magento berechnet den nötigen SQL-Befehl automatisch aus dem Diff.
5Warum Service Contracts statt Collection-Zugriff?
Sie entkoppeln Datenzugriff vom internen Modell, machen das Modul testbar und für REST, GraphQL und Admin-Grid gleichzeitig nutzbar.
6Wie wird die Gutschrift ausgelöst?
Ein Plugin auf den Übergang zu refunded ruft CreditmemoManagementInterface für die tatsächlich eingegangenen Positionen auf.
7Wo stellt der Kunde den Antrag?
Im Kundenkonto, als Hyvä-Section mit Alpine.js. Rückgabefähige Positionen kommen serverseitig aus dem Shipment.
8Wie werden Kunden benachrichtigt?
Jeder Statuswechsel dispatcht ein Event, ein Observer versendet die passende Vorlage aus email_templates.xml mit Template-Variablen.
9Wohin gehört der Statuswechsel?
In eine eigene Statusmaschinen-Klasse mit Transitionstabelle, nicht ins Repository, das nur für Persistenz zuständig ist.
10Lässt sich das im Adminbereich verwalten?
Ja, über ein UI-Component-Grid mit ACL-geprüften Aktionsbuttons, die die Statusmaschine nutzen statt Status direkt zu überschreiben.

Mironsoft

Magento 2 Entwicklung, Service Contracts und Custom-Module

Retourenmanagement, das nicht an Adobe Commerce hängt?

Wir bauen euch ein eigenes RMA-Modul für Magento Open Source, mit Declarative Schema, sauberen Service Contracts, einer echten Statusmaschine und automatischer Gutschrift-Erstellung, statt Retouren über Excel-Listen und E-Mail-Ketten zu verwalten.

RMA-Modul

Custom Entity, Repository und Statusmaschine nach Magento-Konvention

Kundenkonto-Integration

Rückgabeformular als Hyvä-Section mit serverseitiger Validierung

Gutschrift-Automatisierung

Plugin-basierte Gutschrift-Erstellung nach Wareneingangsprüfung