Zum Inhalt springen

This page is available in English. Switch to English

Arbeit, die jede Woche wiederkommt? Muss kein Mensch machen. Mehr zu KI und Automatisierung

Open Source

Rabatte in Magento 2 lesbar machen: eine Zeile pro Warenkorbregel

Ein dicker Metallbarren links, der sich nach rechts in vier flache Leisten unterschiedlicher Breite teilt.

In einem Shop, den ich betreue, stand im Warenkorb eine Notiz, die nie ein Kunde lesen sollte. Der Text stammte aus dem Feld „Beschreibung“ einer Regel, das im Backend als Notizfeld des Teams gedacht ist.

Was Magento speichert und was nicht

Magento fasst alle angewendeten Rabatte in einer Summenzeile zusammen: „Rabatt (…)“. Der Text in der Klammer entsteht aus den Labels der Regeln. Die Kette steht in RulesApplier::getRuleLabel(): zuerst das Store-Label der Regel, dann, sobald ein Gutschein im Spiel ist, die Regelbeschreibung, dann der Gutscheincode.

Das Feld „Beschreibung“ ist im Backend ein Notizfeld. Dort steht, was das Team sich notiert hat: zu welcher Kampagne eine Regel gehört, aus welchem Ticket sie stammt. Genau dieser Text stand im Warenkorb.

Eine Korrektur im Template kann keine Zeile pro Regel erzeugen, weil die Beträge nicht gespeichert sind. In der Bestellung speichert Magento drei Felder:

  • coupon_code, ein einzelner Code
  • discount_description, ein Textfeld
  • applied_rule_ids, eine Liste von IDs

Mehr ist es nicht. sales_order in Magento/Sales/etc/db_schema.xml führt coupon_code und discount_description als varchar(255), applied_rule_ids als varchar(128), und keine Spalte für einen Betrag pro Regel. Geprüft an Magento 2.4.7.

Keine Beträge pro Regel. Sobald die Bestellung existiert, ist die Aufteilung der Rabattsumme auf die Regeln verloren. Jede Lösung, die eine Zeile pro Regel zeigen will, muss die Beträge in dem Moment speichern, in dem die Bestellung entsteht.

Denselben Text habe ich auch in der Versandzeile gesehen, „Versand und Bearbeitung (…)“, bei einer Bestellung, deren einzige Regel Gratisversand war. Eine Gratisversandregel läuft durch denselben Beschreibungs-Code, in Validator::processShippingAmount(). Die Stelle im Core, die den Text in diese Zeile schreibt, habe ich nicht gefunden: die Versandzeile ist also meine Beobachtung aus der Testumgebung, die Rabattzeile der Weg, den der Quelltext zeigt.

Dieselbe Mechanik in der Testumgebung, mit der zusammengefassten Standardzeile in der neuen E-Mail-Vorlage. Die Regelbeschreibung steht im Postfach des Kunden. Testdaten aus einer frischen Mage-OS-Installation.

Die E-Mail selbst ist nicht die von Magento. Dieses Layout kommt aus einem zweiten Modul, das der Schwesterbeitrag beschreibt, und keines der beiden Module braucht das andere.

Das Vorbild war Shopify

Shopify macht das besser, und das war der Anlass für das Modul. Dort erscheint jeder angewendete Rabatt als eigene Zeile in der Bestellübersicht, auch wenn mehrere Rabatte kombiniert werden (Discount Combinations im Changelog). Dass die Zeilen dort getrennt stehen, ist meine eigene Beobachtung an einem Shopify-Checkout und keine dokumentierte Zusage. Ich habe nach einer Seite in Shopifys Dokumentation gesucht, die das festhält, und keine gefunden.

Shopify hat dabei eine eigene Schwäche: die Zeile trägt den Code, den der Kunde eingegeben hat, und ein Shop kann keinen sprechenden Namen hinterlegen (Shopify Community). Was Shopify bei einem automatischen Rabatt zeigt, der gar keinen Code hat, konnte ich aus keiner zitierfähigen Quelle belegen.

