VRPay in Laravel: Zahlungsstatus als State Machine
Ein Zahlungsanbieter ist nicht einfach Redirect plus Callback. Der unangenehme Teil beginnt danach: Wann darfst du eine Bestellung wirklich erfüllen?
Im konkreten Fall geht es um einen Laravel-Shop für Reservierungen und Gutscheine. VRPay kann sofort bestätigen, später nachziehen, ablehnen oder erst beim erneuten Öffnen der Statusseite ein finales Ergebnis liefern. Genau dafür lohnt sich eine kleine State Machine rund um pending, paid und declined. Fulfillment läuft nur einmal, auch wenn die Statusprüfung mehrmals denselben Erfolg sieht.
Warum Zahlungsanbieter keine einfache Weiterleitung sind
Eine VRPay-Anbindung hat vier bewegliche Teile.
Zuerst bereitet dein Checkout die Bestellung und die Zahlung vor. Danach übernimmt ein gehostetes Payment-Widget des Anbieters die Eingabe. Anschließend kommt der Kunde mit einem Rückkehr-Parameter zurück. Erst danach fragt dein Shop den tatsächlichen Status ab.
Das klingt linear. Ist es aber nicht. Der Rückkehr-Parameter kann fehlen. Er kann ungültig sein. Der Anbieter kann einen Pending-Code liefern, obwohl der Kunde gerade bezahlt hat. Eine Statusabfrage kann kurz fehlschlagen. Ein Kunde kann die Statusseite neu laden. Und wenn dein Fulfillment an der falschen Stelle hängt, erzeugst du PDFs, E-Mails oder externe Übergaben doppelt.
Abgelehnte Zahlungen sind der einfache Fall. Schwieriger sind Zahlungen, die weder klar erfolgreich noch klar abgelehnt sind. Die dürfen nicht als Fehler behandelt werden, nur weil sie noch nicht final sind.
Der Ablauf im Laravel-Shop
Der Shop trennt Vorbereitung, Anbieter-UI und Statusbestätigung sauber voneinander.
CheckoutSummary::pay()validiert AGB und Zahlungsregeln. Danach erstelltOrderService::create()die Bestellung mit offenem Zahlungsstatus.PaymentPay::initializePayment()bereitet den VRPay-Checkout überVRPayConnectorundPostCheckoutPreparevor.- Die Blade-Ansicht lädt VRPays
paymentWidgets.jserst, wenn eine Checkout-ID existiert. Ohne vorbereiteten Checkout gibt es kein Widget. PaymentStatus::mount()nimmt nach der Rückkehr denresourcePathentgegen und validiert ihn, bevor der Shop eine Folgeanfrage stellt.OrderService::checkPaymentStatus()speichert die Anbieter-Transaktions-ID und wendet das Ergebnis auf die Bestellung an.PaymentStatus::refreshPaymentStatus()fragt weiter nach, solange die Bestellung nochpendingist und das Retry-Fenster offen bleibt.
Die Statusseite ist damit kein Danke-Page-Anhängsel. Sie ist ein kontrollierter Übergang zwischen Anbieterwelt und Shopwelt.
State Machine statt Zahlungs-if-else
Die Logik braucht nur wenige Zustände, aber klare Übergänge.
| Status | Bedeutung | Nächster Schritt |
|---|---|---|
pending | Zahlung ist angelegt, aber noch nicht final bestätigt oder abgelehnt. | Status erneut abfragen. |
paid | Anbieter hat ein finales Erfolgsmuster geliefert. | Fulfillment auslösen, danach nichts mehr doppelt tun. |
declined | Anbieter hat ein finales Ablehnungsmuster geliefert. | Fehler anzeigen und neue Zahlung erlauben. |
OrderService::applyTransactionResult() macht daraus eine belastbare Regel.
Ist eine Bestellung bereits paid, endet die Methode sofort. PDFs, Kundenevent und externe Übergabe laufen nicht noch einmal.
Erkennt der Shop ein Erfolgsmuster im Ergebniscode, setzt er die Bestellung auf paid, speichert die finale Transaktions-ID und das Zahlungsdatum, erzeugt die PDFs, verschickt das Kundenevent und stößt den Export der Bestellung an.
Erkennt der Shop ein Ablehnungsmuster, wird die Bestellung declined.
Alle anderen Ergebnisse bleiben pending. Dazu gehören echte Pending-Codes, verzögerte Ergebnisse und technische Anbieterfehler. Das ist Absicht. Solange keine finale Aussage vorliegt, bleibt der Prozess retrybar.
Wichtig ist die Trennung der IDs: payment_transaction_id ist die Anbieter-ID, mit der der Shop später erneut nachfragen kann. transaction_id ist die finale Zahlungsreferenz der abgeschlossenen Zahlung. Beides in ein Feld zu werfen wirkt am Anfang bequem, rächt sich aber bei Revisit, Polling und Auswertung.
Sicherheit an den Rändern
Die gefährlichen Stellen liegen an den Grenzen. Also dort, wo Browser, Anbieter und Shop aufeinandertreffen.
PaymentStatus akzeptiert den resourcePath nur, wenn er wie ein interner VRPay-Pfad aussieht. Freie URLs werden nicht weitergereicht. Die Zahlungsrouten sind gedrosselt. Eine Bestellung sieht nur der eingeloggte Besitzer oder ein Gast, dessen Session-Daten und Gast-Token zur Bestellung passen. Der Token-Vergleich nutzt die Semantik von hash_equals, damit kein Timing-Leak aus einem simplen Stringvergleich entsteht.
Auch das Logging bleibt eng. Der Shop protokolliert nur whitelisted Statusfelder wie Ergebniscode, Beschreibung, Zahlungsart und Transaktionsbezug. Keine Rohantworten. Keine Kundendaten. Keine Karten- oder Kontodaten.
Die praktische Regel dahinter: An den Rändern geschlossen scheitern, innen retrybar bleiben. Ein ungültiger Rückkehr-Parameter stoppt sofort. Ein temporärer Anbieterfehler hält die Bestellung dagegen offen, damit die nächste Abfrage noch gewinnen kann.
Fulfillment erst nach finalem Ergebnis
Der Shop erzeugt Gutscheine, Reservierungsunterlagen und externe Übergaben erst nach finalem Erfolg. Nicht beim Erstellen der Bestellung. Nicht beim Laden des Widgets. Nicht beim bloßen Zurückkehren vom Anbieter.
Das ist der Unterschied zwischen “Kunde war im Payment-Flow” und “Zahlung ist bestätigt”. Nur der zweite Zustand darf Fulfillment auslösen.
Vorkasse läuft bewusst daneben. Sie ist ein eigener Bypass mit Zahlungsanweisung und sofortiger weiterer Verarbeitung, aber nicht Teil der VRPay-State-Machine. Für Gutscheine und Reservierungen zählt im VRPay-Pfad nur das finale Anbieterergebnis.
Was du daraus für dein Laravel-Projekt mitnimmst
- Vorbereitung und Statusbestätigung voneinander trennen;
- Rückkehr-Parameter validieren, bevor Folgeanfragen abgeschickt werden;
- die Anbieter-Transaktions-ID für spätere Revisits speichern;
- Fulfillment idempotent gestalten;
- Pending als echten Zustand behandeln, nicht als Fehler;
- Erfolg, Pending, Ablehnung, fehlenden Parameter, ungültigen Parameter, temporären Anbieterfehler und erschöpftes Retry-Fenster testen.
Wenn dein Checkout mehr können muss als Standard-Shop-Verhalten, braucht er saubere Laravel-Anwendungslogik. Genau dafür bauen wir Laravel-Projekte. Wie das bei Reservierungen, Gutscheinen und VRPay zusammenspielt, siehst du im Projekt Vulkaneifel Therme.
Häufige Fragen
Wann darf ein Laravel-Shop eine VRPay-Bestellung erfüllen?
Erst nach einem finalen Erfolg. Nicht beim Erstellen der Bestellung, nicht beim Laden des Payment-Widgets und nicht beim bloßen Zurückkehren vom Anbieter. Nur der Zustand paid darf das Fulfillment auslösen – also Gutscheine, Reservierungsunterlagen und externe Übergaben. Das ist der Unterschied zwischen Kunde war im Payment-Flow und Zahlung ist bestätigt; nur der zweite Zustand zählt.
Wie verhindert man doppeltes Fulfillment bei Zahlungen?
Über idempotentes Fulfillment und eine kleine State Machine. Die Methode applyTransactionResult endet sofort, wenn die Bestellung bereits paid ist – PDFs, Kundenevent und externe Übergabe laufen dann nicht erneut. So kann die Statusseite mehrmals geladen oder gepollt werden und denselben Erfolg sehen, ohne dass etwas doppelt erzeugt wird.
Wie sollte man einen Pending-Zahlungsstatus behandeln?
Als echten Zustand, nicht als Fehler. Solange kein finales Erfolgs- oder Ablehnungsmuster vorliegt – dazu gehören echte Pending-Codes, verzögerte Ergebnisse und temporäre technische Anbieterfehler – bleibt die Bestellung pending und der Prozess retrybar. Erst ein finales Muster setzt die Bestellung auf paid oder declined. Eine noch nicht finale Zahlung als Fehler zu behandeln, wäre falsch.
Wie sichert man die VRPay-Rückkehr in Laravel ab?
Nach der Regel: an den Rändern geschlossen scheitern, innen retrybar bleiben. Der resourcePath wird nur akzeptiert, wenn er wie ein interner VRPay-Pfad aussieht; freie URLs werden nicht weitergereicht. Die Zahlungsrouten sind gedrosselt, eine Bestellung sieht nur der berechtigte Besitzer oder Gast (Token-Vergleich mit hash_equals gegen Timing-Leaks), und geloggt werden nur whitelisted Statusfelder – keine Rohantworten, Kunden- oder Zahlungsdaten.
Passende Leistungen
Passende Projekte
Sr. Full-Stack Developer
Frontend & Backend. Entwickelt skalierbare Webanwendungen mit Fokus auf saubere Architektur und Wartbarkeit.