Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Alle Bereiche im Überblick: wie die 32 Bausteine zusammenspielen

Alle Bereiche im Überblick: wie die 32 Bausteine zusammenspielen

~8 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026

Elf Blöcke, 96 Kapitel, 32 einzelne Modul-Bereiche - jeder für sich in seinem eigenen Kapitel erklärt, verankert in seinem eigenen kleinen Ausschnitt des Punkte-Ledgers, der Prämien-Entity oder eines einzelnen Attributs. Was in dieser separaten Betrachtung leicht verloren geht: Keiner dieser Bausteine steht für sich allein. Ein einziger Einkauf durchläuft in Sekundenbruchteilen fast alle 32 Bereiche nacheinander - von der Beobachtung des Bestellabschlusses bis zur GraphQL-Antwort Wochen später, wenn derselbe Kunde seinen Punktestand über ein Headless-Frontend abfragt. Dieses Kapitel zeichnet genau diesen Weg nach.

Der Weg eines einzelnen Einkaufs

Am einfachsten wird das Zusammenspiel sichtbar, wenn man einem einzigen Kunden folgt: Er kauft ein, sammelt Punkte, sieht sie im Konto, löst sie später gegen eine Prämie ein - und ein Cronjob räumt auf, falls er es nie tut.

Datenfluss: ein Einkauf von der Bestellung bis zur Anzeige

Kauf abgeschlossen
   |
   v
sales_order_place_after (globales Event, Kapitel 29/30)
   |-- AwardPointsOnOrderPlaced (Kapitel 30)
   |     |-- PointsCalculator::calculatePoints()        (Kapitel 5)
   |     |-- CategoryBonusResolver::resolveForProduct() (Kapitel 27)
   |     `-- PointsLedgerRepository::save() -> TYPE_EARN (Kapitel 6)
   |           `-- loyalty_points_balance aktualisiert   (Kapitel 21/30)
   |                 `-- LoyaltyTierBackend::beforeSave() -> Tier neu (Kapitel 26)
   |                       `-- EVENT_TIER_CHANGED dispatcht (Kapitel 35)
   |-- RedeemPointsOnOrderPlaced (Kapitel 63/67, falls Punkte eingesetzt wurden)
   `-- CreditPurchasedPointsPackageOnOrderPlaced (Kapitel 76, Punkte-Paket-Kauf)

Punktestand anzeigen (vier Wege, ein Wert)
   |-- ViewModel PointsBalance   -> Konto-Dashboard/Widget (Kapitel 48/52/56)
   |-- CustomerData PointsBalance -> Mini-Cart/Checkout    (Kapitel 84)
   |-- REST GET /V1/loyalty/points/mine                   (Kapitel 80)
   `-- GraphQL loyaltyPointsSummary                       (Kapitel 82)

Prämie einlösen
   RewardCatalog/Redeem-Controller, REST, GraphQL (Kapitel 49/50/81/83)
      `-- RewardRedemptionManagement::redeem() -> TYPE_REDEEM (Kapitel 81)

Checkout-Integration
   |-- ApplyPointsRedemptionToTotalsPlugin -> Rabatt          (Kapitel 39)
   |-- PointsRedemptionFacade-Zahlungsart -> Randfall 100%    (Kapitel 62-65)
   `-- FreeShippingByPoints-Versandart -> Gratisversand       (Kapitel 66-69)

Verfall
   ExpirePoints-Cronjob, taeglich -> TYPE_EXPIRE, Tier neu berechnet (Kapitel 32-34)

1. Punkte verdienen: Observer → PointsCalculator → Ledger

Der global registrierte Observer AwardPointsOnOrderPlaced (Kapitel 30) reagiert auf sales_order_place_after (Kapitel 29) und delegiert die eigentliche Rechenarbeit vollständig an PointsCalculator::calculatePoints() (Kapitel 5) - unter Berücksichtigung des Produkt-Multiplikators loyalty_points_multiplier (Kapitel 19) und, über CategoryBonusResolver::resolveForProduct() (Kapitel 27), des höchsten aktiven Kategorie-Bonus loyalty_bonus_category (Kapitel 20). Das Ergebnis landet doppelt: als loyalty_points_earned auf Order und Order Item (Kapitel 23) und als TYPE_EARN-Eintrag im Ledger über PointsLedgerRepositoryInterface::save() (Kapitel 6). Kaufte der Kunde stattdessen ein Punkte-Paket-Produkt (Kapitel 71-78), läuft ein dritter, unabhängiger Observer auf demselben Event: CreditPurchasedPointsPackageOnOrderPlaced (Kapitel 76) - bewusst getrennt, damit sich die beiden Idempotenz-Guards nicht gegenseitig blockieren.

2. Punktestand und Treue-Stufe aktualisieren

