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:
- Datenmodell & Basis-Infrastruktur: Model (Kapitel 4), EAV Entity (10-18), EAV-Attribute custom (13-14), Cache (Kapitel 8), Configuration Type (Kapitel 88).
- Attribute auf bestehenden Entitäten: Product (19), Category (20), Customer (21), Company (22), Sales (23).
- Geschäftslogik & Automatisierung: Observer/Event (29-31, 35-36), Crongroup (32), Cronjob (33), Plugin (38-39), Preference/Rewrite (40-42), Helper (44).
- Frontend & Verwaltung: Controller (45, 50), Router (46), Block (47), ViewModel (48), Widget (55-57), Page-Builder Content Type (58-60).
- Checkout-Integration: Payment Method (62-65), Shipping Method (66-69), Product Type (71-78).
- Schnittstellen: Api (79-81), GraphQL Endpoint (82-83), Customer(Section) Data (84).
- 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.