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

503 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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):
```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 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.
```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.