14-04-2026

This commit is contained in:
Kevin Adametz 2026-04-14 18:07:45 +02:00
parent f58c709945
commit 0f82fea88a
72 changed files with 7414 additions and 148 deletions

View file

@ -0,0 +1,503 @@
# 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.