Retourenformular, GraphQL-Anbindung und Status-Timeline für Adobe Commerce, plus Fallback für Open Source
Adobe Commerce bringt mit Magento_Rma ein vollständiges Retouren-Workflow-System inklusive GraphQL-API mit, doch das Modul fehlt in Magento Open Source komplett. Dieser Artikel zeigt, wie sich ein Retouren-Antrag-Formular als Alpine-Komponente in ein Hyvä-Theme integrieren lässt, wie die requestReturn-Mutation und die customer.returns-Query angebunden werden, wie eine Status-Timeline für Kunden aussieht und wie ein Open-Source-Shop ohne natives RMA einen eigenen einfachen Rückgabeprozess bauen kann.
Inhaltsverzeichnis
- 1. RMA in Magento: Adobe-Commerce-Exklusivität und was Hyvä nicht mitbringt
- 2. Menüpunkt im Kundenkonto und Controller-Grundgerüst
- 3. Retouren-Antrag-Formular als Alpine-Komponente
- 4. GraphQL-Anbindung: requestReturn Mutation und Validierung
- 5. Status-Verfolgung: Return-Timeline für den Kunden
- 6. Datei-Upload für Rücksendebeleg und Fotos
- 7. Multi-Item-Retoure mit unterschiedlichen Gründen pro Position
- 8. Fallback ohne Adobe Commerce: eigenes Retouren-Modul gegen Custom-Entity
- 9. Zusammenspiel mit Store Credit und Erstattung nach Genehmigung
- 10. Zusammenfassung
- 11. FAQ
1. RMA in Magento: Adobe-Commerce-Exklusivität und was Hyvä nicht mitbringt
Bevor an einer einzigen Zeile Frontend-Code gearbeitet wird, muss die Lizenzfrage geklärt sein: Das Modul Magento_Rma und das dazugehörige Magento_RmaGraphQl sind ausschließlich Bestandteil von Adobe Commerce und Adobe Commerce Cloud. In Magento Open Source ist die komplette Return-Merchandise-Authorization-Funktionalität, also Retourenanträge, Statusverwaltung und die zugehörigen GraphQL-Typen, schlicht nicht vorhanden. Wer bin/magento module:status Magento_Rma auf einer Open-Source-Installation ausführt, bekommt keinen Treffer, weil das Modul im Composer-Paket gar nicht existiert.
Hyvä selbst ist ein Theme, keine Commerce-Edition, und bringt dementsprechend keine eigene RMA-Logik mit. Das Parent-Theme hyva-themes/magento2-default-theme-csp übernimmt zwar viele Luma-Templates funktional gleichwertig, aber eben nur für Module, die tatsächlich installiert sind. Ist Magento_Rma vorhanden, weil Adobe Commerce läuft, fehlen im Standard-Hyvä-Theme trotzdem die passenden Templates, denn Hyvä hat sich historisch auf Open-Source-Frontends konzentriert und die Adobe-Commerce-only-Module bewusst ausgeklammert.
Für dieses Kapitel bedeutet das zwei parallele Pfade: Läuft Adobe Commerce, wird die native GraphQL Returns API angebunden, inklusive requestReturn-Mutation und customer-Query mit dem Feld returns. Läuft Magento Open Source, bleibt nur der Eigenbau gegen eine Custom-Entity, den ein späteres Kapitel dieses Artikels skizziert. Beide Wege teilen sich das gleiche UI-Muster: ein Alpine-Formular für den Antrag und eine Timeline-Komponente für den Status.
2. Menüpunkt im Kundenkonto und Controller-Grundgerüst
Der Einstiegspunkt für Kunden ist ein neuer Menüpunkt im Kundenkonto-Dashboard, der sich über das Layout-Handle customer_account per account.nav-Block ergänzen lässt, ganz ohne Kern-Templates zu überschreiben. In einer eigenen view/frontend/layout/customer_account.xml wird ein Block vom Typ Magento\Framework\View\Element\Html\Link\Current referenziert, der auf die neue Route rma/returns/index zeigt.
Der Controller selbst bleibt bewusst schlank: Er prüft die Kunden-Session, lädt keine RMA-Daten server-seitig, sondern liefert nur das Grundgerüst der Seite aus. Die eigentliche Kommunikation mit Adobe Commerce läuft komplett client-seitig über GraphQL, wie es dem Hyvä-Grundprinzip entspricht, Server-Rendering auf das Nötigste zu reduzieren und Interaktivität mit Alpine.js im Browser zu erledigen.
Diese Trennung zahlt sich auch beim Full Page Cache aus, denn die Seite selbst bleibt statisch cachefähig, während sämtliche kundenspezifischen Retourendaten erst nach dem Laden über GraphQL nachgezogen werden, genau wie es bei der Bestellhistorie im Kundenkonto bereits üblich ist.
<?php
declare(strict_types=1);
namespace Mironsoft\Rma\Controller\Returns;
use Magento\Customer\Model\Session as CustomerSession;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\View\Result\Page;
use Magento\Framework\View\Result\PageFactory;
/**
* Rendert das Grundgerüst der Retouren-Übersichtsseite im Kundenkonto.
*/
class Index implements HttpGetActionInterface
{
/**
* @param Context $context
* @param PageFactory $resultPageFactory
* @param CustomerSession $customerSession
*/
public function __construct(
private readonly Context $context,
private readonly PageFactory $resultPageFactory,
private readonly CustomerSession $customerSession
) {
}
/**
* Liefert die Retouren-Seite aus, GraphQL-Daten lädt der Alpine-Client.
*
* @return Page
*/
public function execute(): Page
{
$resultPage = $this->resultPageFactory->create();
$resultPage->getConfig()->getTitle()->set(__('Meine Retouren'));
return $resultPage;
}
}
3. Retouren-Antrag-Formular als Alpine-Komponente
Das Herzstück der Retouren-UI ist ein Formular, das aus der Bestellhistorie des Kunden vorbefüllt wird: Positionen der ausgewählten Bestellung, jeweils mit Menge, Grund und gewünschter Resolution. Technisch bietet sich eine eigene Alpine-Komponente an, die per x-data auf ein Datenobjekt mit den Bestellpositionen zugreift und pro Zeile ein eigenes Reactive-Objekt für Menge, Grund und Resolution vorhält, damit unterschiedliche Positionen unabhängig voneinander befüllt werden können.
Die Positionsliste selbst kommt aus einer serverseitig injizierten JSON-Struktur, die der Block aus der Order-Repository-Abfrage aufbereitet, oder alternativ direkt aus einer GraphQL-Query auf customerOrders. Wichtig ist, dass die Menge pro Position gegen die tatsächlich gelieferte Menge validiert wird, sowohl client-seitig für die Sofort-Rückmeldung als auch serverseitig, bevor die Mutation abgeschickt wird, denn ein manipulierter Payload darf niemals unkontrolliert an die GraphQL-API durchgereicht werden.
Resolution und Grund werden als Select-Felder mit festen Enum-Werten abgebildet, die exakt dem GraphQL-Enum ReturnItemRequestResolution und den konfigurierbaren Return-Reasons entsprechen, damit die Mutation im nächsten Schritt ohne zusätzliche Mapping-Schicht auskommt.
<div x-data="returnRequestForm({
orderId: <?= (int) $block->getOrderId() ?>,
items: <?= /* @noEscape */ $block->getOrderItemsJson() ?>
})" class="mx-auto max-w-3xl px-4 py-8">
<template x-for="(item, index) in items" :key="item.orderItemId">
<div class="mb-4 rounded-lg border border-gray-200 p-4">
<p class="font-semibold" x-text="item.name"></p>
<div class="mt-2 grid grid-cols-1 gap-3 sm:grid-cols-3">
<label class="text-sm">
Menge
<input type="number" min="0" :max="item.qtyAvailable"
x-model.number="item.qtyToReturn"
class="mt-1 w-full rounded border-gray-300">
</label>
<label class="text-sm">
Grund
<select x-model="item.reason" class="mt-1 w-full rounded border-gray-300">
<template x-for="reason in reasons" :key="reason">
<option :value="reason" x-text="reason"></option>
</template>
</select>
</label>
<label class="text-sm">
Wunsch
<select x-model="item.resolution" class="mt-1 w-full rounded border-gray-300">
<option value="REFUND">Erstattung</option>
<option value="EXCHANGE">Umtausch</option>
<option value="STORE_CREDIT">Guthaben</option>
</select>
</label>
</div>
</div>
</template>
<button @click="submitReturn()" class="rounded bg-gray-900 px-4 py-2 text-white">
Retoure einreichen
</button>
</div>
4. GraphQL-Anbindung: requestReturn Mutation und Validierung
Die tatsächliche Übermittlung des Antrags läuft über die requestReturn-Mutation der GraphQL Returns API, die Adobe Commerce über Magento_RmaGraphQl bereitstellt. Aus der Alpine-Komponente heraus wird dazu ein Fetch-Aufruf gegen den GraphQL-Endpunkt abgesetzt, mit dem Customer-Token im Authorization-Header, denn Retourenanträge sind grundsätzlich an eingeloggte Kunden gebunden und nicht als Gast-Aktion vorgesehen.
Die Mutation erwartet ein RequestReturnInput-Objekt mit der Order-ID, einem Array aus items, jeweils mit order_item_id, quantity_to_return, request_quantity, reason und resolution, sowie optionalen Kontaktinformationen. Serverseitige Validierung übernimmt Adobe Commerce selbst, etwa die Prüfung, ob eine Position innerhalb der konfigurierten Retouren-Frist liegt, dennoch sollte die Alpine-Komponente offensichtliche Fehler wie eine Menge von null bereits vor dem Absenden abfangen, um unnötige Round-Trips zu vermeiden.
Nach erfolgreicher Mutation liefert die Antwort ein MagentoReturn-Objekt inklusive der neu vergebenen uid zurück, die anschließend als Referenz für die Status-Timeline aus dem nächsten Kapitel dient.
mutation RequestReturn($input: RequestReturnInput!) {
requestReturn(input: $input) {
return {
uid
number
status
items {
uid
quantity
request_quantity
reason
status
}
}
errors {
type
message
}
}
}
5. Status-Verfolgung: Return-Timeline für den Kunden
Sobald ein Retourenantrag existiert, will der Kunde jederzeit sehen, in welchem Stadium sich der Prozess befindet. Adobe Commerce bildet das über ein festes Status-Enum ab, dessen wichtigste Werte authorized, denied, received, approved und closed sind. Diese Werte eignen sich hervorragend für eine horizontale oder vertikale Timeline-Komponente, weil sie eine klare, lineare Reihenfolge suggerieren, auch wenn denied als Abbruchpfad eine Ausnahme bildet.
Die Timeline wird per customer.returns-Query abgefragt und clientseitig in eine Alpine-Komponente gerendert, die pro Status-Schritt einen Punkt mit Label und, sofern vorhanden, Zeitstempel darstellt. Erreichte Schritte werden farblich hervorgehoben, der aktuelle Schritt zusätzlich mit einer Pulsanimation über Tailwind-Utilities, offene Schritte bleiben grau. Der Sonderfall denied sollte visuell klar vom Erfolgspfad abgesetzt werden, etwa mit einer roten statt grünen Markierung, damit Kunden nicht fälschlich einen erfolgreichen Abschluss annehmen.
Für Support-Anfragen lohnt sich zusätzlich die Anzeige der return.number, also der für Menschen lesbaren Retourennummer, direkt über der Timeline, weil Kunden diese Nummer typischerweise auch in Bestätigungs-E-Mails wiederfinden und im Kontakt mit dem Kundenservice referenzieren.
<div x-data="returnStatusTimeline({ steps: <?= /* @noEscape */ $block->getStatusStepsJson() ?> })">
<ol class="flex flex-col gap-4 sm:flex-row sm:items-center">
<template x-for="(step, index) in steps" :key="step.status">
<li class="flex items-center gap-2">
<span class="h-3 w-3 rounded-full"
:class="{
'bg-green-600': step.reached && step.status !== 'denied',
'bg-red-600': step.reached && step.status === 'denied',
'bg-gray-300': !step.reached
}"></span>
<span class="text-sm" x-text="step.label"></span>
<span class="text-xs text-gray-400" x-text="step.timestamp"></span>
</li>
</template>
</ol>
</div>
6. Datei-Upload für Rücksendebeleg und Fotos
Viele Händler verlangen bei bestimmten Retourengründen, etwa Transportschäden, ein Foto oder einen Rücksendebeleg als Nachweis. Die GraphQL Returns API selbst bringt dafür keinen direkten Datei-Upload-Mutationstyp mit, deshalb läuft der Upload praktisch über einen eigenen, klassischen Controller-Endpunkt außerhalb von GraphQL, der die Datei entgegennimmt und mit der zuvor erzeugten Return-UID verknüpft.
Aus CSP-Sicht ist entscheidend, dass der Upload nicht über ein fetch auf eine externe Domain läuft, sondern konsequent gegen die eigene First-Party-Route, damit keine zusätzliche connect-src-Freigabe im Hyvä-CSP-Modul nötig wird. Das Alpine-Formular nutzt FormData mit dem Magento-Formkey aus dem versteckten Feld, denn auch File-Upload-Controller müssen den Standard-CSRF-Schutz von Magento respektieren, sonst schlägt die Anfrage mit einem 403 fehl.
Serverseitig lohnt sich eine strikte Whitelist erlaubter MIME-Types wie image/jpeg, image/png und application/pdf sowie eine Größenbegrenzung, bevor die Datei im pub/media-Verzeichnis unterhalb eines retourenspezifischen Unterordners abgelegt wird, um wahllose Uploads und daraus resultierende Storage- oder Sicherheitsprobleme zu vermeiden.
async function uploadReturnEvidence(file, returnUid) {
const formData = new FormData();
formData.append('evidence', file);
formData.append('return_uid', returnUid);
formData.append('form_key', window.hyva ? window.hyva.getFormKey() : formKeyValue);
const response = await fetch('/rma/returns/upload', {
method: 'POST',
body: formData,
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error('Upload fehlgeschlagen');
}
return response.json();
}
7. Multi-Item-Retoure mit unterschiedlichen Gründen pro Position
Realistische Retouren betreffen selten nur eine einzelne Position: Ein Kunde bestellt drei Artikel, möchte einen wegen falscher Größe umtauschen, einen wegen Defekt erstattet bekommen und den dritten behalten. Die Alpine-Komponente muss deshalb pro Zeile ein unabhängiges Reactive-Objekt führen, nicht ein einziges globales Formularobjekt für die gesamte Bestellung, sonst überschreiben sich Grund und Resolution der Positionen gegenseitig.
Datenstrukturell bewährt sich ein Array von Objekten mit den Feldern orderItemId, qtyToReturn, reason, resolution und einem berechneten included-Flag, das nur dann true wird, wenn qtyToReturn größer null ist. Beim Absenden filtert die Komponente auf genau diese eingeschlossenen Positionen und baut daraus das items-Array für die requestReturn-Mutation, Positionen mit einer Menge von null werden konsequent weggelassen statt mit Nullwerten mitgeschickt.
UX-seitig hilft eine Sammelaktion wie Alle auswählen mit anschließender individueller Anpassung, weil Kunden bei größeren Bestellungen selten jede Position einzeln anklicken wollen, aber trotzdem am Ende pro Position einen eigenen Grund benötigen, damit die Auswertung im Backend später sauber nach Retourengrund segmentiert werden kann.
8. Fallback ohne Adobe Commerce: eigenes Retouren-Modul gegen Custom-Entity
Wer auf Magento Open Source setzt, kann trotzdem ein brauchbares Retouren-Erlebnis anbieten, nur eben ohne die native GraphQL Returns API. Der pragmatische Weg ist ein eigenes kleines Modul mit einer Custom-Entity, modelliert über db_schema.xml, mit Feldern für Order-ID, Kunden-ID, Positionen als serialisiertes JSON oder eigener Kind-Tabelle, Status, Grund und Resolution, im Prinzip eine schlanke Kopie der wichtigsten RMA-Konzepte.
Das Alpine-Formular aus einem früheren Kapitel bleibt weitgehend unverändert wiederverwendbar, nur die Anbindung ändert sich: Statt einer GraphQL-Mutation ruft die Komponente einen eigenen REST-Endpunkt oder eine eigene GraphQL-Mutation im Custom-Modul auf, die den Antrag validiert und als neuen Datensatz in der eigenen Tabelle speichert. Der Status folgt einem selbst definierten, aber bewusst ähnlichen Enum, etwa requested, approved, rejected, received und completed, damit sich die Timeline-Komponente fast unverändert weiterverwenden lässt.
Die eigentliche Rückerstattung muss dann allerdings manuell im Admin ausgelöst werden, etwa über einen Credit Memo, den ein Support-Mitarbeiter nach Prüfung der Ware anlegt, denn ohne Adobe Commerce fehlt die automatisierte Verknüpfung zwischen Return-Status und Erstattungsprozess, die im kommerziellen Modul mitgeliefert wird.
<?xml version="1.0"?>
<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_return" resource="default" engine="innodb"
comment="Eigene Retouren-Tabelle für Open-Source-Fallback">
<column xsi:type="int" name="entity_id" unsigned="true" nullable="false" identity="true"/>
<column xsi:type="int" name="order_id" unsigned="true" nullable="false"/>
<column xsi:type="int" name="customer_id" unsigned="true" nullable="false"/>
<column xsi:type="text" name="items_json" nullable="false"/>
<column xsi:type="varchar" name="status" length="32" nullable="false" default="requested"/>
<column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP"/>
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="entity_id"/>
</constraint>
</table>
</schema>
9. Zusammenspiel mit Store Credit und Erstattung nach Genehmigung
Sobald eine Retoure den Status approved erreicht und die Ware beim Händler als received markiert wurde, stellt sich die Frage der tatsächlichen Erstattung. Adobe Commerce verknüpft an dieser Stelle die Resolution aus dem ursprünglichen Antrag, Erstattung, Umtausch oder Guthaben, mit dem passenden Folgeprozess, wahlweise einem klassischen Credit Memo oder einer Gutschrift auf das Store-Credit-Konto des Kunden.
Die Detailtiefe dieses Themas, insbesondere die korrekte Anzeige des Store-Credit-Saldos im Kundenkonto, dessen Verrechnung beim nächsten Checkout und die zugehörige GraphQL-Anbindung über customer.store_credit, gehört inhaltlich in einen eigenen Artikel über Store Credit und Kundenguthaben im Hyvä-Theme und wird hier bewusst nicht vertieft.
Für die Retouren-UI selbst reicht an dieser Stelle der Hinweis in der Status-Timeline, dass nach closed ein Verweis auf die Erstattungsart angezeigt wird, etwa als kleiner Info-Text unterhalb des letzten Timeline-Punkts, der bei Guthaben auf die Kontoseite mit dem aktuellen Saldo verlinkt.
| Status | GraphQL-Enum-Wert | Bedeutung für den Kunden | Nächster Schritt | Timeline-Farbe |
|---|---|---|---|---|
| Beantragt | authorized | Antrag wurde geprüft und zur Rücksendung freigegeben | Ware verpacken und zurücksenden | Grün |
| Abgelehnt | denied | Antrag wurde nicht genehmigt, keine Rücksendung notwendig | Support kontaktieren bei Rückfragen | Rot |
| Eingegangen | received | Ware ist beim Händler eingetroffen und wird geprüft | Auf Prüfergebnis warten | Gelb |
| Genehmigt | approved | Prüfung abgeschlossen, Erstattung oder Umtausch wird vorbereitet | Erstattung oder Versand des Ersatzartikels abwarten | Grün |
| Abgeschlossen | closed | Retoure vollständig bearbeitet, Erstattung oder Umtausch erfolgt | Kein weiterer Schritt notwendig | Grau |
Mironsoft
Hyvä-Theme-Entwicklung und Luma-Migration
Noch auf Luma unterwegs oder ein Hyvä-Theme, das nicht rund läuft?
Wir entwickeln Hyvä-Themes für Magento von Grund auf oder migrieren bestehende Luma-Shops sauber, mit Tailwind CSS, Alpine.js und ohne unnötiges JavaScript-Gepäck.
Luma-zu-Hyvä-Migration
Bestehenden Shop strukturiert und ohne Funktionsverlust auf Hyvä umstellen.
Custom-Theme-Entwicklung
Individuelles Hyvä-Theme nach Design-Vorgaben von Grund auf umsetzen.
Performance-Optimierung
Core Web Vitals und Ladezeiten im Hyvä-Frontend gezielt verbessern.
10. Zusammenfassung
RMA-Retouren-UI in Hyvä: Das Wichtigste auf einen Blick
Adobe Commerce only
Magento_Rma und die GraphQL Returns API existieren ausschließlich in Adobe Commerce, Open Source hat keine native Retouren-Funktion.
Alpine-Antragsformular
Item-Auswahl aus der Bestellhistorie, Menge, Grund und Resolution pro Position in einer eigenen Alpine-Komponente.
Status-Timeline
authorized, denied, received, approved und closed als visuelle Timeline über die customer.returns-Query.
Fallback-Modul
Ohne Adobe Commerce ersetzt eine Custom-Entity mit eigenem Status-Enum die native RMA-Funktionalität.