20 KiB
Payment Dashboard – Entwicklungsplan
Ziel: Integration eines Payment-Monitoring-Dashboards in mivita.care, das PAYONE-Probleme
frühzeitig sichtbar macht. Das Dashboard kombiniert manuell erfasste Incidents mit echten Daten
aus den bestehenden Tabellen (payment_transactions, shopping_payments) und den vorhandenen
PAYONE-Logs.
Implementierungsstand (Stand: 13.04.2026)
| Phase | Thema | Status | Anmerkungen |
|---|---|---|---|
| 1 | Datenbank & Models | ✅ Umgesetzt | Migration, alle drei Models vorhanden |
| 2 | Controller & Routes | ✅ Umgesetzt | Routes in routes/domains/crm.php, Entwurfsdatei unter dev/ ist veraltet |
| 3 | Views (Bootstrap/Appwork) | ✅ Umgesetzt | Index, Management, Show, Transactions, Logs + 4 Partials |
| 4 | Live-Transaktionsdaten | ✅ Umgesetzt | transactions() + View mit Filtern, Statistiken, Pagination |
| 5 | Log-Viewer | ✅ Umgesetzt | ?date=-Parameter wird ausgewertet; sichere Validierung der Datumseingabe |
| 6 | Uptime-Check Artisan Command | ✅ Umgesetzt | Command payment:check-uptime, Schedule alle 5 Minuten, Uptime-Karten im Dashboard |
| 7 | E-Mail-Benachrichtigung | ✅ Umgesetzt | Dediziertes Mailable PaymentIncidentAlert (Queue), sendet bei critical-Incidents |
| 8 | Tests | ✅ Umgesetzt | PaymentDashboardAccessTest, PaymentIncidentCrudTest, CheckPaymentUptimeCommandTest |
Zusätzlich umgesetzt (nicht in Phasen geplant)
- Navigation: Sidenav-Link mit gecachtem Badge für offene Incidents in
layout-sidenav.blade.php - Übersetzungen:
payment_monitorinde,en,esnavigation.php - Form Requests:
StorePaymentIncidentRequest+AddIncidentActivityRequestinapp/Http/Requests/PaymentIncident/ - Cache-Invalidierung:
Cache::forget('open_incident_count')instore(),updateStatus(),addActivity()
Bekannte Abweichungen vom Plan
- Views: Nutzen
layouts.layout-2stattlayout-1(wie im Plan angegeben) - Entwurfsdatei
dev/payment-dashboard/routes/payment-dashboard.php: Veraltet — falscher Namespace (App\Http\Controllers\PaymentDashboardController), falsche Methodennamen (developerstattindex), nurauth-Middleware stattadmin
Offene Punkte
- Phase 9 (Auto-Incident aus
PayoneController): Noch nicht begonnen — kritische Fehler (z.B. Error:2008) könnten automatisch Incidents anlegen - Entwurfsdatei aufräumen:
dev/payment-dashboard/routes/payment-dashboard.phpggf. löschen oder als Archiv kennzeichnen
Analyse des Ist-Zustands
Bereits vorhandene Datenquellen
| Quelle | Relevanz |
|---|---|
payment_transactions |
Jeder PAYONE-Callback: txaction, errorcode, errormessage, mode, transmitted_data |
shopping_payments |
Jede Zahlung: reference, txaction, clearingtype, amount |
shopping_orders |
Bestellstatus, paid-Flag, Verknüpfung zu Payments |
storage/logs/payone.log |
Fehler-Details aus MyLog::writeLog('payone', ...) (Error 2001–2008) |
storage/logs/payment.log |
Allgemeine Zahlungsfehler |
Bestehende PAYONE-Fehlercodes (aus Api/PayoneController)
| Code | Bedeutung |
|---|---|
| 2001 | Callback: Parameter unvollständig |
| 2002 | Callback: Key-Validierung fehlgeschlagen |
| 2003 | Callback: ShoppingOrder nicht gefunden |
| 2004 | Callback: ShoppingPayment nicht gefunden |
| 2005 | Callback: Payment ↔ Order Zuordnung falsch |
| 2006 | Callback: Preisabweichung |
| 2008 | Callback: Datenbank-Transaktion fehlgeschlagen |
Wichtige Unterschiede zum Entwurf
- Layout: Das Dashboard nutzt das bestehende Appwork/Bootstrap-Layout (
layout-1.blade.php), nicht das Custom-Dark-Mode-Layout aus dem Entwurf. - Auth: Middleware
admin(admin >= 2) statt nurauth— kein separates Rollen-System nötig. - Echte Daten: Statt rein manuelle Incidents, werden bestehende Transaktionsdaten direkt eingebunden (Live-Tab). Manuelle Incidents bleiben als Eskalationswerkzeug erhalten.
- Logs: Der
payone-Log-Kanal wird direkt im Dashboard lesbar gemacht.
Phasen-Übersicht
| Phase | Titel | Aufwand | Priorität |
|---|---|---|---|
| 1 | Datenbank & Models | ~1h | Muss |
| 2 | Controller & Routes | ~2h | Muss |
| 3 | Views (Bootstrap/Appwork) | ~3h | Muss |
| 4 | Live-Daten: Transaktionen | ~2h | Hoch |
| 5 | Log-Viewer | ~1h | Hoch |
| 6 | Uptime-Check Artisan Command | ~2h | Mittel |
| 7 | E-Mail-Benachrichtigung | ~1h | Mittel |
| 8 | Tests | ~2h | Muss |
Phase 1: Datenbank & Models ✅
Migration (aus dem Entwurf übernehmen, leicht angepasst)
Datei: database/migrations/YYYY_MM_DD_000001_create_payment_incidents_table.php
Drei neue Tabellen:
payment_incidents— manuell erfasste Störungen/Incidentsincident_activities— Kommunikationsverlauf pro Incident (Notizen, Tickets, Anrufe)provider_uptime_logs— automatische Uptime-Checks (Phase 6)
Anpassungen zum Entwurf:
provider-Enum zunächst aufpayonefokussiert (andere Provider ergänzen, wenn aktiv genutzt)notes-Feld ergänzen inpayment_incidentsfür freie interne Kommentare- Index auf
detected_atundproviderfür Performance bei größeren Datenmengen
Models
app/Models/PaymentIncident.php — aus dem Entwurf übernehmen, ergänzen um:
scopeOpen()/scopePayone()/scopeLastDays(int $days)für häufige AbfragengetTypeIconAttribute()für die View-Darstellung
app/Models/IncidentActivity.php — direkt aus dem Entwurf übernehmen
app/Models/ProviderUptimeLog.php — neu erstellen für Phase 6
Phase 2: Controller & Routes ✅
Controller-Struktur
app/Http/Controllers/Admin/PaymentDashboardController.php
Platzierung unter Admin/, konsistent mit PaymentSalesController.php.
Actions
| Method | URL | Beschreibung |
|---|---|---|
index |
GET /admin/payment-dashboard |
Entwickler-Ansicht: alle Incidents + Stats |
management |
GET /admin/payment-dashboard/management |
GF-Ansicht: Ampel-Karten, kein Bearbeiten |
show |
GET /admin/payment-dashboard/{incident} |
Incident-Detail mit Timeline |
store |
POST /admin/payment-dashboard |
Neuen Incident anlegen |
addActivity |
POST /admin/payment-dashboard/{incident}/activity |
Aktivität hinzufügen |
updateStatus |
PATCH /admin/payment-dashboard/{incident}/status |
Status ändern |
transactions |
GET /admin/payment-dashboard/transactions |
Live-PAYONE-Transaktionen (Phase 4) |
logs |
GET /admin/payment-dashboard/logs |
PAYONE-Log-Viewer (Phase 5) |
Stats-Methoden im Controller
Zusätzlich zu den Stats aus dem Entwurf:
getPayoneTransactionStats()— Fehlerquote der letzten 7/30 Tage auspayment_transactionsgetFailedPayments()— alle Payments mittxaction = 'failed'der letzten 30 TagegetErrorDistribution()— Häufigkeit der Fehlercodes 2001–2008 aus den Logs
Routes
In routes/web.php ergänzen (nicht als separates File, analog zu bestehender Struktur):
Route::prefix('admin/payment-dashboard')
->name('admin.payment-dashboard.')
->middleware(['auth', 'admin'])
->group(function () {
Route::get('/', [PaymentDashboardController::class, 'index'])->name('index');
Route::get('/management', [PaymentDashboardController::class, 'management'])->name('management');
Route::get('/transactions', [PaymentDashboardController::class, 'transactions'])->name('transactions');
Route::get('/logs', [PaymentDashboardController::class, 'logs'])->name('logs');
Route::get('/{incident}', [PaymentDashboardController::class, 'show'])->name('show');
Route::post('/', [PaymentDashboardController::class, 'store'])->name('store');
Route::post('/{incident}/activity', [PaymentDashboardController::class, 'addActivity'])->name('activity.store');
Route::patch('/{incident}/status', [PaymentDashboardController::class, 'updateStatus'])->name('status.update');
});
Phase 3: Views (Bootstrap/Appwork) ✅
Layout-Anpassung
Kein Custom-Dark-Mode-Layout. Stattdessen: @extends('layouts.layout-1'), analog zu
anderen Admin-Views (z.B. resources/views/admin/payment/salesvolume.blade.php).
View-Struktur
resources/views/admin/payment-dashboard/
├── index.blade.php # Entwickler-Ansicht
├── management.blade.php # GF-Ansicht
├── show.blade.php # Incident-Detail
├── transactions.blade.php # Live-Transaktionen (Phase 4)
├── logs.blade.php # Log-Viewer (Phase 5)
└── _partials/
├── stats-cards.blade.php
├── incident-table.blade.php
├── activity-timeline.blade.php
└── create-incident-modal.blade.php
UI-Komponenten
Entwickler-Ansicht (index.blade.php):
- Stat-Karten oben: Offene Incidents | In Bearbeitung | PAYONE-Fehler (30 Tage) | Betroffener Umsatz
- Tabs: "Incidents" | "Live-Transaktionen" | "PAYONE Logs"
- Tabelle: Alle Incidents mit Status-Badge, Schwere-Farbe, Dauer, Quick-Status-Update
- Floating Button: "Neuen Incident anlegen" → Bootstrap-Modal
GF-Ansicht (management.blade.php):
- Ampel-Karten: Grün/Gelb/Rot je nach offenen Incidents und Schwere
- Sehr einfach, keine Bearbeitungsfunktionen
- Tagesaktuelle Zusammenfassung
Incident-Detail (show.blade.php):
- Kopfbereich: Titel, Provider-Badge, Status-Badge, Severity-Indikator
- Timeline: Chronologische Aktivitätsliste mit Icon je Typ
- Formulare: Aktivität hinzufügen, Status ändern
- Link zur betroffenen Bestellung (falls
ticket_numbereine Bestell-ID ist)
Phase 4: Live-Transaktionsdaten ✅
Das wertvollste Feature: Echtzeit-Einblick in PAYONE-Transaktionen aus bestehenden Tabellen.
Tab "Live-Transaktionen" in der Entwickler-Ansicht
Datenquelle: payment_transactions JOIN shopping_payments JOIN shopping_orders
Anzeigen:
- Alle Transaktionen der letzten 7 Tage, gefiltert nach
txaction - Fehlgeschlagene Transaktionen (
txaction = 'failed') hervorgehoben in Rot - Fehlercodes und Fehlermeldungen aus
errorcode/errormessage/customermessage mode-Feld: unterscheidet Test vs. Live-Modus (wichtig für Debugging)transmitted_dataJSON: aufklappbar für Detailinspektion
Filter-Optionen:
- Nach
txaction:appointed,pending,paid,failed - Nach Zeitraum: Heute / Letzte 7 Tage / Letzte 30 Tage
- Nach Modus: Test / Live
Stat-Block oben:
- Erfolgsrate (paid / gesamt) der letzten 24h
- Anzahl
failedTransaktionen heute - Letzte
failedTransaktion: vor X Minuten - Verteilung der
clearingtype(wlt, cc, elv…)
Neue Methode im Controller
private function getTransactionStats(int $days = 7): array
{
$since = now()->subDays($days);
return [
'total' => PaymentTransaction::where('created_at', '>=', $since)->count(),
'failed' => PaymentTransaction::where('txaction', 'failed')->where('created_at', '>=', $since)->count(),
'paid' => PaymentTransaction::where('txaction', 'paid')->where('created_at', '>=', $since)->count(),
'errors' => PaymentTransaction::whereNotNull('errorcode')->where('created_at', '>=', $since)
->select('errorcode', 'errormessage', DB::raw('count(*) as count'))
->groupBy('errorcode', 'errormessage')
->orderByDesc('count')
->get(),
'last_failed' => PaymentTransaction::where('txaction', 'failed')->latest()->first(),
];
}
Phase 5: Log-Viewer ✅
Tab "PAYONE Logs" in der Entwickler-Ansicht
Liest direkt aus storage/logs/payone.log (aktuellste Datei bei daily rotation).
Anzeigen:
- Letzte 100 Log-Einträge (konfigurierbar)
- Farbliche Markierung nach Level:
error(rot),warning(gelb),info(blau),notice(grau) - Suche/Filter nach Fehlercode (z.B. "Error:2003")
- Zeitstempel, Log-Level, Nachricht, JSON-Payload (aufklappbar)
Implementierung:
public function logs(): View
{
$logPath = storage_path('logs/payone-' . now()->format('Y-m-d') . '.log');
$entries = [];
if (file_exists($logPath)) {
$lines = array_reverse(file($logPath));
foreach (array_slice($lines, 0, 200) as $line) {
if (preg_match('/\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[^\]]*)\] \w+\.(\w+): (.+)/', $line, $m)) {
$entries[] = [
'timestamp' => $m[1],
'level' => $m[2],
'message' => $m[3],
];
}
}
}
return view('admin.payment-dashboard.logs', compact('entries'));
}
Phase 6: Artisan Command – Uptime-Check ✅
php artisan payment:check-uptime
Datei: app/Console/Commands/CheckPaymentUptime.php
Prüft erreichbare PAYONE-Endpunkte (Status-API oder bekannte öffentliche URLs) und legt bei Ausfall automatisch einen Incident an.
// Prüft: PAYONE Server-Status-Seite oder einen konfigurierbaren Health-Endpoint
// Speichert Ergebnis in provider_uptime_logs
// Bei Ausfall: erstellt PaymentIncident mit severity = 'critical', type = 'outage'
// Bei Wiederherstellung: setzt offene Outage-Incidents auf 'resolved'
Scheduling in app/Console/Kernel.php:
$schedule->command('payment:check-uptime')->everyFiveMinutes();
Konfiguration in .env / config/services.php:
PAYONE_HEALTH_CHECK_URL=https://api.pay1.de/post-gateway/
PAYONE_HEALTH_CHECK_ENABLED=true
Phase 7: E-Mail-Benachrichtigung ✅
Bei neu eröffnetem Critical-Incident: automatische Mail via bestehenden MyLog-Mechanismus.
// In PaymentDashboardController::store()
if ($validated['severity'] === 'critical') {
MyLog::writeLog(
'payment',
'error',
'Kritischer Zahlungs-Incident eröffnet: ' . $validated['title'],
$validated,
true // sendet Mail an config('app.exception_mail')
);
}
Alternativ: Dedizierte Mailable App\Mail\PaymentIncidentAlert für bessere Darstellung.
Phase 8: Tests ✅ (vollständig)
Feature-Tests:
tests/Feature/PaymentDashboard/
├── PaymentDashboardAccessTest.php # Auth, Admin-Middleware
├── PaymentIncidentCrudTest.php # Create, Status-Update, Aktivität
├── PaymentDashboardStatsTest.php # Korrekte Stats aus Testdaten
└── CheckPaymentUptimeCommandTest.php # Artisan Command (Phase 6)
Testszenarien:
- Nicht eingeloggter User → Redirect
- Eingeloggter User ohne Admin (admin < 2) → 403
- Admin (admin >= 2) → Zugriff auf Entwickler-Ansicht
- Incident anlegen: Pflichtfelder, korrekte Aktivität wird auto-erstellt
- Status auf "resolved" setzen →
resolved_atwird gesetzt - Stats-Methoden liefern korrekte Werte mit Testdaten
Implementierungsreihenfolge (empfohlen)
Phase 1 (Migration + Models)
→ Phase 2 (Routes + Controller-Grundstruktur)
→ Phase 3 (Views mit Bootstrap, ohne Echtdaten)
→ Phase 4 (Live-Transaktionsdaten einbauen)
→ Phase 5 (Log-Viewer)
→ Phase 8 (Tests für Phasen 1–5)
→ Phase 6 (Uptime-Check, optional)
→ Phase 7 (Benachrichtigung, optional)
Datenbankzugriff auf bestehende Daten – kein Code-Eingriff nötig
Phase 4 liest nur lesend aus bestehenden Tabellen. Es sind keine Änderungen an:
Api/PayoneController(PAYONE-Callback-Handler)ShoppingPayment,PaymentTransaction(Models)- bestehenden Migrations
Navigation (layout-sidenav.blade.php)
Datei: resources/views/layouts/includes/layout-sidenav.blade.php
Der Eintrag kommt als letztes Item im bestehenden admin/payments-Untermenü (nach "Steuerberater",
aktuell Zeile ~372). Kein eigener Top-Level-Eintrag — das Dashboard gehört thematisch zu Payments.
<li class="sidenav-item{{ Request::is('admin/payment-dashboard*') ? ' active' : '' }}">
<a href="{{ route('admin.payment-dashboard.index') }}" class="sidenav-link">
<i class="sidenav-icon ion ion-md-alert"></i>
<div>{{ __('navigation.payment_monitor') }}</div>
@php $openIncidentCount = Cache::remember('open_incident_count', 60, fn() =>
\App\Models\PaymentIncident::whereIn('status', ['open','in_progress','waiting_provider'])->count()
); @endphp
@if ($openIncidentCount > 0)
<div class="pl-1 ml-auto">
<div class="badge badge-danger">{{ $openIncidentCount }}</div>
</div>
@endif
</a>
</li>
Der Badge ist gecacht (60 Sekunden) um N+1-Queries bei jedem Seitenaufruf zu vermeiden.
Der Cache wird im Controller bei Incident-Änderungen mit Cache::forget('open_incident_count') invalidiert.
Übersetzungsschlüssel
In alle drei Sprachdateien ergänzen:
resources/lang/de/navigation.php
'payment_monitor' => 'Payment Monitor',
resources/lang/en/navigation.php
'payment_monitor' => 'Payment Monitor',
resources/lang/es/navigation.php
'payment_monitor' => 'Monitor de Pagos',
Form Request Klassen
Laut Projektkonventionen: keine Inline-Validierung im Controller.
app/Http/Requests/PaymentIncident/
├── StorePaymentIncidentRequest.php # Validierung für store()
└── AddIncidentActivityRequest.php # Validierung für addActivity()
Auto-Incident aus PayoneController (Phase 9, optional)
Der wertvollste Ausbauschritt: Kritische Fehler im PAYONE-Callback-Handler legen automatisch einen Incident an, ohne manuelle Erfassung.
Eingriff in app/Http/Controllers/Api/PayoneController.php:
// Bei Error:2008 (DB-Rollback) → sofort Critical-Incident
} catch (\Exception $e) {
\DB::rollBack();
MyLog::writeLog('payone', 'error', 'Error:2008 ...', [...]);
// NEU: Automatischer Critical-Incident
PaymentIncident::firstOrCreate(
['type' => 'payment_failure', 'status' => 'open', 'provider' => 'payone',
'detected_at' => now()->startOfHour()], // deduplication per Stunde
['title' => 'Automatisch: DB-Fehler bei PAYONE-Callback (Error:2008)',
'severity' => 'critical', 'ticket_number' => $data['txid'] ?? null]
);
}
firstOrCreate mit Stunden-Deduplication verhindert Duplikate bei mehrfachen Fehlern.
Offene Fragen vor Start
-
Welcher Admin-User ist "Alois"? Hat er
admin >= 2? Falls nicht → eigene schlanke Route ohneadmin-Middleware, geschützt durch separaten Login oder feste User-ID-Prüfung. unter Superadmin! -
PAYONE Health-Endpoint: Welche URL soll der Uptime-Check prüfen? (Phase 6 — kann vorerst übersprungen werden)
-
Mollie/Stripe/PayPal aktiv? Falls nein → Provider-Enum im ersten Schritt auf
payone+otherreduzieren, spätere Erweiterung bleibt möglich. -
Cache-Invalidierung:
Cache::forget('open_incident_count')muss instore(),updateStatus()und dem Artisan Command aufgerufen werden.