Das Ziel war deshalb beides: eine Zeile pro Regel und eine Beschriftung aus einem Feld, das für Kunden geschrieben ist, auch bei einer Regel, für die der Kunde nichts eingegeben hat.

Die Beschriftungsregel

ArtBeschriftungBetragsspalte
couponder Gutscheincode-Betrag
automaticdas Store-Label der Regel, sonst ein übersetztes „Rabatt“-Betrag
free_shippingder Code oder das Store-Label, sonst „Gratisversand“-Versandrabatt oder „Gratis“
giftder Gutscheincode der Aktion„Geschenk“ ohne Betrag, sonst der Betrag
gift_card„Geschenkkarte ••••1234“, maskiert-Betrag

Regelbeschreibung und Regelname kommen nie vor. Beschriftungen und Beträge werden beim Anlegen der Bestellung festgehalten, also ändert ein späteres Umbenennen oder Löschen einer Regel alte Bestellungen nicht.

Dieselbe Bestellung mit einer Zeile pro Regel. Die automatische Regel zeigt ihr Store-Label, der Gutschein zeigt den Code, und jede Zeile nennt ihren eigenen Betrag.
Drei Arten in einem Warenkorb: eine automatische Regel, ein Gutschein und eine Geschenkkarte. Der Kartencode ist bis auf die letzten vier Stellen maskiert, was genügt, damit ein Kunde die Karte wiedererkennt.

Wie die Beträge den Checkout überleben

Speicherung an der Quote-Adresse bei der Summenberechnung. Nachdem die Summen einer Adresse mit collectTotals berechnet sind, werden die Zeilen pro Regel aufgelöst und an der Quote-Adresse gespeichert (quote_address.pp_discount_lines). Die Auflösung übernimmt ein generischer Regel-Resolver, der die regelweise Aufschlüsselung des Kerns ausliest und damit jede simple_action abdeckt, darunter by_percent, by_fixed, cart_fixed und buy_x_get_y. Dazu kommt ein Resolver für Gratisversand. Beide stecken in einem Composite, und dieses Composite ist der Erweiterungspunkt: ein Adapter hängt sich per di.xml als benannter Resolver ein. Eine fehlgeschlagene Summenberechnung löscht die gespeicherten Zeilen.

Extension Attribute am Summensegment. Ein Plugin auf CartTotalRepositoryInterface hängt die Zeilen als extension_attributes.discount_lines an das Segment discount an und ersetzt dessen Titel, der standardmäßig die Regelbeschreibung trägt. Eine Quote mit ausschließlich betragslosen Zeilen, also Geschenk oder Gratisversand, bekommt ein Segment discount mit dem Wert 0, damit die Renderer die Zeilen trotzdem erhalten. Gespeicherte Zeilen, die nicht zum Segmentwert passen, werden gar nicht erst ausgegeben, und der Titel bleibt das schlichte „Rabatt“.

Persistenz beim Anlegen der Bestellung. Bei sales_model_service_quote_submit_success werden die Zeilen in die Tabelle pp_discount_line geschrieben: Zeilen auf Bestellebene plus Beträge pro Regel auf Positionsebene, und genau die machen die Aufteilung einer Gutschrift möglich. Eine rabattierte Bestellung ohne zuordenbare Zeilen bekommt eine allgemeine automatic-Zeile „Rabatt“ über den vollen Betrag, damit eine Bestellung nie auf die Beschriftung von Magento zurückfällt.

Die Schnittstelle für eigene Renderer

Jede Zeile ist eine eigene Summenzeile in den Standard-Totals-Blöcken von Magento, mit dem Code pp_discount_line_<n> anstelle der Standardzeile discount. Die Zeile trägt den Datenschlüssel discount_line:

