Store Credit / Kundenguthaben in Magento 2 selbst entwickeln
AI generated
M2
di.xml
Magento 2 · Store Credit · Kundenguthaben · Custom Module
Store Credit und Kundenguthaben in Magento 2
ein eigenes Modul fuer das Guthaben-Konto statt eines Gutschein-Codes

Magento Open Source kennt kein natives Store-Credit-Feature: die Funktion fuer ein kontogebundenes Kundenguthaben existiert nur in Adobe Commerce. Wer Store Credit trotzdem anbieten will, um Retouren als Guthaben statt als Rueckbuchung zu erstatten oder ein Treueprogramm mit einem laufenden Kundenguthaben-Konto aufzubauen, entwickelt ein eigenes Modul: mit deklarativem Schema, Service Contracts, einem unveraenderlichen Ledger und einer eigenen Checkout-Total-Berechnung.

18 Min. Lesezeit db_schema.xml · Service Contracts · Observer · Quote Total Magento 2.4.8-p4 · PHP 8.4 · Hyvä

1. Store Credit als Konzept: Guthaben-Konto statt Code

Ein Store Credit unterscheidet sich fundamental von einem Gift-Card-Code. Eine Gift Card ist ein einloesbarer Code mit einem eigenen, oft anonymen Guthabenwert, der unabhaengig von einem konkreten Kundenkonto existiert und im Prinzip weitergegeben werden kann. Store Credit, im Deutschen meist Kundenguthaben genannt, ist dagegen ein laufendes Konto, das fest an die Kundenentitaet gebunden ist: kein Code, keine Weitergabe, sondern ein Saldo, der bei jedem Login und in jedem Checkout automatisch zur Verfuegung steht und sich ueber die Zeit veraendert.

Die typischen Einsatzszenarien fuer ein Kundenguthaben-Konto liegen dort, wo eine Rueckerstattung nicht zwingend auf das urspruengliche Zahlungsmittel zurueckfliessen muss. Bei einer Retoure kann der Betrag statt einer Rueckbuchung auf die Kreditkarte als Store Credit gutgeschrieben werden, was Zahlungsdienstleister-Gebuehren spart und den Kunden zum naechsten Einkauf motiviert. Ein Treueprogramm kann regelmaessig kleine Betraege als Kundenguthaben gutschreiben, ohne dass dafuer Gutscheincodes generiert und verteilt werden muessen. Auch Kulanzfaelle, bei denen der Support einem Kunden ohne konkrete Retoure einen Betrag gutschreiben moechte, profitieren von einem zentralen Guthaben-Konto statt vieler einzelner Gutscheincodes.

Magento Open Source bringt kein natives Store-Credit-Feature mit: das Modul Magento_CustomerBalance, das dieses Konzept in der Commerce-Edition unter dem Namen "Store Credit" beziehungsweise "Customer Balance" abbildet, ist ausschliesslich Adobe Commerce vorbehalten. Fuer Open-Source-Projekte bleibt daher nur der Weg ueber ein eigenstaendiges Modul, das die Kernidee, ein persistentes, an den Kunden gebundenes Guthaben mit vollstaendiger Nachvollziehbarkeit, sauber im eigenen Namespace nachbaut. Genau dieser Aufbau, von der Datenbank ueber die Service Contracts bis zum Hyvä-Frontend, ist Thema dieses Artikels.

2. Architektur eines eigenen Store-Credit-Moduls

Ein sauberes Store-Credit-Modul beginnt mit einem eigenen Namespace unter der Projektkonvention Mironsoft. Der Modulname Mironsoft_StoreCredit liegt unter app/code/Mironsoft/StoreCredit und benoetigt zwingend eine registration.php sowie eine etc/module.xml mit Abhaengigkeiten zu den Kernmodulen, die das Guthaben-Konto ueberhaupt sinnvoll machen: Magento_Customer fuer die Kundenentitaet, Magento_Sales fuer die Verknuepfung mit Bestellungen und Gutschriften, sowie Magento_Quote fuer die spaetere Checkout-Integration.

