Store-Credit-Saldo, Historie und Checkout-Einlösung sauber in Hyvä integriert
Magento bringt mit Magento_CustomerBalance ein vollwertiges Store-Credit-System mit, das im Luma-Theme über klassische Blöcke ausgegeben wird. Im Hyvä-Theme fehlt diese Ausgabe meist komplett und muss neu gedacht werden: als ViewModel, als GraphQL-Query und als Alpine-Komponente im Checkout. Dieser Artikel zeigt den kompletten Weg von der Saldo-Anzeige bis zur Einlösung.
Inhaltsverzeichnis
- 1. Store Credit, Gutschein und Guthaben: Was Magento wirklich unterscheidet
- 2. Saldo-Anzeige im Kundenkonto: Block-Logik als ViewModel nach Hyvä portieren
- 3. Saldo und Historie per GraphQL laden
- 4. Guthaben-Historie als Tabelle: Erstattung, Bestellung und Admin-Anpassung
- 5. Einlösung im Checkout: Alpine-Komponente mit Toggle Guthaben verwenden
- 6. Zusammenspiel mit dem Rabattcode-Feld: Reihenfolge und UX bei Ausschluss
- 7. private content / customer-data.js: Saldo als eigene Section pflegen
- 8. Multi-Currency und Website-Scope: Fallstricke bei Store Credit
- 9. Edge Cases: Guthaben über der Bestellsumme, Restguthaben und gesperrte Kombinationen
- 10. Zusammenfassung
- 11. FAQ
1. Store Credit, Gutschein und Guthaben: Was Magento wirklich unterscheidet
In der deutschsprachigen Shop-Praxis wird alles, was den Bestellbetrag reduziert, gerne als Gutschein bezeichnet. Magento kennt dafür jedoch mehrere technisch getrennte Konzepte, die im Hyvä-Theme unterschiedlich behandelt werden müssen. Store Credit, umgesetzt durch das Modul Magento_CustomerBalance, ist ein Kundenguthaben, das auf ein Kundenkonto gebucht wird, etwa durch eine Retoure, eine manuelle Admin-Anpassung oder einen direkten Kauf von Guthaben. Es unterscheidet sich klar von einem Coupon-Code aus dem SalesRule-Modul, der als Zeichenkette im Checkout eingegeben wird und an eine Rabattregel gebunden ist.
Zusätzlich gibt es Magento_GiftCardAccount, physische oder digitale Gutscheinkarten mit eigenem Code, die beim Einlösen letztlich ebenfalls in Store Credit umgewandelt werden können. Wer im Hyvä-Kundenkonto also von Guthaben spricht, meint in den allermeisten Fällen den Saldo aus Magento_CustomerBalance, während Gutschein je nach Kontext sowohl eine Gift Card als auch einen Coupon-Code bezeichnen kann. Diese Begriffsklärung ist keine Wortklauberei, sondern entscheidet darüber, welches Backend-Model, welche Tabelle und welcher GraphQL-Endpunkt für eine Anforderung tatsächlich zuständig ist.
Konfigurativ findet sich Store Credit im Admin unter Stores > Configuration > Customers > Customer Configuration > Customer Balance Options, mit Einstellungen wie automatischer Rückbuchung bei Retoure oder der Sichtbarkeit des Saldos. Diese Konfigurationswerte werden im Hyvä-ViewModel ausgelesen, nicht hart im Template kodiert, damit Redakteure die Sichtbarkeit weiterhin über den Admin steuern können.
2. Saldo-Anzeige im Kundenkonto: Block-Logik als ViewModel nach Hyvä portieren
Im Luma-Theme liefert Magento\CustomerBalance\Block\Info den Saldo an das Template. In Hyvä wird diese Logik nicht wiederverwendet, sondern als schlankes ArgumentInterface-ViewModel neu implementiert, das direkt über das Balance-Model lädt und den Website-Scope berücksichtigt. Wichtig ist, den Saldo nicht am Kunden selbst zu cachen, da er sich nach jeder Bestellung oder Admin-Anpassung ändert und im Kundenkonto stets aktuell sein muss.
Das folgende ViewModel kapselt Laden und Formatierung und kann sowohl im Dashboard als auch in einer eigenen Guthaben-Seite eingebunden werden. Die Formatierung nutzt bewusst PricingHelper, damit die Ausgabe konsistent zur restlichen Preisdarstellung im Theme bleibt.
<?php
declare(strict_types=1);
namespace Mironsoft\CustomerBalance\ViewModel;
use Magento\Customer\Model\Session as CustomerSession;
use Magento\CustomerBalance\Model\Balance;
use Magento\CustomerBalance\Model\BalanceFactory;
use Magento\Framework\Pricing\Helper\Data as PricingHelper;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Store\Model\StoreManagerInterface;
/**
* Stellt den aktuellen Store-Credit-Saldo für das Hyvä-Kundenkonto bereit.
* Kapselt Laden und Formatierung, damit Templates keine Model-Logik enthalten.
*/
class StoreCreditBalance implements ArgumentInterface
{
/**
* @param CustomerSession $customerSession Aktuelle Kundensession
* @param BalanceFactory $balanceFactory Factory für das Balance-Model
* @param PricingHelper $pricingHelper Formatierung des Währungsbetrags
* @param StoreManagerInterface $storeManager Zugriff auf den aktiven Website-Scope
*/
public function __construct(
private readonly CustomerSession $customerSession,
private readonly BalanceFactory $balanceFactory,
private readonly PricingHelper $pricingHelper,
private readonly StoreManagerInterface $storeManager,
) {
}
/**
* Lädt den aktuellen Saldo für den eingeloggten Kunden im aktiven Website-Scope.
*
* @return float
*/
public function getBalanceAmount(): float
{
if (!$this->customerSession->isLoggedIn()) {
return 0.0;
}
/** @var Balance $balance */
$balance = $this->balanceFactory->create();
$balance->setCustomerId((int) $this->customerSession->getCustomerId());
$balance->setWebsiteId((int) $this->storeManager->getWebsite()->getId());
$balance->loadByCustomer();
return (float) $balance->getAmount();
}
/**
* Formatiert den Saldo als Währungsstring für die Ausgabe im Template.
*
* @return string
*/
public function getFormattedBalance(): string
{
return $this->pricingHelper->currency($this->getBalanceAmount(), true, false);
}
/**
* Prueft, ob überhaupt ein positiver Saldo vorhanden ist.
*
* @return bool
*/
public function hasBalance(): bool
{
return $this->getBalanceAmount() > 0.0;
}
}
3. Saldo und Historie per GraphQL laden
Für ein PWA-nahes oder headless angebundenes Hyvä-Setup ist der direkte Model-Zugriff im ViewModel nicht immer der richtige Weg, vor allem wenn Saldo und Historie asynchron nachgeladen werden sollen. Adobe Commerce liefert dafür das Modul Magento_CustomerBalanceGraphQl mit dem Feld store_credit am Customer-Type. In reinem Magento Open Source ist dieses Feld nicht vorhanden, hier muss ein eigener Resolver ergänzt werden, der intern dieselbe Balance-Klasse anspricht wie das ViewModel aus dem vorherigen Abschnitt.
Die Historie selbst wird von Magento nicht als GraphQL-Feld ausgeliefert und muss praktisch immer selbst resolved werden, üblicherweise über Magento\CustomerBalance\Model\ResourceModel\Balance\History\CollectionFactory. Wichtig für die Cache-Strategie ist, dass diese Query stets personalisierte, kundenbezogene Daten liefert und deshalb niemals über den Full-Page-Cache, sondern ausschließlich authentifiziert über den Customer-Token abgefragt werden darf.
query CustomerStoreCredit {
customer {
store_credit {
enabled
current_balance {
value
currency
}
}
}
}
# Erweiterung um Historie erfordert einen eigenen Resolver,
# da Magento diese Daten nicht nativ per GraphQL ausliefert.
query CustomerStoreCreditHistory($pageSize: Int = 20) {
customer {
store_credit {
current_balance {
value
currency
}
}
balance_history(pageSize: $pageSize) {
items {
action_type
balance_delta {
value
currency
}
additional_info
created_at
}
}
}
}
4. Guthaben-Historie als Tabelle: Erstattung, Bestellung und Admin-Anpassung
Kundinnen und Kunden wollen nachvollziehen können, wie ihr Saldo zustande gekommen ist. Die Historie-Einträge in Magento_CustomerBalance tragen einen Typ, der sich grob in drei Kategorien gruppieren lässt: Erstattung bei Retouren oder Stornierungen, Bestellung bei Einlösung im Checkout und Admin-Anpassung bei manueller Buchung durch das Support-Team. Im Hyvä-Template lohnt es sich, diese Typen in ein eigenes Badge-Mapping zu übersetzen, damit die Tabelle auf einen Blick lesbar ist, statt interne Codes wie used oder updated anzuzeigen.
Für größere Historien ist Pagination Pflicht, da manche Kunden über Jahre hinweg dutzende Einträge ansammeln. Das folgende Rendering-Pattern nutzt ein separates StoreCreditHistory-ViewModel, das die Collection lädt, sortiert und paginiert, während das Template ausschließlich für die Darstellung zuständig bleibt.
<?php /** @var \Mironsoft\CustomerBalance\ViewModel\StoreCreditHistory $historyViewModel */ ?>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200 text-left">
<th class="py-2 pr-4">Datum</th>
<th class="py-2 pr-4">Typ</th>
<th class="py-2 pr-4">Betrag</th>
<th class="py-2">Saldo danach</th>
</tr>
</thead>
<tbody>
<?php foreach ($historyViewModel->getHistoryItems() as $item): ?>
<tr class="border-b border-gray-100">
<td class="py-2 pr-4"><?= $escaper->escapeHtml($item->getFormattedDate()) ?></td>
<td class="py-2 pr-4">
<span class="inline-flex items-center rounded-full px-2 py-1 text-xs <?= $escaper->escapeHtmlAttr($item->getTypeBadgeClass()) ?>">
<?= $escaper->escapeHtml($item->getTypeLabel()) ?>
</span>
</td>
<td class="py-2 pr-4"><?= $escaper->escapeHtml($item->getFormattedDelta()) ?></td>
<td class="py-2"><?= $escaper->escapeHtml($item->getFormattedBalanceAfter()) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
5. Einlösung im Checkout: Alpine-Komponente mit Toggle Guthaben verwenden
Im Checkout darf Store Credit nicht als eigenständiges Formularfeld auftauchen, sondern als klar erkennbarer Toggle, der den vorhandenen Saldo anzeigt und dessen Anwendung sofort im Total-Bereich sichtbar macht. Eine Alpine-Komponente eignet sich hierfür ideal, da sie ohne zusätzliches JavaScript-Bundle auskommt und sich nahtlos in bestehende Hyvä-Checkout-Templates einfügt.
Nach dem Umschalten wird ein Request an einen eigenen Controller oder Resolver gesendet, der das use_store_credit-Flag auf der Quote setzt. Anschließend müssen die Totals neu geladen werden, wofür sich das customer-data.js-Event reload-customer-section-data anbietet, damit sich Warenkorb-Mini-Cart und Checkout-Summary konsistent aktualisieren, ohne die komplette Seite neu zu laden.
<div
x-data="{
useStoreCredit: <?= $storeCreditViewModel->hasBalance() && $storeCreditViewModel->isApplied() ? 'true' : 'false' ?>,
balance: <?= (float) $storeCreditViewModel->getBalanceAmount() ?>,
applying: false,
async toggle() {
this.applying = true;
try {
await hyva.postForm('<?= $escaper->escapeUrl($block->getUrl('storecredit/checkout/apply')) ?>', {
use_store_credit: this.useStoreCredit ? 0 : 1
});
this.useStoreCredit = !this.useStoreCredit;
window.dispatchEvent(new CustomEvent('reload-customer-section-data', {
detail: ['cart']
}));
} finally {
this.applying = false;
}
}
}"
class="rounded border border-gray-200 p-4"
>
<label class="flex items-center gap-3">
<input type="checkbox" x-model="useStoreCredit" x-on:change="toggle()" :disabled="applying">
<span>Guthaben verwenden (<span x-text="balance.toFixed(2)"></span> verfügbar)</span>
</label>
</div>
6. Zusammenspiel mit dem Rabattcode-Feld: Reihenfolge und UX bei Ausschluss
Store Credit und Coupon-Code sind aus Sicht des Quote-Totals-Systems zwei unabhängige Collectors: Magento\SalesRule\Model\Total\Quote\Discount für den Rabattcode und Magento\CustomerBalance\Model\Total\Quote\Customerbalance für das Guthaben. In der Standard-Sortierreihenfolge greift der Rabattcode zuerst auf den Zwischensumme, danach reduziert Store Credit den bereits rabattierten Betrag weiter. Für die UI bedeutet das, dass eine Änderung am Coupon-Feld immer auch die Anzeige des angewendeten Guthabens neu berechnen muss, nicht umgekehrt.
Anders als oft angenommen, gibt es in Magento Core keine native Einstellung, die Store Credit und Coupon-Code gegenseitig ausschließt. Beide lassen sich technisch gleichzeitig anwenden. Wer aus Geschäftsgründen einen Ausschluss braucht, etwa um Rabattaktionen nicht mit Kundenguthaben zu kombinieren, muss dies über einen eigenen Observer auf sales_quote_collect_totals_before oder eine Validierung im Coupon-Apply-Controller umsetzen und die UI entsprechend deaktivieren, statt sich auf einen Core-Schalter zu verlassen.
Für die UX empfiehlt sich eine klare Priorisierung im Layout: Rabattcode-Feld zuerst, Store-Credit-Toggle direkt darunter, mit einem Hinweistext, sobald eine Kombination aus geschäftlicher Sicht gesperrt ist. So bleibt für Kundinnen und Kunden nachvollziehbar, warum ein Feld inaktiv erscheint, statt dass die Anwendung stillschweigend fehlschlägt.
// Beispielhafte Totals-Antwort nach Anwendung von Coupon-Code und Store Credit.
// Der Rabatt reduziert zuerst die Zwischensumme, Customerbalance greift danach.
{
"totals": {
"subtotal": 129.90,
"discount": -10.00,
"customerbalance": -25.00,
"shipping": 4.90,
"tax": 18.02,
"grand_total": 116.82
}
}
7. private content / customer-data.js: Saldo als eigene Section pflegen
Store Credit ist typisch privater, kundenbezogener Inhalt und gehört deshalb in eine eigene customer-data.js-Section statt in den serverseitig gecachten Seiteninhalt. Nur so bleibt der Full-Page-Cache für alle Besucher gültig, während der Saldo dennoch pro Kunde aktuell im Frontend erscheint. Die Section wird über SectionPoolInterface registriert und liefert ein einfaches Array mit Saldo und Formatierung zurück.
Nach jeder Aktion, die den Saldo verändert, etwa Einlösung im Checkout oder ein simulierter Admin-Push, muss die Section gezielt invalidiert werden, damit sie beim nächsten Request neu vom Server geladen wird. Ein globaler Reload aller Sections ist meist unnötig teuer und sollte vermieden werden, wenn nur der Guthaben-Bereich betroffen ist.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Customer\CustomerData\SectionPoolInterface">
<arguments>
<argument name="sectionSourceMap" xsi:type="array">
<item name="storecredit" xsi:type="string">Mironsoft\CustomerBalance\CustomerData\StoreCredit</item>
</argument>
</arguments>
</type>
</config>
8. Multi-Currency und Website-Scope: Fallstricke bei Store Credit
Der Store-Credit-Saldo wird pro Kunde und Website gespeichert, nicht pro Store View und nicht pro Währung. Betreibt ein Shop mehrere Websites mit unterschiedlicher Basiswährung, existiert für jede Website ein eigener, unabhängiger Saldo, der weder automatisch übertragen noch umgerechnet wird. Ein Kunde, der auf der deutschen Website Guthaben aufgebaut hat, sieht auf einer separaten Schweizer oder US-Website mit eigener Website-ID einen Saldo von null, auch wenn beide Websites denselben Kunden-Account teilen.
Innerhalb einer einzelnen Website mit mehreren Store Views und Anzeigewährungen entsteht ein anderes Problem: Der gespeicherte Saldo liegt immer in der Basiswährung der Website vor, während die Anzeige im Frontend in der aktuell gewählten Store-View-Währung erfolgt. Die Umrechnung zur Anzeigezeit kann bei stark schwankenden Wechselkursen zu einem gefühlten Widerspruch zwischen dem im Kundenkonto angezeigten Betrag und dem im Checkout tatsächlich angerechneten Betrag führen, wenn zwischen beiden Aufrufen ein Kursupdate stattgefunden hat.
In der Praxis empfiehlt es sich, im ViewModel und in der GraphQL-Antwort immer explizit die Basiswährung mitzugeben und im UI transparent zu machen, in welcher Währung der Saldo tatsächlich geführt wird, statt sich allein auf die aktuell aktive Store-View-Währung zu verlassen. Das vermeidet Support-Anfragen, wenn Kunden zwischen Store Views mit unterschiedlicher Anzeigewährung wechseln.
9. Edge Cases: Guthaben über der Bestellsumme, Restguthaben und gesperrte Kombinationen
Ist das verfügbare Guthaben höher als die Bestellsumme, begrenzt der Customerbalance-Total-Collector die tatsächlich angerechnete Summe automatisch auf den offenen Betrag, sodass der Grand Total nicht negativ werden kann. Das restliche, nicht verbrauchte Guthaben bleibt unverändert auf dem Kundenkonto bestehen und steht für die nächste Bestellung zur Verfügung. Im UI sollte dieser Fall explizit kommuniziert werden, etwa mit einem Hinweis wie einem verbleibenden Restguthaben nach Abschluss der Bestellung, damit Kundinnen und Kunden nicht den Eindruck bekommen, ihr komplettes Guthaben sei verbraucht worden.
Ein zweiter Edge Case betrifft Zahlungsmethoden bei vollständiger Deckung durch Guthaben: Deckt der Saldo die gesamte Bestellsumme inklusive Versand und Steuer ab, entfällt die Notwendigkeit einer klassischen Zahlungsmethode, und Magento bietet dafür intern die Free-Payment-Methode an, die im Checkout entsprechend als eigener Zahlungsschritt sichtbar gemacht werden muss, statt den Checkout ins Leere laufen zu lassen.
Da es, wie im vorherigen Abschnitt beschrieben, keinen nativen Ausschluss zwischen Coupon-Code und Store Credit gibt, muss ein eigens implementierter Ausschluss auch alle Edge Cases sauber abdecken, etwa den Fall, dass ein Kunde zuerst Guthaben aktiviert und danach versucht, einen ausschließenden Coupon-Code einzugeben. Die UI sollte in diesem Moment das jeweils andere Feld deaktivieren oder eine klare Fehlermeldung anzeigen, statt den Request serverseitig ohne Rückmeldung abzulehnen.
| Transaktionsart | Typischer Auslöser | Effekt auf Saldo | Sichtbarkeit im Frontend |
|---|---|---|---|
| Erstattung | Retoure oder Storno mit Rückbuchung auf Guthaben | Erhöht den Saldo | Sofort in Historie und customer-data.js Section |
| Bestellung | Einlösung von Guthaben im Checkout | Verringert den Saldo | Nach Bestellabschluss in Historie sichtbar |
| Admin-Anpassung | Manuelle Buchung durch das Support-Team | Erhöht oder verringert den Saldo | Erscheint mit Kommentarfeld in der Historie |
| Guthaben-Aktivierung | Direkter Kauf oder Aktivierung von Guthaben | Erhöht den Saldo | Sichtbar direkt nach Zahlungsbestätigung |
| Stornierte Bestellung | Rückbuchung bei Order-Cancel nach Einlösung | Erhöht den Saldo | Sofort nach Stornierung in Historie |
| Ablauf (falls konfiguriert) | Automatischer Verfall nach Ablaufdatum | Verringert den Saldo | Als eigener Historieneintrag mit Ablaufhinweis |
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
Guthaben & Gutscheine
Store Credit ist kein Coupon-Code
Magento_CustomerBalance verwaltet Kundenguthaben getrennt von SalesRule-Coupons und Gift Cards. Diese Trennung bestimmt Model, Tabelle und GraphQL-Endpunkt.
GraphQL-Feld ist Commerce-spezifisch
Das native store_credit Feld stammt aus Magento_CustomerBalanceGraphQl. In Open Source muss ein eigener Resolver für Saldo und Historie ergänzt werden.
Reihenfolge im Checkout beachten
Der Rabattcode reduziert die Zwischensumme zuerst, Store Credit greift danach auf den bereits rabattierten Betrag. Ein nativer gegenseitiger Ausschluss existiert nicht.
Saldo gehört in eine eigene Section
Als private, kundenbezogene customer-data.js Section bleibt der Full-Page-Cache gültig, während der Saldo gezielt und ohne kompletten Reload aktualisiert wird.