# 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: 1. Dynamische mehrsprachige Status-Legende 2. Race Condition Fix für Payone Requests 3. Status Update für alle Bestellungstypen 4. ShoppingInstance Model Korrektur 5. 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: 1. `appointed` - Zahlung autorisiert 2. `paid` - 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.txaction` und `shopping_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: ```php $shopping_order->txaction = $data['txaction']; // IMMER überschrieben! $shopping_payment->txaction = $data['txaction']; // IMMER überschrieben! ``` ### Lösung Prioritätsprüfung vor dem Update: ```php $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: ```php 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: ```php // 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_instances` hat `identifier` (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: ```php class ShoppingInstance extends Model { protected $primaryKey = 'identifier'; public $incrementing = false; protected $keyType = 'string'; // ... } ``` --- ## Implementierung ### 1. OrderPaymentService - Zentrale Status-Definition **Datei:** `app/Services/OrderPaymentService.php` ```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( ' %s', $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` ```blade {{-- Status-Legende --}}
{{ __('tables.status') }} - {{ __('legend') }}
@foreach(\App\Services\OrderPaymentService::getStatusBadgeClasses() as $statusKey => $badgeClass)
{{ __('payment.' . $statusKey) }}
@endforeach
``` **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` ```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:** ```php // 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:** ```php // 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` ```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` ```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 ```php 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 ```php '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) ```bash 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). 1. Kunde bekommt Payment Link → ShoppingInstance wird erstellt 2. Kunde zahlt → ShoppingOrder.paid = 1 3. System löscht ShoppingInstance (nicht mehr benötigt) 4. 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: ```php // In FixPaymentLinkStatus Command: if (!$instance && $payment->identifier) { $payment->identifier = null; $payment->save(); } ``` ### 2. Monitoring Status-Änderungen in separater Log-Tabelle tracken: ```php 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