Die module.xml definiert diese Reihenfolge ueber sequence-Eintraege, damit das Setup-System die Kundendaten-Tabellen vor dem eigenen Kundenguthaben-Schema anlegt. Die Ordnerstruktur folgt dem ueblichen Magento-2-Muster: Api und Api/Data fuer die Service-Contract-Interfaces, Model und Model/ResourceModel fuer die Implementierung, Observer fuer die Event-Listener, Block/ViewModel fuer die Hyvä-Anbindung sowie Controller/Adminhtml fuer die manuelle Anpassung im Backend.

Wichtig fuer die Dual-Vendor-Konvention dieses Projekts: Namespace, Modulname und alle Konfigurationspfade werden konsequent unter Mironsoft gepflegt, waehrend eine parallele Kopie unter Abrams mit identischer Struktur und ausschliesslich getauschtem Namespace existiert. Beide Varianten teilen dieselbe Abhaengigkeit zu Mironsoft_Core beziehungsweise Abrams_Core, sodass gemeinsame Hilfsklassen nicht dupliziert werden muessen.

3. Datenmodell: Guthaben-Tabelle und Ledger-Prinzip

Das Datenmodell eines eigenen Store-Credit-Moduls besteht aus zwei Tabellen mit klar getrennter Verantwortung. Die erste Tabelle, mironsoft_storecredit_balance, haelt genau eine Zeile pro Kunde mit dem aktuellen Saldo: eine 1:1-Beziehung zu customer_entity ueber eine eindeutige Fremdschluessel-Spalte. Diese Tabelle beantwortet schnell und ohne Aggregation die Frage "wie hoch ist das Kundenguthaben gerade".

Die zweite Tabelle, mironsoft_storecredit_ledger, ist das eigentliche Herzstueck des Datenmodells: ein unveraenderliches Journal aller Buchungen. Jede Gutschrift und jede Belastung erzeugt genau eine neue Zeile mit Betrag, Grund, Referenz auf die ausloesende Entitaet, dem Saldo nach dieser Buchung und, bei manuellen Anpassungen, der ID des Admin-Users. Die Regel, die dieses Ledger-Prinzip erst robust macht: der Saldo in mironsoft_storecredit_balance wird niemals per direktem UPDATE veraendert, sondern ausschliesslich innerhalb derselben Datenbank-Transaktion, die auch die zugehoerige Ledger-Zeile schreibt. Damit laesst sich der aktuelle Saldo jederzeit aus der Summe aller Ledger-Eintraege eines Kunden rekonstruieren und gegen Abweichungen pruefen, was fuer Buchhaltung und Support unverzichtbar ist.

Die Indizes auf customer_id und reason_code sowie auf reference_type und reference_id beschleunigen sowohl die Kontohistorie im Kundenkonto als auch die Idempotenz-Pruefung, die in Abschnitt 5 fuer die automatische Gutschrift bei Retouren wichtig wird. Die vollstaendige db_schema.xml fuer beide Tabellen sieht wie folgt aus:


<?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_storecredit_balance" resource="default" engine="innodb" comment="Store Credit Balance">
        <column xsi:type="int" name="entity_id" unsigned="true" nullable="false" identity="true" comment="Entity ID"/>
        <column xsi:type="int" name="customer_id" unsigned="true" nullable="false" comment="Customer ID"/>
        <column xsi:type="decimal" name="current_balance" scale="4" precision="12" unsigned="false" nullable="false" default="0.0000" comment="Current Store Credit Balance"/>
        <column xsi:type="varchar" name="currency_code" nullable="false" length="3" default="EUR" comment="Currency Code"/>
        <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="unique" referenceId="MIRONSOFT_STORECREDIT_BALANCE_CUSTOMER_ID">
            <column name="customer_id"/>
        </constraint>
        <constraint xsi:type="foreign" referenceId="MIRONSOFT_STORECREDIT_BALANCE_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID"
                    table="mironsoft_storecredit_balance" column="customer_id"
                    referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/>
    </table>
    <table name="mironsoft_storecredit_ledger" resource="default" engine="innodb" comment="Store Credit Ledger">
        <column xsi:type="int" name="ledger_id" unsigned="true" nullable="false" identity="true" comment="Ledger ID"/>
        <column xsi:type="int" name="customer_id" unsigned="true" nullable="false" comment="Customer ID"/>
        <column xsi:type="decimal" name="amount_delta" scale="4" precision="12" unsigned="false" nullable="false" comment="Amount Delta, positive equals credit, negative equals debit"/>
        <column xsi:type="decimal" name="balance_after" scale="4" precision="12" unsigned="false" nullable="false" comment="Balance After This Entry"/>
        <column xsi:type="varchar" name="reason_code" nullable="false" length="64" comment="Reason Code, e.g. creditmemo_refund, admin_adjustment"/>
        <column xsi:type="varchar" name="reference_type" nullable="true" length="64" comment="Reference Entity Type"/>
        <column xsi:type="int" name="reference_id" unsigned="true" nullable="true" comment="Reference Entity ID"/>
        <column xsi:type="int" name="admin_user_id" unsigned="true" nullable="true" comment="Admin User ID for manual adjustments"/>
        <column xsi:type="timestamp" name="created_at" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/>
        <constraint xsi:type="primary" referenceId="PRIMARY">
            <column name="ledger_id"/>
        </constraint>
        <constraint xsi:type="foreign" referenceId="MIRONSOFT_STORECREDIT_LEDGER_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID"
                    table="mironsoft_storecredit_ledger" column="customer_id"
                    referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/>
        <index referenceId="MIRONSOFT_STORECREDIT_LEDGER_CUSTOMER_ID_REASON_CODE" indexType="btree">
            <column name="customer_id"/>
            <column name="reason_code"/>
        </index>
        <index referenceId="MIRONSOFT_STORECREDIT_LEDGER_REFERENCE_TYPE_REFERENCE_ID" indexType="btree">
            <column name="reference_type"/>
            <column name="reference_id"/>
        </index>
    </table>
</schema>

4. Service Contracts: BalanceRepositoryInterface und BalanceManagementInterface

Die Projektkonvention verlangt Service Contracts statt direkter Model-Zugriffe, und genau das zahlt sich bei einem Store-Credit-Modul besonders aus. Unter Api/Data definiert ein BalanceInterface die reinen Getter und Setter der Saldo-Entitaet. Unter Api beschreibt BalanceRepositoryInterface das Laden und Speichern per Kunden-ID, waehrend BalanceManagementInterface die eigentliche Geschaeftslogik kapselt: credit() und debit() als klar benannte Operationen, die niemals den Saldo direkt manipulieren, sondern immer ueber die Ledger-Buchung laufen.

Die Implementierung BalanceManagement nutzt konsequent Constructor Property Promotion und kapselt die Transaktionslogik: Saldo laden, neuen Wert berechnen, Saldo speichern und Ledger-Zeile schreiben, alles innerhalb derselben Datenbank-Transaktion. Schlaegt einer der Schritte fehl, wird die komplette Transaktion zurueckgerollt, sodass niemals ein inkonsistenter Zustand zwischen Saldo-Tabelle und Ledger entstehen kann. Diese Klasse ist die einzige Stelle im gesamten Modul, die schreibend auf den Kundenguthaben-Saldo zugreifen darf, alle anderen Komponenten, Observer, Admin-Controller und ViewModel, rufen ausschliesslich diese Service Contracts auf.


<?php

declare(strict_types=1);

namespace Mironsoft\StoreCredit\Model;

