# 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 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 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 2001–2008 aus den Logs

### Routes

In `routes/web.php` ergänzen (nicht als separates File, analog zu bestehender Struktur):

```php
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

```php
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:**

```php
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.

```php
// 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`:**

```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.

```php
// 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 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.

```blade
<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`**
```php
'payment_monitor' => 'Payment Monitor',
```

**`resources/lang/en/navigation.php`**
```php
'payment_monitor' => 'Payment Monitor',
```

**`resources/lang/es/navigation.php`**
```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`:**

```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.