discount_line = {
  kind:        "coupon" | "automatic" | "gift" | "free_shipping" | "gift_card",
  rule_id:     int|null,      // null bei gift_card
  code:        string|null,   // Gutschein- oder Kartencode, bei gift_card maskiert
  label:       string,        // von der Beschriftungsregel aufgelöst, sicher anzeigbar
  amount:      float,         // Bestellwährung, positive Zahl = Rabatt
  base_amount: float
}

Du erkennst eine Rabattzeile an diesem Datenschlüssel, nicht am Zeilencode. Der Mageplaza-Adapter behält die eigenen Zeilencodes von Mageplaza und trägt discount_line trotzdem, mit kind gleich gift_card. label und value der Zeile bleiben gefüllt, wie Magento es erwartet, also gibt auch eine Vorlage, die den Datenschlüssel ignoriert, eine korrekte schlichte Zeile aus.

Ein Basispaket, kleine Renderer

Das Basispaket kennt kein Frontend. Es verantwortet die Daten, die Speicherung, die Beschriftungsregel und alle Bestelldokumente. Die Storefront-Darstellung liegt in eigenen Paketen, damit du nur installierst, was du betreibst.

PaketWas es ergänzt
magento2-module-discount-linesDaten, Speicherung, Beschriftung, Dokumente, Konfiguration
-hyva-checkoutPreisübersicht in Hyvä Checkout
-hyva-themeWarenkorbseite im Hyvä-Standardtheme
-lumaWarenkorb und Checkout-Sidebar in Luma
-amasty-promoAmasty Free Gift als Geschenkzeilen
-mageplaza-giftcardMageplaza-Geschenkkarten, maskiert

Alle sechs sind am 1. November 2026 als 1.0.0 erschienen, MIT-lizenziert, auf GitHub und Packagist, die Renderer für Hyvä und Luma existieren also heute.

Die beiden Hyvä-Pakete sind keine Alternativen zueinander. -hyva-theme rendert die Warenkorbseite des Hyvä-Standardthemes, -hyva-checkout die Preisübersicht in Hyvä Checkout. Betreibst du beides, brauchst du beide.

Hyvä Checkout liest dieselben Daten über das Summensegment, also stimmen Checkout-Übersicht und Warenkorb überein, ohne dass eines vom anderen weiß.
Luma, dieselbe Testbestellung. Der Renderer ist ein eigenes Paket, ein Luma-Shop installiert also keinen Hyvä-Code und umgekehrt.

Für Loki Checkout gibt es keinen Renderer. Weil das Basispaket kein Frontend kennt, kann jeder weitere Checkout einen bekommen, nur existiert heute keiner.

Warum Amasty und Mageplaza, und wie du einen eigenen Adapter baust

Diese beiden Adapter gibt es, weil diese beiden Erweiterungen in der Installation stecken, mit der ich arbeite, nicht weil ich den Markt gesichtet hätte. Mageplaza ist die Geschenkkarten-Erweiterung, die dort läuft.

Für jeden anderen Anbieter schreibst du den Adapter selbst. Es ist eine Klasse mit einer Methode:

interface DiscountLineResolverInterface
{
    /**
     * Resolve discount lines for a collected quote address.
     *
     * @return DiscountLineInterface[]
     */
    public function resolve(Address $address, Quote $quote): array;
}

Danach übergibst du den Resolver dem Composite unter einem eigenen Namen, in di.xml:

<type name="PixelPerfect\DiscountLines\Service\Resolver\CompositeResolver">
    <arguments>
        <argument name="resolvers" xsi:type="array">
            <item name="your_provider" xsi:type="object">
                Your\Module\Service\Resolver\YourResolver
            </item>
        </argument>
    </arguments>
</type>

Ein Eintrag, der die Schnittstelle nicht implementiert, wird übersprungen und protokolliert, statt eine Exception auszulösen. Ein kaputter Adapter fällt also auf die Standardzeile zurück, statt den Warenkorb lahmzulegen.

Vier Varianten je Store View