use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Exception\LocalizedException;
use Mironsoft\StoreCredit\Api\BalanceManagementInterface;
use Mironsoft\StoreCredit\Api\BalanceRepositoryInterface;
use Mironsoft\StoreCredit\Model\ResourceModel\Ledger as LedgerResource;

/**
 * Service class for crediting and debiting the Store Credit ledger.
 */
class BalanceManagement implements BalanceManagementInterface
{
    /**
     * @param BalanceRepositoryInterface $balanceRepository Repository for the balance entity
     * @param LedgerFactory $ledgerFactory Factory for ledger entry entities
     * @param LedgerResource $ledgerResource Resource model for persisting ledger entries
     * @param ResourceConnection $resourceConnection Database connection for transaction handling
     */
    public function __construct(
        private readonly BalanceRepositoryInterface $balanceRepository,
        private readonly LedgerFactory $ledgerFactory,
        private readonly LedgerResource $ledgerResource,
        private readonly ResourceConnection $resourceConnection
    ) {
    }

    /**
     * Credits an amount to the customer's Store Credit balance and writes an immutable ledger entry.
     *
     * @param int $customerId Customer entity ID
     * @param float $amount Amount to credit, always positive
     * @param string $reasonCode Reason code, e.g. "creditmemo_refund"
     * @param string|null $referenceType Reference entity type, e.g. "creditmemo"
     * @param int|null $referenceId Reference entity ID
     * @return float New balance after the credit
     * @throws LocalizedException
     */
    public function credit(
        int $customerId,
        float $amount,
        string $reasonCode,
        ?string $referenceType = null,
        ?int $referenceId = null
    ): float {
        if ($amount <= 0.0) {
            throw new LocalizedException(__('Credit amount must be positive.'));
        }

        $connection = $this->resourceConnection->getConnection();
        $connection->beginTransaction();

        try {
            $balance = $this->balanceRepository->getByCustomerId($customerId);
            $newBalance = round($balance->getCurrentBalance() + $amount, 4);
            $balance->setCurrentBalance($newBalance);
            $this->balanceRepository->save($balance);

            $ledgerEntry = $this->ledgerFactory->create();
            $ledgerEntry->setCustomerId($customerId);
            $ledgerEntry->setAmountDelta($amount);
            $ledgerEntry->setBalanceAfter($newBalance);
            $ledgerEntry->setReasonCode($reasonCode);
            $ledgerEntry->setReferenceType($referenceType);
            $ledgerEntry->setReferenceId($referenceId);
            $this->ledgerResource->save($ledgerEntry);

            $connection->commit();
        } catch (\Throwable $exception) {
            $connection->rollBack();
            throw new LocalizedException(__('Store Credit could not be credited.'), $exception);
        }

        return $newBalance;
    }
}

5. Automatische Gutschrift bei Retoure

Der ueberzeugendste Anwendungsfall fuer ein eigenes Store-Credit-Modul ist die automatische Gutschrift bei einer Retoure. Ein Observer auf das Event sales_order_creditmemo_save_after prueft, ob fuer die betreffende Gutschrift die Erstattung als Kundenguthaben gewaehlt wurde, und ruft dann BalanceManagementInterface::credit() mit dem Gutschriftsbetrag auf. Der Observer selbst enthaelt keine Buchungslogik, er delegiert vollstaendig an den Service Contract aus Abschnitt 4.

Entscheidend ist die Idempotenz: das Event sales_order_creditmemo_save_after kann bei einem erneuten Speichern derselben Gutschrift, etwa nach einem Reindex oder einer manuellen Korrektur im Admin, mehrfach ausgeloest werden. Ohne Schutzmechanismus wuerde derselbe Betrag mehrfach gutgeschrieben. Die Loesung: vor jeder Gutschrift prueft der Observer ueber reference_type und reference_id im Ledger, ob fuer diese konkrete Gutschrift bereits eine Buchung existiert, und bricht andernfalls ohne Fehler ab. Diese Pruefung macht den gesamten Vorgang wiederholbar und sicher gegen Doppelbuchungen.


