mivita/dev/payment-dashboard/ENTWICKLUNGSPLAN.md
2026-04-14 18:07:45 +02:00

20 KiB
Raw Permalink Blame History

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_monitor in de, en, es navigation.php
  • Form Requests: StorePaymentIncidentRequest + AddIncidentActivityRequest in app/Http/Requests/PaymentIncident/
  • Cache-Invalidierung: Cache::forget('open_incident_count') in store(), updateStatus(), addActivity()

Bekannte Abweichungen vom Plan

  • Views: Nutzen layouts.layout-2 statt layout-1 (wie im Plan angegeben)
  • Entwurfsdatei dev/payment-dashboard/routes/payment-dashboard.php: Veraltet — falscher Namespace (App\Http\Controllers\PaymentDashboardController), falsche Methodennamen (developer statt index), nur auth-Middleware statt admin

Offene Punkte

  1. Phase 9 (Auto-Incident aus PayoneController): Noch nicht begonnen — kritische Fehler (z.B. Error:2008) könnten automatisch Incidents anlegen
  2. Entwurfsdatei aufräumen: dev/payment-dashboard/routes/payment-dashboard.php ggf. 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 20012008)
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 nur auth — 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/Incidents
  • incident_activities — Kommunikationsverlauf pro Incident (Notizen, Tickets, Anrufe)
  • provider_uptime_logs — automatische Uptime-Checks (Phase 6)

Anpassungen zum Entwurf:

  • provider-Enum zunächst auf payone fokussiert (andere Provider ergänzen, wenn aktiv genutzt)
  • notes-Feld ergänzen in payment_incidents für freie interne Kommentare
  • Index auf detected_at und provider fü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 Abfragen
  • getTypeIconAttribute() 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 aus payment_transactions
  • getFailedPayments() — alle Payments mit txaction = 'failed' der letzten 30 Tage
  • getErrorDistribution() — Häufigkeit der Fehlercodes 20012008 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_number eine 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_data JSON: 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 failed Transaktionen heute
  • Letzte failed Transaktion: 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_at wird 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 15)
            → 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

  1. Welcher Admin-User ist "Alois"? Hat er admin >= 2? Falls nicht → eigene schlanke Route ohne admin-Middleware, geschützt durch separaten Login oder feste User-ID-Prüfung. unter Superadmin!

  2. PAYONE Health-Endpoint: Welche URL soll der Uptime-Check prüfen? (Phase 6 — kann vorerst übersprungen werden)

  3. Mollie/Stripe/PayPal aktiv? Falls nein → Provider-Enum im ersten Schritt auf payone + other reduzieren, spätere Erweiterung bleibt möglich.

  4. Cache-Invalidierung: Cache::forget('open_incident_count') muss in store(), updateStatus() und dem Artisan Command aufgerufen werden.