Unter Stores > Configuration > Sales > Discount Lines: A setzt den Gutschein-Chip unter die Beschriftung, B macht den Chip zur Beschriftung und ist der Standard, C gruppiert die Zeilen mit einer Summe „Sie sparen“, und D zeigt ein Icon und den Code in Monospace, ohne Chip.

Variante C gruppiert dieselben zwei Zeilen und ergänzt ihre Summe. B und C unterscheiden sich nur in der Darstellung, nicht in den Daten dahinter.

Bestelldokumente verwenden immer die schlichte Textform, zum Beispiel „Gutschein CODE -2,25“, weil Outlook und PDF-Renderer keinen Chip verlässlich zeichnen.

Was die Zeilen zusammen ergeben

Die Zeilen ergeben zusammen immer die Rabattsumme des Dokuments, auf den Cent genau.

Rundungsdifferenzen von bis zu einem Cent pro Zeile übernimmt die größte Zeile. Ein größerer Rest, der zu keiner Regel gehört, wird zu einer allgemeinen Zeile „Rabatt“, statt einer Regel zugeschrieben zu werden, weil eine falsche Zuordnung schwerer auffällt als eine unbestimmte. Eine Rechnung skaliert die gespeicherten Zeilen auf den Rabattbetrag der Rechnung, eine Gutschrift teilt jede Regel im Verhältnis der erstatteten Mengen auf.

Grenzen

  • Bestellungen aus der Zeit vor der Installation behalten die zusammengefasste Standardzeile. Die Beträge pro Regel lassen sich nicht rekonstruieren, weil Magento sie nie gespeichert hat.
  • Version 1.0 unterstützt kein Multishipping.
  • Über mehrere Rechnungen einer Bestellung können die Beträge pro Regel um einen Cent pro Regel abweichen. Die Rabattsumme jedes Dokuments ist exakt.
  • Bei Amasty bekommt die Geschenkzeile zu viel vom Betrag, wenn eine automatische Regel auch den Geschenkartikel rabattiert. Die Summe bleibt richtig. Abhilfe ist die Amasty-Bedingung „is promo item = No“ an den automatischen Regeln.
  • Bestehende Warenkörbe zeigen die Standardzeile bis zur nächsten Summenberechnung.

Installation

composer require pixelperfectat/magento2-module-discount-lines
composer require pixelperfectat/magento2-module-discount-lines-hyva-checkout
bin/magento setup:upgrade

Die zweite Zeile ist ein Renderer. Installiere einen pro Frontend, das du betreibst, nicht einen insgesamt: -hyva-checkout für Hyvä Checkout, -hyva-theme für die Warenkorbseite des Hyvä-Standardthemes, -luma für Luma. Betreibst du Hyvä-Theme und Hyvä Checkout, installierst du beide. Dazu -amasty-promo oder -mageplaza-giftcard, falls du diese Erweiterungen einsetzt.

Qualität in Zahlen

Ich habe alles in einer frischen Mage-OS-3.1.0-Installation auf PHP 8.5 gebaut und getestet, ausschließlich mit Demodaten. Jedes Paket durchläuft in der CI composer check: composer validate, PHP-Lint, PHP_CodeSniffer, PHPStan und PHPUnit. Vier der sechs Pakete haben zusätzlich Mutationstests mit Infection, die den Build unter einem MSI von 85 oder einem covered-code MSI von 90 scheitern lassen.

Mutationstests sind absichtlich kaputte Kopien des Codes, und der Score ist der Anteil, den die Testsuite davon bemerkt. Jede Zahl unten stammt aus dem öffentlichen CI-Log des daneben verlinkten Laufs, nicht aus einer Release-Notiz.

PaketMSI / covered-code MSILauf
magento2-module-discount-lines93 / 9342242224997, 06.11.2026
-amasty-promo100 / 10042242248810, 06.11.2026
-mageplaza-giftcard99 / 9942242346501, 06.11.2026
-luma93 / 9342242324118, 06.11.2026
-hyva-themekein Infection-Schrittvalidate, lint, cs, stan, test
-hyva-checkoutkein Infection-Schrittvalidate, lint, cs, stan, test