<?php

declare(strict_types=1);

namespace Mironsoft\StoreCredit\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Sales\Model\Order\Creditmemo;
use Mironsoft\StoreCredit\Api\BalanceManagementInterface;
use Mironsoft\StoreCredit\Api\LedgerRepositoryInterface;
use Psr\Log\LoggerInterface;

/**
 * Credits Store Credit automatically when a credit memo is created with refund-to-storecredit selected.
 */
class CreditMemoStoreCreditObserver implements ObserverInterface
{
    private const REASON_CODE = 'creditmemo_refund';
    private const REFERENCE_TYPE = 'creditmemo';

    /**
     * @param BalanceManagementInterface $balanceManagement Service for crediting the ledger
     * @param LedgerRepositoryInterface $ledgerRepository Repository for checking existing ledger entries
     * @param LoggerInterface $logger Logger for failed credit attempts
     */
    public function __construct(
        private readonly BalanceManagementInterface $balanceManagement,
        private readonly LedgerRepositoryInterface $ledgerRepository,
        private readonly LoggerInterface $logger
    ) {
    }

    /**
     * Executes the observer on sales_order_creditmemo_save_after.
     *
     * @param Observer $observer Event observer instance
     * @return void
     */
    public function execute(Observer $observer): void
    {
        /** @var Creditmemo $creditmemo */
        $creditmemo = $observer->getEvent()->getData('creditmemo');

        if (!$creditmemo->getData('refund_to_storecredit')) {
            return;
        }

        // Idempotency guard: skip if this creditmemo already produced a ledger entry.
        // Prevents double crediting on reindex, resave or repeated event dispatch.
        if ($this->ledgerRepository->existsByReference(self::REFERENCE_TYPE, (int) $creditmemo->getEntityId())) {
            return;
        }

        $customerId = (int) $creditmemo->getOrder()->getCustomerId();
        if ($customerId === 0) {
            return;
        }

        try {
            $this->balanceManagement->credit(
                $customerId,
                (float) $creditmemo->getGrandTotal(),
                self::REASON_CODE,
                self::REFERENCE_TYPE,
                (int) $creditmemo->getEntityId()
            );
        } catch (\Throwable $exception) {
            $this->logger->error('Store Credit credit failed for creditmemo ' . $creditmemo->getEntityId(), ['exception' => $exception]);
        }
    }
}

6. Checkout-Integration: eigener Quote Address Total Collector

Damit das Kundenguthaben im Checkout tatsaechlich verrechnet wird, reicht die reine Existenz des Saldos nicht aus: Magento muss den Betrag in die Grand-Total-Berechnung der Quote einbeziehen. Der dafuer vorgesehene Erweiterungspunkt ist ein eigener Total Collector, eine Klasse, die von Magento\Quote\Model\Quote\Address\Total\AbstractTotal erbt und die Methoden collect() und fetch() implementiert. In collect() wird der verfuegbare Store-Credit-Betrag mit dem aktuellen Grand Total verglichen und maximal bis zur Hoehe des Grand Total abgezogen, damit die Bestellung niemals einen negativen Gesamtbetrag ausweist.

Registriert wird dieser Total Collector nicht in der di.xml, sondern ueber die dedizierte totals.xml, die Magento fuer genau diesen Zweck vorsieht. Der sort_order entscheidet, an welcher Stelle in der Kette der Total-Berechnung, nach Versand und Steuer, aber vor der finalen Rundung, der Store-Credit-Abzug greift. Diese saubere Trennung ueber ein eigenes XML-Schema statt einer generischen Plugin-Loesung entspricht dem von Magento vorgesehenen Weg fuer Quote-Totals und bleibt bei Core-Updates stabil.