Beide Observer schreiben loyalty_points_balance ausschließlich über CustomerRepositoryInterface::getCustomAttribute()/setCustomAttribute() (Kapitel 21/30), nie über rohes getData()/setData(). Jedes einzelne Speichern läuft durch LoyaltyTierBackend::beforeSave() (Kapitel 26), das die Treue-Stufe automatisch aus dem neuen Punktestand neu berechnet - mit denselben Bronze/Silber/Gold-Schwellen, die PointsCalculator intern kennt (Kapitel 5). Ändert sich die Stufe dabei tatsächlich, dispatcht das Backend Model das modul-eigene EVENT_TIER_CHANGED (Kapitel 35) - ein Erweiterungspunkt, auf den Kapitel 102 noch einmal zurückkommt.

3. Den Punktestand anzeigen: vier Wege, ein Wert

Kontoseite und Widget lesen über die PointsBalance-ViewModel (Kapitel 48, eingebunden in Kapitel 51/52/56), der Mini-Cart über die CustomerData-Section loyalty_points (Kapitel 84), externe Clients über GET /V1/loyalty/points/mine (Kapitel 80) und Headless-Frontends über die GraphQL-Query loyaltyPointsSummary (Kapitel 82). REST und GraphQL greifen dabei sogar auf dieselbe Implementierung PointsManagementInterface zurück (Kapitel 80, in Kapitel 82 unverändert wiederverwendet) - vier Oberflächen, ein einziger Lesepfad.

4. Eine Prämie einlösen

Prämienkatalog und Redeem-Controller im Storefront (Kapitel 49/50) sowie die REST- und GraphQL-Einlösung (Kapitel 81/83) rufen alle dieselbe Methode auf: RewardRedemptionManagementInterface::redeem() (Kapitel 81) - eine einzige Implementierung, drei Oberflächen. Sie bucht einen TYPE_REDEEM-Ledger-Eintrag und reduziert den Punktestand über denselben CustomerRepositoryInterface-Pfad wie beim Verdienen, wodurch LoyaltyTierBackend auch hier automatisch die Tier-Stufe neu berechnet.

5. Punkte im Checkout einsetzen: Rabatt, Zahlungsart, Versandart

Der Regelfall ist ein Rabatt: ApplyPointsRedemptionToTotalsPlugin (Kapitel 39) reduziert grand_total direkt in den Checkout-Totals. Die eigene Zahlungsart mironsoft_loyalty_points (Kapitel 62-65) springt nur im Randfall ein, in dem Punkte den kompletten Betrag decken; die Versandart FreeShippingByPoints (Kapitel 66-69) tauscht Punkte gegen Gratisversand. Erst wenn die Bestellung wirklich platziert wird, bucht RedeemPointsOnOrderPlaced (Kapitel 63/67) die tatsächlichen Ledger-Einträge - der Rabatt im Warenkorb bleibt bis dahin reine Vorschau, ohne Buchung, um Mehrfachausführung zu vermeiden.

6. Verfall und Aufräumen

Der tägliche Cronjob ExpirePoints (Kapitel 32-34) bucht TYPE_EXPIRE-Einträge nach demselben Soll-Ist-Reconciliation-Muster, das auch ReversePointsOnCreditmemoSave (Kapitel 31) für TYPE_ADJUST-Rückbuchungen bei Gutschriften nutzt - und reduziert damit wieder loyalty_points_balance, wieder mit automatischer Tier-Neuberechnung.

Die 32 Bereiche in sieben Gruppen

Statt der rohen Liste aus der Spezifikation hier dieselben 32 Bereiche, thematisch gruppiert - als Landkarte für die eigene Praxis:

  1. Datenmodell & Basis-Infrastruktur: Model (Kapitel 4), EAV Entity (10-18), EAV-Attribute custom (13-14), Cache (Kapitel 8), Configuration Type (Kapitel 88).
  2. Attribute auf bestehenden Entitäten: Product (19), Category (20), Customer (21), Company (22), Sales (23).
  3. Geschäftslogik & Automatisierung: Observer/Event (29-31, 35-36), Crongroup (32), Cronjob (33), Plugin (38-39), Preference/Rewrite (40-42), Helper (44).
  4. Frontend & Verwaltung: Controller (45, 50), Router (46), Block (47), ViewModel (48), Widget (55-57), Page-Builder Content Type (58-60).
  5. Checkout-Integration: Payment Method (62-65), Shipping Method (66-69), Product Type (71-78).
  6. Schnittstellen: Api (79-81), GraphQL Endpoint (82-83), Customer(Section) Data (84).
  7. Konfiguration, Sprache & Qualität: System/Config/Setting (7), Console Command (9), Language (89-90), Unit Test (91-95).

Tipp: Zwei Klassen tauchen in praktisch jeder Gruppe wieder auf: PointsLedgerRepositoryInterface (Kapitel 6) und PointsCalculator (Kapitel 5). Sie sind das Rückgrat des Moduls - alle anderen 30 Bereiche liefern letztlich nur unterschiedliche Ein- und Ausgänge für genau diese beiden Klassen.

Mit diesem Überblick im Kopf widmet sich Kapitel 98 der Frage, wie diese ganze Konstruktion in der module.xml überhaupt korrekt lädt.