Die beiden Hyvä-Renderer haben keinen Infection-Schritt und tragen deshalb keinen Score. Ein fehlender Score gilt für mich nicht als bestandene Prüfung.

Mutationstests haben vor dem Release einen echten Fehler gefunden: eine Mageplaza-Geschenkkartennummer aus reinen Ziffern brachte die Maskierung zum Absturz.

Bitte mach es kaputt

Die Module sind neu, und eine Testumgebung ist ein schmaler Ausschnitt dessen, was Magento-Shops an Regelkombinationen, Währungen, Steuerkonfigurationen und weiteren Erweiterungen auf die Summen loslassen.

Installiere sie in einer Testumgebung und kombiniere Rabatte. Mehrere Gutscheine gleichzeitig, Gratisversand mit einem Geschenk, eine Teilgutschrift über zwei Rechnungen, ein Währungswechsel zwischen Bestell- und Basiswährung. Wenn die Zahlen nicht zusammenpassen oder eine Zeile falsch aussieht, ist das ein Issue auf GitHub wert, auch dann, wenn es nur hässlich ist und nicht falsch rechnet. Ich lese jedes.

Die E-Mails sind ein eigenes Thema

Die Zeilen stehen auch in der Bestellbestätigung, aber die E-Mail selbst bleibt die von Magento. Ein zweites Modul ersetzt die E-Mail-Vorlagen von Magento, und dieser Beitrag beschreibt es. Keines der beiden Module braucht das andere.

Fragen

Warum zeigt Magento nur eine Rabattzeile?

Magento fasst alle angewendeten Rabatte in der Summenzeile „Rabatt (…)“ zusammen. In der Bestellung speichert es nur `coupon_code`, `discount_description` und `applied_rule_ids`, also keine Beträge pro Regel. Nach dem Checkout lässt sich die Aufteilung nicht mehr rekonstruieren, weshalb E-Mails, Rechnungen und Gutschriften sie nicht anzeigen können.

Kann die Beschreibung einer Warenkorbregel beim Kunden landen?

Ja. `RulesApplier::getRuleLabel()` nimmt in Magento 2.4.7 zuerst das Store-Label der Regel, und wenn ein Gutschein im Spiel ist und kein Store-Label gesetzt ist, nimmt es die Regelbeschreibung, die im Backend ein Notizfeld ist. Eine interne Notiz steht damit in der Rabattzeile des Warenkorbs, der Bestellbestätigung und der Rechnung. Denselben Text habe ich auch in der Versandzeile gesehen, bei einer Bestellung, deren einzige Regel Gratisversand war.

Wie verhalten sich die Zeilen zur Rabattsumme von Magento?

Die Rabattzeilen ergeben zusammen immer genau die Rabattsumme von Magento, auf den Cent genau. Rundungsdifferenzen von bis zu einem Cent pro Zeile übernimmt die größte Zeile. Ein größerer Rest, der zu keiner Regel gehört, wird zu einer allgemeinen Zeile „Rabatt“, statt einer Regel zugeschrieben zu werden. Liegen die Zeilen zusammen über der Rabattsumme, oder widersprechen sich Bestell- und Basiswährung, werden die Zeilen verworfen und es erscheint die Standardzeile von Magento mit der schlichten Beschriftung „Rabatt“.

Welche Frontends werden unterstützt?

Hyvä Checkout, das Hyvä-Standardtheme und Luma, jeweils als eigenes Paket, alle am 1. November 2026 als 1.0.0 veröffentlicht. Das Basispaket kennt kein Frontend, ein weiterer Checkout kann also einen Renderer bekommen, nur existiert heute keiner.

Wo steht dein Magento-Projekt gerade?

Schildere mir kurz, woran du arbeitest, wo du stehst und was bereits versucht wurde. Ich sage dir ehrlich, ob ich der richtige Entwickler dafür bin.