<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Quote:etc/totals.xsd">
    <total_collectors sales_channel="website">
        <default>
            <storecredit instance="Mironsoft\StoreCredit\Model\Total\Quote\StoreCredit" sort_order="100"/>
        </default>
    </total_collectors>
    <total_collectors sales_channel="quote">
        <default>
            <storecredit instance="Mironsoft\StoreCredit\Model\Total\Quote\StoreCredit" sort_order="100"/>
        </default>
    </total_collectors>
</config>

7. Admin-UI: Guthaben manuell anpassen mit Audit-Log

Neben der automatischen Gutschrift bei Retouren braucht jedes Store-Credit-Modul eine manuelle Anpassungsmoeglichkeit im Backend, etwa fuer Kulanzfaelle oder Korrekturen durch den Support. Ein eigenes UI-Component-Grid unter Customer > Store Credit zeigt pro Kunde den aktuellen Saldo sowie die vollstaendige Ledger-Historie mit Datum, Betrag, Grund und, sofern vorhanden, dem verantwortlichen Admin-User. Ein Formular erlaubt das Hinzufuegen einer neuen Buchung mit Pflichtfeld fuer den Grund.

Gemaess Projektregel benoetigt jede neue Funktionalitaet einen eigenen ACL-Eintrag in etc/acl.xml, hier zum Beispiel Mironsoft_StoreCredit::manage, der im Adminhtml-Controller vor jeder Anpassung geprueft wird. Jede manuelle Aenderung laeuft, wie in Abschnitt 4 beschrieben, ausschliesslich ueber BalanceManagementInterface und erzeugt eine Ledger-Zeile mit der ID des ausfuehrenden Admin-Users. Damit ist jede Aenderung am Kundenguthaben vollstaendig nachvollziehbar: wer, wann, warum und in welcher Hoehe.

8. Frontend-Anzeige im Kundenkonto: ViewModel-Pattern

Im Kundenkonto-Bereich des Hyvä-Themes soll der Kunde seinen aktuellen Store-Credit-Saldo und die Historie seiner Buchungen einsehen koennen. Der Projektkonvention entsprechend wird dafuer kein Block, sondern ein ViewModel nach dem ArgumentInterface-Muster verwendet, das per Layout-XML in das Template account/storecredit.phtml injiziert wird. Das ViewModel kapselt den Zugriff auf die Kundensession und delegiert an BalanceRepositoryInterface und LedgerRepositoryInterface, ohne selbst Geschaeftslogik zu enthalten.

Im Template iteriert Hyvä ueber das Array aus getLedgerHistory() und rendert eine einfache Tabelle mit Datum, Betrag und Grund je Zeile, ergaenzt um Tailwind-Klassen fuer positive und negative Betraege. Da das ViewModel keine schwergewichtigen Objekte zurueckgibt, sondern primitive Werte und flache Arrays, bleibt die Komponente auch mit aktiviertem Hyvä-CSP-Modus unproblematisch und benoetigt keine zusaetzlichen Inline-Skripte.


<?php

declare(strict_types=1);

namespace Mironsoft\StoreCredit\ViewModel;

use Magento\Customer\Model\Session as CustomerSession;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Mironsoft\StoreCredit\Api\BalanceRepositoryInterface;
use Mironsoft\StoreCredit\Api\LedgerRepositoryInterface;

/**
 * Exposes the current Store Credit balance and ledger history to Hyva templates.
 */
class CustomerBalance implements ArgumentInterface
{
    /**
     * @param CustomerSession $customerSession Current customer session
     * @param BalanceRepositoryInterface $balanceRepository Repository for the balance entity
     * @param LedgerRepositoryInterface $ledgerRepository Repository for ledger history entries
     */
    public function __construct(
        private readonly CustomerSession $customerSession,
        private readonly BalanceRepositoryInterface $balanceRepository,
        private readonly LedgerRepositoryInterface $ledgerRepository
    ) {
    }

