15 KiB
Payment Status Legende & Race Condition Fix
Datum: 28.01.2026
Bereich: Payment System / Payone Integration
Status: ✅ Abgeschlossen
Übersicht
Umfassende Verbesserung des Payment-Link-Systems mit folgenden Komponenten:
- Dynamische mehrsprachige Status-Legende
- Race Condition Fix für Payone Requests
- Status Update für alle Bestellungstypen
- ShoppingInstance Model Korrektur
- Artisan Command für Datenbank-Cleanup
Problem 1: Fehlende Status-Übersicht
Ausgangslage
- Benutzer sahen Payment Status-Badges in der Payment Links Übersicht
- Keine Erklärung, was die verschiedenen Status/Farben bedeuten
- Keine Legende zur Orientierung
Lösung
Dynamische Status-Legende oberhalb der Tabelle mit allen verfügbaren Status.
Problem 2: Race Condition bei Payone Requests
Ausgangslage
Payone sendet mehrere Status-Updates für eine Zahlung:
appointed- Zahlung autorisiertpaid- Zahlung abgeschlossen
Problem: Wenn die Requests in falscher Reihenfolge ankommen:
1. paid kommt zuerst → txaction = 'paid' ✓
2. appointed kommt später → txaction = 'appointed' ✗ (überschreibt paid!)
Dies führte zu:
- Falschen Status in
shopping_orders.txactionundshopping_payments.txaction - ShoppingInstance Status blieb teilweise auf 4 (appointed) statt 10 (paid)
- Verwirrung bei Benutzern und Support
Root Cause
In PayoneController.php wurden die txaction Felder immer überschrieben:
$shopping_order->txaction = $data['txaction']; // IMMER überschrieben!
$shopping_payment->txaction = $data['txaction']; // IMMER überschrieben!
Lösung
Prioritätsprüfung vor dem Update:
$txaction_priority = [
'appointed' => 1,
'pending' => 2,
'failed' => 3,
'paid' => 10, // höchste Priorität - finaler Status
];
// Nur updaten wenn neue Priorität höher ist
if ($new_priority > $current_priority) {
$shopping_order->txaction = $data['txaction'];
$shopping_payment->txaction = $data['txaction'];
}
Problem 3: Status nur für Abo-Bestellungen
Ausgangslage
In Payment::paymentStatusPaidAction() wurde Status 10 (link_paid) nur gesetzt wenn:
if ($shopping_order->is_abo) {
Util::setInstanceStatusByPayment($shopping_payment, 10);
}
Resultat:
- ✅ Abo-Bestellungen: Status korrekt auf 10
- ❌ Normale Bestellungen: Status blieb auf 4 (appointed)
Lösung
Status 10 wird jetzt immer bei erfolgreicher Zahlung gesetzt:
// Set payment link status to paid for all orders
if ($shopping_payment) {
Util::setInstanceStatusByPayment($shopping_payment, 10);
}
// Abo-spezifische Logik separat
if ($shopping_order->is_abo) {
AboHelper::setAboActive($shopping_order, 2, true);
}
Problem 4: ShoppingInstance Model
Ausgangslage
- Tabelle
shopping_instanceshatidentifier(VARCHAR) als Primary Key - Laravel Model hatte keine Primary Key Konfiguration
- Eloquent verwendete standardmäßig
id(nicht existent) save()schlug fehl mit: "Unknown column 'id' in 'where clause'"
Lösung
Primary Key im Model korrekt konfiguriert:
class ShoppingInstance extends Model
{
protected $primaryKey = 'identifier';
public $incrementing = false;
protected $keyType = 'string';
// ...
}
Implementierung
1. OrderPaymentService - Zentrale Status-Definition
Datei: app/Services/OrderPaymentService.php
public static function getStatusBadgeClasses()
{
return [
'link_sent' => 'default',
'link_openly' => 'info',
'link_paid' => 'secondary',
'link_check' => 'warning',
'link_pending' => 'warning',
'link_appointed' => 'warning',
'link_failed' => 'danger',
'link_canceled' => 'danger'
];
}
public static function getStatusBadge(ShoppingInstance $shoppingInstance)
{
$status = $shoppingInstance->getStatus();
$badgeClasses = self::getStatusBadgeClasses(); // Nutzt zentrale Definition
if (isset($badgeClasses[$status])) {
return sprintf(
' <span class="badge badge-pill badge-%s">%s</span>',
$badgeClasses[$status],
__('payment.' . $status)
);
}
return '';
}
Vorteile:
- ✅ Eine zentrale Stelle für Status-Definitionen
- ✅ DRY Prinzip (Don't Repeat Yourself)
- ✅ Leicht erweiterbar für neue Status
2. Payment Links View - Dynamische Legende
Datei: resources/views/user/order/payment/index.blade.php
{{-- Status-Legende --}}
<div class="card mb-3">
<div class="card-body">
<h6 class="card-title mb-3">{{ __('tables.status') }} - {{ __('legend') }}</h6>
<div class="row">
@foreach(\App\Services\OrderPaymentService::getStatusBadgeClasses() as $statusKey => $badgeClass)
<div class="col-md-3 mb-2">
<span class="badge badge-pill badge-{{ $badgeClass }}">
{{ __('payment.' . $statusKey) }}
</span>
</div>
@endforeach
</div>
</div>
</div>
Vorteile:
- ✅ Vollständig dynamisch
- ✅ Automatisch synchron mit Backend-Logik
- ✅ Mehrsprachig über Laravel Translations
- ✅ Neue Status erscheinen automatisch
3. PayoneController - Race Condition Fix
Datei: app/Http/Controllers/Api/PayoneController.php
// Define txaction priority (higher number = higher priority)
$txaction_priority = [
'appointed' => 1,
'pending' => 2,
'failed' => 3,
'paid' => 10, // highest priority - final state
];
$current_priority = isset($txaction_priority[$shopping_order->txaction])
? $txaction_priority[$shopping_order->txaction] : 0;
$new_priority = isset($txaction_priority[$data['txaction']])
? $txaction_priority[$data['txaction']] : 0;
// Only update txaction if new priority is higher than current
if ($new_priority > $current_priority) {
$shopping_order->txaction = $data['txaction'];
$shopping_order->save();
$shopping_payment->txaction = $data['txaction'];
$shopping_payment->save();
} else {
MyLog::writeLog(
'payone',
'info',
'txaction not updated (current has higher/equal priority)',
$data,
false
);
}
Szenarien:
| Szenario | Request 1 | Request 2 | Resultat | Status |
|---|---|---|---|---|
| Normal | appointed (1) | paid (10) | paid | ✅ Korrekt |
| Race | paid (10) | appointed (1) | paid | ✅ Korrekt (nicht überschrieben) |
| Failed | appointed (1) | failed (3) | failed | ✅ Korrekt |
4. Payment Service - Status für alle Bestellungen
Datei: app/Services/Payment.php
Vorher:
// the Order is Pay, so we can set the Status in the Abo
if ($shopping_order->is_abo) {
if ($shopping_payment) {
Util::setInstanceStatusByPayment($shopping_payment, 10); // link_paid
}
AboHelper::setAboActive($shopping_order, 2, true);
}
Nachher:
// Set payment link status to paid for all orders
if ($shopping_payment) {
Util::setInstanceStatusByPayment($shopping_payment, 10); // link_paid
$shopping_payment->identifier = null;
$shopping_payment->save();
}
// the Order is Pay, so we can set the Status in the Abo
if ($shopping_order->is_abo) {
AboHelper::setAboActive($shopping_order, 2, true);
}
5. ShoppingInstance Model - Primary Key
Datei: app/Models/ShoppingInstance.php
class ShoppingInstance extends Model
{
protected $primaryKey = 'identifier';
public $incrementing = false;
protected $keyType = 'string';
// Rest des Models...
}
6. Artisan Command - Datenbank Cleanup
Datei: app/Console/Commands/FixPaymentLinkStatus.php
class FixPaymentLinkStatus extends Command
{
protected $signature = 'payment:fix-link-status {--dry-run : Run without making changes}';
protected $description = 'Fix payment link status for paid orders';
public function handle()
{
$isDryRun = $this->option('dry-run');
// Find all paid payments with identifiers
$paidPayments = ShoppingPayment::whereNotNull('identifier')
->whereHas('shopping_order', function ($query) {
$query->where('paid', 1)
->where('txaction', 'paid');
})
->get();
foreach ($paidPayments as $payment) {
$instance = ShoppingInstance::where('identifier', $payment->identifier)->first();
if ($instance && $instance->status < 10) {
if (!$isDryRun) {
$instance->status = 10;
$instance->save();
}
$this->line("✅ Updated payment #{$payment->id}");
}
}
}
}
Features:
- ✅ Dry-Run Modus (
--dry-run) - ✅ Detaillierte Ausgabe mit Statistiken
- ✅ Fehlerbehandlung für fehlende Instances
- ✅ Progress-Anzeige
Übersetzungen
Spanische Übersetzungen korrigiert
Datei: resources/lang/es/payment.php
| Key | Alt | Neu | Grund |
|---|---|---|---|
link_paid |
"pagado" | "Pago exitoso" | Zu kurz/generisch, inkonsistent mit anderen Sprachen |
link_check |
"Pago en curso" | "Pago en revisión" | Falsche Bedeutung ("in progress" statt "in review") |
Alle Sprachen vollständig:
- ✅ Deutsch (DE) - alle 8 Status korrekt
- ✅ Englisch (EN) - alle 8 Status korrekt
- ✅ Spanisch (ES) - 2 Korrekturen vorgenommen
Status-Hierarchie
ShoppingInstance Status
public $statuses = [
0 => 'link_sent', // Link versendet
1 => 'link_openly', // Link geöffnet
2 => 'link_check', // In Prüfung
3 => 'link_pending', // In Bearbeitung
4 => 'link_appointed', // Angewiesen
5 => 'link_failed', // Fehlgeschlagen
6 => 'link_canceled', // Abgebrochen
10 => 'link_paid', // Bezahlt (FINAL)
];
Badge-Farben
'link_sent' => 'default', // Grau
'link_openly' => 'info', // Blau
'link_paid' => 'secondary', // Dunkelgrau
'link_check' => 'warning', // Gelb
'link_pending' => 'warning', // Gelb
'link_appointed' => 'warning',// Gelb
'link_failed' => 'danger', // Rot
'link_canceled' => 'danger' // Rot
Datenbank Cleanup Ergebnisse
Command Ausführung (28.01.2026)
php artisan payment:fix-link-status
Statistik:
- ✅ 2.117 Payment Links korrigiert (Status 4 → 10)
- ✅ 490 bereits korrekt (durch neue Logik)
- ⚠️ 12.754 ShoppingInstances nicht gefunden
- 📊 15.361 bezahlte Payments total
Warum fehlen 12.754 ShoppingInstances?
Grund: ShoppingInstances werden nach erfolgreicher Zahlung gelöscht (by design).
- Kunde bekommt Payment Link → ShoppingInstance wird erstellt
- Kunde zahlt → ShoppingOrder.paid = 1
- System löscht ShoppingInstance (nicht mehr benötigt)
- ShoppingPayment.identifier bleibt erhalten (historische Referenz)
Ist das ein Problem? Nein! Die Zahlung ist abgeschlossen, die temporäre Instance wird nicht mehr benötigt.
Beispiel-Output
🔎 Searching for payment links with incorrect status...
Found 15361 paid payments with identifiers
✅ Payment #41963: link_appointed (4) → link_paid (10) (Order #45027, Amount: 92,60 EUR)
✅ Payment #41967: link_appointed (4) → link_paid (10) (Order #45031, Amount: 115,90 EUR)
✅ Payment #41970: link_appointed (4) → link_paid (10) (Order #45034, Amount: 27,80 EUR)
⚠️ ShoppingInstance not found for identifier: c5cdac250...
⚠️ ShoppingInstance not found for identifier: 0ea47bde4...
📊 Summary:
+-----------------+-------+
| Status | Count |
+-----------------+-------+
| Fixed/Would fix | 2117 |
| Already correct | 490 |
| Errors | 12754 |
| Total processed | 15361 |
+-----------------+-------+
✨ Successfully updated 2117 payment link(s)!
Testing
Manuelle Tests durchgeführt
1. Status-Legende
- ✅ Legende wird oberhalb der Tabelle angezeigt
- ✅ Alle 8 Status werden korrekt dargestellt
- ✅ Farben entsprechen den Badges in der Tabelle
- ✅ Mehrsprachigkeit funktioniert (DE, EN, ES)
2. Race Condition Fix
- ✅ Payone Request: paid → appointed → txaction bleibt "paid" ✓
- ✅ Payone Request: appointed → paid → txaction wird "paid" ✓
- ✅ Logging erfolgt bei übersprungenen Updates
- ✅ ShoppingInstance Status bleibt auf 10 bei Race Condition
3. Status für alle Bestellungen
- ✅ Normale Bestellung bezahlt → Status 10 gesetzt
- ✅ Abo-Bestellung bezahlt → Status 10 gesetzt
- ✅ Beide Szenarien funktionieren korrekt
4. Artisan Command
- ✅ Dry-Run zeigt korrekte Anzahl zu korrigierender Einträge
- ✅ Echte Ausführung aktualisiert Datenbank korrekt
- ✅ Fehlerbehandlung für fehlende Instances funktioniert
- ✅ Statistik ist korrekt und übersichtlich
Vorteile der Lösung
Für Benutzer
- ✅ Klare Übersicht über alle möglichen Payment Status
- ✅ Korrekte Status-Anzeige (keine Race Conditions mehr)
- ✅ Mehrsprachige Unterstützung
- ✅ Besseres Verständnis des Zahlungsprozesses
Für Entwickler
- ✅ Zentrale Status-Definition (DRY Prinzip)
- ✅ Automatische Synchronisation zwischen Backend und Frontend
- ✅ Einfache Erweiterbarkeit für neue Status
- ✅ Klare Logging-Informationen bei Problemen
- ✅ Tool zur Datenbereinigung vorhanden
Für das System
- ✅ Konsistente Datenhaltung
- ✅ Keine verlorenen "paid" Status mehr
- ✅ Historische Daten bereinigt
- ✅ Robustere Payone-Integration
Zukünftige Verbesserungen (Optional)
1. Identifier Cleanup
Optional: ShoppingPayment.identifier auf NULL setzen, wenn ShoppingInstance nicht mehr existiert:
// In FixPaymentLinkStatus Command:
if (!$instance && $payment->identifier) {
$payment->identifier = null;
$payment->save();
}
2. Monitoring
Status-Änderungen in separater Log-Tabelle tracken:
PaymentStatusLog::create([
'shopping_payment_id' => $payment->id,
'old_status' => $oldStatus,
'new_status' => $newStatus,
'source' => 'payone',
'txaction' => $txaction,
]);
3. Webhooks
Benachrichtigungen bei unerwarteten Status-Änderungen:
- E-Mail an Admin bei "paid" → "appointed" Versuch
- Slack-Notification bei kritischen Fehlern
Zusammenfassung
Umfassende Verbesserung des Payment-Systems mit:
- 🎨 Benutzerfreundliche Status-Legende
- 🐛 Race Condition Bug behoben
- 🔧 Konsistente Status-Vergabe
- 🛠️ Model-Korrekturen
- 🧹 Datenbank-Cleanup Tool
- 🌍 Mehrsprachigkeit
- 📊 2.117 historische Einträge korrigiert
Status: ✅ Produktionsreif und deployed Datum: 28.01.2026