    /**
     * Returns the current Store Credit balance for the logged in customer.
     *
     * @return float
     */
    public function getCurrentBalance(): float
    {
        $customerId = (int) $this->customerSession->getCustomerId();
        if ($customerId === 0) {
            return 0.0;
        }

        return $this->balanceRepository->getByCustomerId($customerId)->getCurrentBalance();
    }

    /**
     * Returns the ledger history for the logged in customer, newest entries first.
     *
     * @param int $limit Maximum number of entries to return
     * @return array<int, array<string, mixed>>
     */
    public function getLedgerHistory(int $limit = 20): array
    {
        $customerId = (int) $this->customerSession->getCustomerId();
        if ($customerId === 0) {
            return [];
        }

        return $this->ledgerRepository->getRecentByCustomerId($customerId, $limit);
    }
}

9. Store Credit im Vergleich: eigenes Modul vs. Gift Cards vs. Reward Points

Ein eigenes Store-Credit-Modul ist nicht die einzige Moeglichkeit, Kunden monetaeren Mehrwert zurueckzugeben. Gift Cards mit Codes und Bonuspunkte-Systeme (Reward Points) loesen aehnliche Geschaeftsziele mit anderen technischen und buchhalterischen Voraussetzungen. Die folgende Tabelle stellt die drei Ansaetze entlang der Dimensionen gegenueber, die in der Praxis ueber die Wahl entscheiden.

Dimension Store Credit (eigenes Modul) Gift Card Codes Reward Points
Bindung an Kundenkonto fest, kein Code, kein Transfer keine Bindung, Code frei uebertragbar fest, aber meist an Punktewert gebunden
Implementierungsaufwand hoch: Schema, Service Contracts, Total, Admin, Frontend mittel: Code-Generierung, Einloese-Logik hoch: Punkteregeln, Umrechnungslogik, Ablaufdaten
Buchhaltungslogik Ledger-Prinzip, vollstaendig auditierbar Code-Status (aktiv/eingeloest), weniger granular Punktestand, oft ohne monetaeren Bezug
Einsatzzweck Retouren-Erstattung, Kulanz, laufendes Guthaben Geschenke, Marketing-Aktionen, B2B-Voucher Kundenbindung, Wiederkauf-Anreiz

Die drei Ansaetze schliessen sich nicht gegenseitig aus. In der Praxis kombinieren viele Shops ein Store-Credit-Konto fuer Retouren und Kulanz mit Gift-Card-Codes fuer Marketingaktionen, waehrend Reward Points als separates Anreizsystem darueber liegen. Entscheidend ist, dass jedes System sein eigenes, klar abgegrenztes Datenmodell besitzt und nicht versucht wird, alle drei Konzepte in eine gemeinsame Tabelle zu pressen, denn die Buchhaltungssemantik unterscheidet sich in Details, die schnell zu Inkonsistenzen fuehren.

10. Zusammenfassung

Ein eigenes Store-Credit-Modul fuer Magento 2 Open Source ist kein triviales Feature-Flag, sondern ein vollstaendiger Custom-Build: eine Saldo-Tabelle und ein unveraenderliches Ledger als Datenmodell, Service Contracts, die jede Buchung ausschliesslich ueber eine zentrale Management-Klasse leiten, ein Observer fuer die automatische Gutschrift bei Retouren mit sauberer Idempotenz-Pruefung, ein eigener Quote-Total-Collector fuer die Checkout-Verrechnung sowie ein ACL-gesichertes Admin-Grid und ein Hyvä-ViewModel fuer die Kundenkonto-Anzeige.

Der rote Faden durch alle Abschnitte ist das Ledger-Prinzip: niemals den Saldo direkt schreiben, immer eine nachvollziehbare Buchung erzeugen. Wer dieses Prinzip konsequent durchhaelt, bekommt ein Kundenguthaben-System, das sich sowohl gegen Doppelbuchungen als auch gegen spaeter auftauchende Buchhaltungsfragen verteidigen laesst, ohne dass dafuer die Adobe-Commerce-Lizenz noetig waere.

Store Credit und Kundenguthaben in Magento 2, das Wichtigste auf einen Blick

Datenmodell & Ledger

Saldo-Tabelle plus unveraenderliches Ledger-Journal. Saldo wird nie direkt geschrieben, immer aus einer Buchung abgeleitet.

Service Contracts

BalanceRepositoryInterface und BalanceManagementInterface buendeln jede Gutschrift und Belastung in einer Transaktion.

Checkout-Integration

Eigener Total Collector ueber totals.xml, verrechnet den Saldo sicher gegen den Grand Total, nie ins Negative.

Admin & Frontend

ACL-gesichertes Admin-Grid fuer manuelle Anpassungen, Hyvä-ViewModel fuer die Anzeige im Kundenkonto.

11. FAQ: Store Credit in Magento 2

1Was ist der Unterschied zwischen Store Credit und einer Gift Card?
Store Credit ist an das Kundenkonto gebunden und ohne Code direkt im Checkout verfuegbar. Eine Gift Card ist ein eigenstaendiger, uebertragbarer Code.
2Gibt es Store Credit nativ in Magento Open Source?
Nein. Magento_CustomerBalance existiert nur in Adobe Commerce. In Open Source ist ein eigenes Modul der einzige Weg.
3Wie verhindert das Ledger-Prinzip Buchungsfehler?
Jede Buchung erzeugt eine unveraenderliche Ledger-Zeile mit Saldo nach der Buchung, der Saldo laesst sich daraus jederzeit rekonstruieren.
4Warum nie direkt per UPDATE den Saldo aendern?
Ohne begleitende Ledger-Zeile ist die Aenderung nicht nachvollziehbar. Saldo-Update und Ledger-Buchung muessen in einer Transaktion erfolgen.
5Wie wird Store Credit im Checkout verrechnet?
Ein eigener Total Collector ueber totals.xml zieht den Betrag vom Grand Total ab, maximal bis zu dessen Hoehe.
6Wie wird eine doppelte Gutschrift verhindert?
Pruefung ueber reference_type und reference_id im Ledger vor jeder Buchung verhindert Doppelbuchungen bei Reindex oder Resave.
7Braucht das Modul eine eigene ACL-Berechtigung?
Ja, ein eigener ACL-Eintrag fuer die manuelle Anpassung im Admin-Grid ist Pflicht gemaess Projektkonvention.
8Wie zeigt man das Guthaben im Hyvä-Frontend an?
Ueber ein ViewModel nach ArgumentInterface-Muster, das Saldo und Ledger-Historie als einfache Werte bereitstellt.
9Kann Store Credit mit Gutscheincodes kombiniert werden?
Ja, beide Systeme lassen sich parallel betreiben, solange die Datenmodelle sauber getrennt bleiben.
10Was passiert mit dem Guthaben bei Kontoloeschung?
Eine Foreign-Key-Kaskade auf customer_entity entfernt Saldo und Ledger automatisch, alternativ ist ein Export vor der Loeschung sinnvoll.

Mironsoft

Magento-2-Individualentwicklung, Service Contracts und Hyvä-Frontends

Store Credit fuer euren Magento-2-Shop entwickeln lassen?

Wir bauen euer eigenes Store-Credit- und Kundenguthaben-Modul, von der db_schema.xml ueber Service Contracts und Checkout-Total bis zum Hyvä-Frontend im Kundenkonto, sauber nach Magento-2-Konventionen.

Modul-Konzeption

Datenmodell, Ledger-Design und Service Contracts fuer euer Store-Credit-Konzept

Checkout-Integration

Eigener Quote-Total-Collector, sauber ueber totals.xml registriert

Admin & Hyvä-Frontend

ACL-gesichertes Admin-Grid und ViewModel-basierte Kundenkonto-Anzeige