From 8e09aaefe86d64978bfa8956f2301ce9a83655be Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 10 Jul 2026 16:57:50 +0000 Subject: [PATCH] =?UTF-8?q?Stripe-Integration=20(S7.4):=20Payment-Dashboar?= =?UTF-8?q?d=20f=C3=BCr=20Stripe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Provider-Status/Uptime: Stripe-Kachel aktiviert; payment:check-uptime prüft zusätzlich api.stripe.com/v1/ (401 ohne Key = erreichbar) inkl. Auto-Incident - Zahlungen: Anbieter-Filter, Provider-Badges, Umsatz je Provider, Stripe-Dashboard-Deep-Links (ShoppingPayment/PaymentTransaction:: stripeDashboardUrl, pi_-Guard, testmodus-bewusst) - Rohe Transaktionen: Anbieter-Filter/-Spalte, Erfolgsraten je Anbieter; Bugfix: Stringable-Vergleich ließ den txaction-Filter immer mit leerem Wert greifen — Liste war dauerhaft leer - Neue Kachel „Abos je Anbieter" (Index + GF-Ansicht) mit Fehlerzählern und Stripe-Anteil-Fortschrittsbalken (Auslaufmodell PAYONE → Stripe ablesbar) - Incident-Kachel weist Stripe/PAYONE getrennt aus; Log-Viewer mit Kanal-Umschalter stripe/payone - Plan-Doku: S7.4-Umsetzungsstatus, Abo-Go-Live 2026-07-10 vermerkt Tests: PaymentDashboardProviderTest (9 neu), Dashboard-Suite 49 grün. Co-Authored-By: Claude Fable 5 --- app/Console/Commands/CheckPaymentUptime.php | 6 +- .../Admin/PaymentDashboardController.php | 145 ++++++++++-- app/Models/PaymentTransaction.php | 15 ++ app/Models/ShoppingPayment.php | 15 ++ .../ENTWICKLUNGSPLAN-STRIPE.md | 60 +++-- .../_partials/abo-provider-stats.blade.php | 45 ++++ .../_partials/stats-cards.blade.php | 7 +- .../admin/payment-dashboard/index.blade.php | 5 +- .../admin/payment-dashboard/logs.blade.php | 19 +- .../payment-dashboard/management.blade.php | 8 + .../payment-dashboard/payments.blade.php | 49 +++- .../payment-dashboard/transactions.blade.php | 51 +++- .../PaymentDashboardProviderTest.php | 220 ++++++++++++++++++ 13 files changed, 600 insertions(+), 45 deletions(-) create mode 100644 resources/views/admin/payment-dashboard/_partials/abo-provider-stats.blade.php create mode 100644 tests/Feature/PaymentDashboard/PaymentDashboardProviderTest.php diff --git a/app/Console/Commands/CheckPaymentUptime.php b/app/Console/Commands/CheckPaymentUptime.php index 4cb1bd9b..567defab 100644 --- a/app/Console/Commands/CheckPaymentUptime.php +++ b/app/Console/Commands/CheckPaymentUptime.php @@ -13,12 +13,16 @@ class CheckPaymentUptime extends Command { protected $signature = 'payment:check-uptime'; - protected $description = 'Prüft die Erreichbarkeit von PAYONE und legt bei Ausfall automatisch einen Incident an.'; + protected $description = 'Prüft die Erreichbarkeit von Stripe und PAYONE und legt bei Ausfall automatisch einen Incident an.'; /** + * Ein HTTP-Status < 500 gilt als erreichbar — bei Stripe antwortet /v1/ ohne + * API-Key mit 401, was Erreichbarkeit belegt, ohne einen Key zu benötigen. + * * @var array */ private array $endpoints = [ + 'stripe' => 'https://api.stripe.com/v1/', 'payone' => 'https://api.pay1.de/post-gateway/', ]; diff --git a/app/Http/Controllers/Admin/PaymentDashboardController.php b/app/Http/Controllers/Admin/PaymentDashboardController.php index 5211ecaf..18e1fddf 100644 --- a/app/Http/Controllers/Admin/PaymentDashboardController.php +++ b/app/Http/Controllers/Admin/PaymentDashboardController.php @@ -14,7 +14,9 @@ use App\Models\PaymentTransaction; use App\Models\ProviderUptimeLog; use App\Models\ShoppingOrder; use App\Models\ShoppingPayment; +use App\Models\UserAbo; use App\Services\MyLog; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; @@ -39,6 +41,7 @@ class PaymentDashboardController extends Controller $allIncidents = PaymentIncident::with('activities')->orderBy('detected_at', 'desc')->paginate(20); $providerStats = $this->getProviderStats(); $uptimeStats = $this->getUptimeStats(); + $aboProviderStats = $this->getAboProviderStats(); return view('admin.payment-dashboard.index', compact( 'stats', @@ -47,6 +50,7 @@ class PaymentDashboardController extends Controller 'allIncidents', 'providerStats', 'uptimeStats', + 'aboProviderStats', )); } @@ -61,6 +65,7 @@ class PaymentDashboardController extends Controller $openIncidents = PaymentIncident::open()->orderBy('detected_at', 'desc')->get(); $recentIncidents = PaymentIncident::orderBy('detected_at', 'desc')->take(10)->get(); $providerStats = $this->getProviderStats(); + $aboProviderStats = $this->getAboProviderStats(); return view('admin.payment-dashboard.management', compact( 'stats', @@ -68,6 +73,7 @@ class PaymentDashboardController extends Controller 'openIncidents', 'recentIncidents', 'providerStats', + 'aboProviderStats', )); } @@ -165,7 +171,8 @@ class PaymentDashboardController extends Controller public function transactions(): View { $days = request()->integer('days', 7); - $txactionFilter = request()->string('txaction', ''); + $txactionFilter = request()->string('txaction', '')->toString(); + $providerFilter = request()->string('provider', '')->toString(); $query = PaymentTransaction::with('shopping_payment') ->orderBy('created_at', 'desc'); @@ -174,14 +181,16 @@ class PaymentDashboardController extends Controller $query->where('created_at', '>=', now()->subDays($days)); } - if ($txactionFilter && $txactionFilter !== '') { + if ($txactionFilter !== '') { $query->where('txaction', $txactionFilter); } + $this->applyProviderFilter($query, 'provider', $providerFilter); + $transactions = $query->paginate(30)->withQueryString(); $transactionStats = $this->getTransactionStats($days > 0 ? $days : 30); - return view('admin.payment-dashboard.transactions', compact('transactions', 'transactionStats', 'days', 'txactionFilter')); + return view('admin.payment-dashboard.transactions', compact('transactions', 'transactionStats', 'days', 'txactionFilter', 'providerFilter')); } /** @@ -361,6 +370,7 @@ class PaymentDashboardController extends Controller $days = request()->integer('days', 7); $statusFilter = request()->string('txaction', '')->toString(); $modeFilter = request()->string('mode', '')->toString(); + $providerFilter = request()->string('provider', '')->toString(); $query = ShoppingPayment::with([ 'payment_transactions' => fn ($q) => $q->orderBy('created_at'), @@ -380,26 +390,34 @@ class PaymentDashboardController extends Controller $query->where('mode', $modeFilter); } + $this->applyProviderFilter($query, 'payment_provider', $providerFilter); + $payments = $query->paginate(25)->withQueryString(); $paymentStats = $this->getPaymentStats($days > 0 ? $days : 30); - return view('admin.payment-dashboard.payments', compact('payments', 'paymentStats', 'days', 'statusFilter', 'modeFilter')); + return view('admin.payment-dashboard.payments', compact('payments', 'paymentStats', 'days', 'statusFilter', 'modeFilter', 'providerFilter')); } /** - * PAYONE Log-Viewer. + * Log-Viewer für die Provider-Kanäle (payone-/stripe-Tageslogs). */ public function logs(): View { - $availableDates = $this->getAvailableLogDates(); + $availableChannels = ['stripe', 'payone']; + $selectedChannel = request()->string('channel', 'stripe')->toString(); + if (! in_array($selectedChannel, $availableChannels, true)) { + $selectedChannel = 'stripe'; + } + + $availableDates = $this->getAvailableLogDates($selectedChannel); $selectedDate = request()->string('date', now()->format('Y-m-d'))->toString(); if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $selectedDate) || ! in_array($selectedDate, $availableDates)) { $selectedDate = ! empty($availableDates) ? $availableDates[0] : now()->format('Y-m-d'); } - $logFile = storage_path('logs/payone-'.$selectedDate.'.log'); + $logFile = storage_path('logs/'.$selectedChannel.'-'.$selectedDate.'.log'); $entries = []; if (file_exists($logFile)) { @@ -415,7 +433,7 @@ class PaymentDashboardController extends Controller } } - return view('admin.payment-dashboard.logs', compact('entries', 'availableDates', 'selectedDate')); + return view('admin.payment-dashboard.logs', compact('entries', 'availableDates', 'selectedDate', 'availableChannels', 'selectedChannel')); } // ─── Private Helpers ────────────────────────────────────────────────────── @@ -429,6 +447,7 @@ class PaymentDashboardController extends Controller ->whereMonth('resolved_at', now()->month)->count(), 'total_affected_revenue' => PaymentIncident::open()->sum('affected_revenue'), 'payone_incidents_30d' => PaymentIncident::payone()->lastDays(30)->count(), + 'stripe_incidents_30d' => PaymentIncident::where('provider', 'stripe')->lastDays(30)->count(), 'critical_open' => PaymentIncident::open()->where('severity', 'critical')->count(), ]; } @@ -451,6 +470,27 @@ class PaymentDashboardController extends Controller $lastFailed = PaymentTransaction::where('txaction', 'failed')->latest()->first(); + // Erfolgs-/Fehlerraten je Provider (Alt-PAYONE-Zeilen ohne Provider zählen als payone) + $providerRows = PaymentTransaction::where('created_at', '>=', $since) + ->selectRaw("COALESCE(provider, 'payone') as provider_key") + ->selectRaw('COUNT(*) as total') + ->selectRaw("SUM(CASE WHEN txaction = 'paid' THEN 1 ELSE 0 END) as paid") + ->selectRaw("SUM(CASE WHEN txaction = 'failed' THEN 1 ELSE 0 END) as failed") + ->groupBy('provider_key') + ->get(); + + $providers = []; + foreach ($providerRows as $row) { + $providers[$row->provider_key] = [ + 'label' => strtoupper($row->provider_key), + 'total' => (int) $row->total, + 'paid' => (int) $row->paid, + 'failed' => (int) $row->failed, + 'success_rate' => $row->total > 0 ? round(((int) $row->paid / $row->total) * 100, 1) : 0, + ]; + } + ksort($providers); + return [ 'total' => $total, 'paid' => $paid, @@ -458,6 +498,7 @@ class PaymentDashboardController extends Controller 'success_rate' => $successRate, 'error_distribution' => $errorDistribution, 'last_failed' => $lastFailed, + 'providers' => $providers, 'days' => $days, ]; } @@ -465,10 +506,9 @@ class PaymentDashboardController extends Controller private function getProviderStats(): array { $providers = [ + 'stripe', 'payone', - // 'stripe', // aktuell nicht aktiv 'paypal', - // 'mollie', // aktuell nicht aktiv 'other', ]; $stats = []; @@ -496,6 +536,26 @@ class PaymentDashboardController extends Controller $base = ShoppingPayment::where('created_at', '>=', $since); + // Umsatz (bezahlt) und Volumen je Provider (Alt-Zeilen ohne Provider = payone) + $providerRows = (clone $base) + ->selectRaw("COALESCE(payment_provider, 'payone') as provider_key") + ->selectRaw('COUNT(*) as total') + ->selectRaw("SUM(CASE WHEN txaction = 'paid' THEN 1 ELSE 0 END) as paid") + ->selectRaw("SUM(CASE WHEN txaction = 'paid' THEN amount ELSE 0 END) as paid_amount") + ->groupBy('provider_key') + ->get(); + + $providers = []; + foreach ($providerRows as $row) { + $providers[$row->provider_key] = [ + 'label' => strtoupper($row->provider_key), + 'total' => (int) $row->total, + 'paid' => (int) $row->paid, + 'paid_amount' => round((int) $row->paid_amount / 100, 2), + ]; + } + ksort($providers); + return [ 'total' => (clone $base)->count(), 'paid' => (clone $base)->where('txaction', 'paid')->count(), @@ -506,18 +566,74 @@ class PaymentDashboardController extends Controller (clone $base)->whereHas('payment_transactions', fn ($q) => $q->where('txaction', 'failed'))->sum('amount') / 100, 2 ), + 'providers' => $providers, 'days' => $days, ]; } - private function getAvailableLogDates(): array + /** + * Provider-Filter für Listen: Alt-PAYONE-Zeilen haben teils NULL in der + * Provider-Spalte und müssen beim payone-Filter mitkommen. + * + * @param Builder|Builder $query + */ + private function applyProviderFilter($query, string $column, string $provider): void + { + if ($provider === '') { + return; + } + + if ($provider === 'payone') { + $query->where(fn ($q) => $q->where($column, 'payone')->orWhereNull($column)); + } else { + $query->where($column, $provider); + } + } + + /** + * Aktive Abos je Provider — macht den Auslauf-Fortschritt PAYONE → Stripe ablesbar. + * + * @return array{providers: array, total: int, stripe_share: float|null} + */ + private function getAboProviderStats(): array + { + $rows = UserAbo::query() + ->activeAbo() + ->selectRaw("COALESCE(payment_provider, 'payone') as provider_key") + ->selectRaw('COUNT(*) as total') + ->selectRaw('SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) as failed') + ->groupBy('provider_key') + ->get(); + + $providers = []; + $total = 0; + foreach ($rows as $row) { + $providers[$row->provider_key] = [ + 'label' => strtoupper($row->provider_key), + 'total' => (int) $row->total, + 'failed' => (int) $row->failed, + ]; + $total += (int) $row->total; + } + ksort($providers); + + $stripeCount = $providers['stripe']['total'] ?? 0; + + return [ + 'providers' => $providers, + 'total' => $total, + 'stripe_share' => $total > 0 ? round($stripeCount / $total * 100, 1) : null, + ]; + } + + private function getAvailableLogDates(string $channel): array { $logPath = storage_path('logs'); - $files = glob($logPath.'/payone-*.log'); + $files = glob($logPath.'/'.$channel.'-*.log'); $dates = []; foreach ($files as $file) { - if (preg_match('/payone-(\d{4}-\d{2}-\d{2})\.log$/', $file, $m)) { + if (preg_match('/'.$channel.'-(\d{4}-\d{2}-\d{2})\.log$/', $file, $m)) { $dates[] = $m[1]; } } @@ -533,10 +649,9 @@ class PaymentDashboardController extends Controller private function getUptimeStats(): array { $providers = [ + 'stripe', 'payone', - // 'stripe', // aktuell nicht aktiv 'paypal', - // 'mollie', // aktuell nicht aktiv ]; $stats = []; $since = now()->subHours(24); diff --git a/app/Models/PaymentTransaction.php b/app/Models/PaymentTransaction.php index 1f2874eb..21a4c092 100644 --- a/app/Models/PaymentTransaction.php +++ b/app/Models/PaymentTransaction.php @@ -77,6 +77,21 @@ class PaymentTransaction extends Model return $this->belongsTo('App\Models\ShoppingPayment', 'shopping_payment_id'); } + /** + * Deep-Link ins Stripe-Dashboard für Stripe-Transaktionen + * (txid trägt dort die PaymentIntent-ID, siehe StripeTransactionRecorder). + */ + public function stripeDashboardUrl(): ?string + { + if ($this->provider !== 'stripe' || ! str_starts_with((string) $this->txid, 'pi_')) { + return null; + } + + $base = $this->mode === 'test' ? 'https://dashboard.stripe.com/test' : 'https://dashboard.stripe.com'; + + return $base.'/payments/'.$this->txid; + } + /** * Returns a human-readable description for known PAYONE error codes. * diff --git a/app/Models/ShoppingPayment.php b/app/Models/ShoppingPayment.php index 2149b99f..322e96de 100644 --- a/app/Models/ShoppingPayment.php +++ b/app/Models/ShoppingPayment.php @@ -180,4 +180,19 @@ class ShoppingPayment extends Model default => __('payment.stripe_online_short'), }; } + + /** + * Deep-Link ins Stripe-Dashboard, sobald ein PaymentIntent zugeordnet ist + * (external_reference; vor dem ersten Webhook steht dort noch die Session-ID). + */ + public function stripeDashboardUrl(): ?string + { + if ($this->payment_provider !== 'stripe' || ! str_starts_with((string) $this->external_reference, 'pi_')) { + return null; + } + + $base = $this->mode === 'test' ? 'https://dashboard.stripe.com/test' : 'https://dashboard.stripe.com'; + + return $base.'/payments/'.$this->external_reference; + } } diff --git a/dev/2026-07-08-stripe/ENTWICKLUNGSPLAN-STRIPE.md b/dev/2026-07-08-stripe/ENTWICKLUNGSPLAN-STRIPE.md index 65607620..93d15fa3 100644 --- a/dev/2026-07-08-stripe/ENTWICKLUNGSPLAN-STRIPE.md +++ b/dev/2026-07-08-stripe/ENTWICKLUNGSPLAN-STRIPE.md @@ -1,6 +1,6 @@ # Entwicklungsplan: Stripe als Zahlungsanbieter (PAYONE bleibt als Option) -> **Status:** In Arbeit (S0 ✓, S1 ✓, S2 ✓, S3 ✓ — **live in Produktion**; S4 ✓ **E2E komplett** (Abschluss, Cron, Decline→Recovery, authentication_required); S5 ✓ „Zahlungsart ändern" inkl. E2E über CRM + Kunden-Link (nur Portal-Pfad ohne Sichttest, gleicher Code-Pfad); S7.6 ✓ Zahlarten-Kommunikation — **vor Abo-Freigabe:** Deployment + 3 Migrationen, dann `abo-payments-frozen` aufheben, §16) · **Erstellt:** 2026-07-08 · **Zuletzt aktualisiert:** 2026-07-09 +> **Status:** In Arbeit (S0–S5 ✓, S7.4 ✓, S7.6 ✓ — **alles live seit 2026-07-10 inkl. Abos**, Freeze aufgehoben, Vorkasse wieder sichtbar, läuft fehlerfrei; offen: S7.1/S7.2/S7.3/S7.5, S6-Rest, S9 → §16) · **Erstellt:** 2026-07-08 · **Zuletzt aktualisiert:** 2026-07-10 > > **Lage-Update 2026-07-09 (Stakeholder):** Das PAYONE-Konto ist **wieder freigeschaltet**. > Daraus ergeben sich folgende verbindliche Entscheidungen (Details §7.2): @@ -459,8 +459,9 @@ in `StripeCheckoutTest.php`; Karten-Update-Regressionssuite (17 Tests) unveränd 57831, 57832 — müssen noch storniert/bereinigt werden, Statistik/Provision!). 8. [x] Checkout geöffnet: Einmalzahlungen laufen seit 2026-07-08 abends für alle über Stripe. 9. [x] Kontrolle: `storage/logs/stripe.log`, `stripe_webhook_events`, Payment-Dashboard. -10. [ ] **Nach S4/S5 (JETZT offen, → §16):** Deployment + 3 neue Migrationen, dann - `abo-payments-frozen` aufheben und Vorkasse wieder einblenden. +10. [x] **Nach S4/S5:** Deployment + 3 neue Migrationen, `abo-payments-frozen` + aufgehoben, Vorkasse wieder eingeblendet — ✅ **live seit 2026-07-10, läuft + fehlerfrei** (→ §16). --- @@ -713,10 +714,38 @@ Payment-Dashboard kennt Stripe. | S7.1 | **Schalter-UI** im Admin (Einstellungen): aktueller Provider, Umschalten mit Bestätigungsdialog + Hinweistext („wirkt nur auf neue Zahlungen"), `SysLog`-Eintrag. Nur berechtigte Admin-Rolle. | | S7.2 | **Refunds:** `OrderWithdrawalController` (Widerruf) um automatische Erstattung erweitern — Provider der Ursprungszahlung entscheidet (Stripe: `Refund`-API; PAYONE: bestehender/manueller Weg). Teil-Erstattungen unterstützen. | | S7.3 | Admin-Detailansichten (`ShoppingOrder`, `UserAbo`, Zahlungen): Provider-Badge, Stripe-IDs verlinkt zum Stripe-Dashboard (Deep-Link `https://dashboard.stripe.com/...`). | -| S7.4 | Payment-Dashboard (`/admin/payment-dashboard`) **für Stripe anpassen/optimieren** (Stakeholder 2026-07-09): Kacheln/Listen kennen bisher nur PAYONE — Stripe-Zahlungen korrekt ausweisen (Provider-Filter, Zahlart-Labels, Verlinkung ins Stripe-Dashboard), Umsatz/Fehlerraten je Provider, Übersicht „Abos je Provider" (Auslauf-Fortschritt ablesbar). | +| S7.4 | Payment-Dashboard (`/admin/payment-dashboard`) **für Stripe anpassen/optimieren** (Stakeholder 2026-07-09): Kacheln/Listen kennen bisher nur PAYONE — Stripe-Zahlungen korrekt ausweisen (Provider-Filter, Zahlart-Labels, Verlinkung ins Stripe-Dashboard), Umsatz/Fehlerraten je Provider, Übersicht „Abos je Provider" (Auslauf-Fortschritt ablesbar). **ERLEDIGT ✅ (2026-07-10), siehe Umsetzungsstatus S7.4 unten.** | | S7.5 | Doku: Betriebs-Runbook (Webhook neu ausliefern, Key-Rotation, Testmodus) in diesem Ordner ablegen. | | S7.6 | **Zahlarten-Kommunikation** (Stakeholder 2026-07-09): Die über Stripe verfügbaren Zahlarten klar benennen, möglichst mit Logos — (a) im Checkout unter der „Online-Zahlung"-Option, (b) auf den Shop-Seiten, (c) im Footer bei den Zahlungsmöglichkeiten, (d) auf der Seite `/zahlungsarten`. Vorkasse als „Vorkasse (PAYONE)" ausweisen. **ERLEDIGT ✅ (2026-07-09), siehe Umsetzungsstatus unten.** | +### Umsetzungsstatus S7.4 (ERLEDIGT ✅, 2026-07-10) + +`/admin/payment-dashboard` kennt jetzt beide Provider (Alt-Zeilen ohne Provider-Wert +zählen defensiv als PAYONE): + +- **Provider-Status/Uptime:** Stripe in `getProviderStats()`/`getUptimeStats()` aktiviert; + `payment:check-uptime` prüft zusätzlich `https://api.stripe.com/v1/` (401 ohne Key + belegt Erreichbarkeit → kein API-Key nötig) inkl. Auto-Incident/Auto-Resolve wie PAYONE. + Incident-Kachel zeigt Stripe+PAYONE getrennt (`stripe_incidents_30d`). +- **Zahlungen** (`payments`): Anbieter-Filter (Stripe/PAYONE), Provider-Badge statt + rohem clearingtype, Umsatz-je-Provider-Kacheln (bezahlt/gesamt + Summe), Deep-Link + ins Stripe-Dashboard an Zahlung (external_reference `pi_…`, testmodus-bewusst) und + an jeder Stripe-Transaktion (txid). Helper: `ShoppingPayment::stripeDashboardUrl()`, + `PaymentTransaction::stripeDashboardUrl()`. +- **Rohe Transaktionen** (`transactions`): Anbieter-Filter + -Spalte, Karte + „Erfolgsraten je Anbieter". **Bugfix nebenbei:** `request()->string()` liefert ein + `Stringable` (immer truthy, `!== ''`) → der txaction-Filter griff IMMER mit leerem + Wert, die Liste war dauerhaft leer; jetzt `->toString()` wie in `payments()`. +- **Abos je Anbieter** (Index + GF-Ansicht): neue Kachel mit aktiven Abos je Provider + (`scopeActiveAbo`), Fehlerzähler (Status 3) und Fortschrittsbalken „Anteil Stripe" — + der Auslauf PAYONE → Stripe (§7.2) ist damit direkt ablesbar. GF-Ansicht zeigt + zusätzlich Zahlungsquote je Provider im Anbieter-Status. +- **Log-Viewer:** Kanal-Umschalter stripe/payone (Default stripe, Whitelist), + Tab heißt jetzt „Provider-Logs". +- **Tests:** `tests/Feature/PaymentDashboard/PaymentDashboardProviderTest.php` (9 Tests: + Filter, Stats je Provider, Abo-Auslauf, Deep-Links, Log-Kanäle, Uptime via Http::fake); + gesamte Dashboard-Suite (49 Tests) grün. + ### Umsetzungsstatus S7.6 (ERLEDIGT ✅, 2026-07-09) Kommuniziertes Zahlarten-Set (per API gegen die aktive Stripe `PaymentMethodConfiguration` @@ -885,14 +914,12 @@ Abo-/Order-Views (Provider-Badge, maskiertes Zahlungsmittel) PayPal, PAYONE→Stripe-Umzug), Decline → Status 3, Recovery-Checkout, authentication_required. Aufräumen: Test-Abos #440/#442 (Abo 442 steht nach dem letzten Test auf Status 3 mit der `…3184`-Testkarte) und Test-Bestellungen bereinigen. -2. **Deployment + Migrationen** (3 neue: Provider-/Stripe-Abo-Spalten, - `widen_clearingtype`), dann im Admin: **`abo-payments-frozen` aufheben** (§7.2), - Vorkasse wieder einblenden (`payment-vorkasse-hidden` aus). Ersten 04:00-Cron-Lauf - beobachten (holt eingefrorene PAYONE-Abbuchungen nach → großer Lauf!): - `abo_order.log`, `stripe.log`, `stripe_webhook_events`, Payment-Dashboard. - Der Mischlauf PAYONE+Stripe verifiziert sich dabei. Dazu im **Live**-Stripe-Dashboard - das Zahlarten-Set mit dem Testmodus abgleichen (S7.6 kommuniziert u. a. Link, EPS, - Bancontact — Test/Live sind getrennt konfiguriert!). +2. ~~**Deployment + Migrationen + Freeze aufheben**~~ ✅ **LIVE seit 2026-07-10** + (Stakeholder): S4/S5/S7.6 deployt, Abo-Freeze aufgehoben, Vorkasse wieder sichtbar — + läuft bisher fehlerfrei. **Weiter beobachten:** die ersten 04:00-Cron-Läufe + (`abo_order.log`, `stripe.log`, `stripe_webhook_events`, Payment-Dashboard) und im + **Live**-Stripe-Dashboard das Zahlarten-Set mit dem Testmodus abgleichen (S7.6 + kommuniziert u. a. Link, EPS, Bancontact — Test/Live sind getrennt konfiguriert!). 3. ~~Phase **S5**~~ ✅ umgesetzt + E2E (2026-07-09, Scope erweitert auf „Zahlungsart ändern"): CRM-Pfad und Berater-Kunden-Link im Browser verprobt (inkl. PayPal und PAYONE→Stripe-Umzug, Abo 440). Nur der **Portal-Pfad** ist ohne Sichttest @@ -900,5 +927,10 @@ Abo-/Order-Views (Provider-Badge, maskiertes Zahlungsmittel) 4. ~~**S7.6 Zahlarten-Kommunikation**~~ ✅ umgesetzt (2026-07-09, siehe Umsetzungsstatus in §11): Logos + Benennung in Checkout, Footer (alle Shop-Seiten) und `/zahlungsarten`. **Offen:** kurzer Sichttest im Browser (Footer, `/zahlungsarten`, Checkout). -5. **S7.4 Payment-Dashboard** für Stripe anpassen/optimieren (Stakeholder 2026-07-09). -6. Nachgelagert: **S9 Rückbuchungen & Stornierungen** (§12.1). +5. ~~**S7.4 Payment-Dashboard**~~ ✅ umgesetzt (2026-07-10, siehe Umsetzungsstatus in + §11): Provider-Filter/-Badges, Stripe-Deep-Links, Umsatz-/Erfolgsraten je Provider, + Abos-je-Anbieter-Kachel (Auslauf-Fortschritt), Stripe-Uptime-Check, Log-Viewer für + stripe/payone. **Offen:** kurzer Sichttest im Browser. +6. Verbleibend aus S7: S7.1 (SysLog für Schalter), S7.2 (Refunds), S7.3 + (Admin-Detailansichten mit Provider-Badge/Deep-Links), S7.5 (Betriebs-Runbook). +7. Nachgelagert: **S9 Rückbuchungen & Stornierungen** (§12.1). diff --git a/resources/views/admin/payment-dashboard/_partials/abo-provider-stats.blade.php b/resources/views/admin/payment-dashboard/_partials/abo-provider-stats.blade.php new file mode 100644 index 00000000..87ba687c --- /dev/null +++ b/resources/views/admin/payment-dashboard/_partials/abo-provider-stats.blade.php @@ -0,0 +1,45 @@ +{{-- Abos je Provider: macht den Auslauf-Fortschritt PAYONE → Stripe ablesbar (Plan S7.4). --}} +
+
+ Abos je Anbieter + — Auslaufmodell PAYONE → Stripe +
+
+ @if($aboProviderStats['total'] === 0) +
Keine aktiven Abos vorhanden.
+ @else +
+ @foreach($aboProviderStats['providers'] as $providerKey => $providerRow) +
+
+ {{ $providerRow['label'] }} + + {{ $providerRow['total'] }} aktiv + +
+
+ @if($providerRow['failed'] > 0) + {{ $providerRow['failed'] }} mit fehlgeschlagener Zahlung + @else + keine fehlgeschlagenen Zahlungen + @endif +
+
+ @endforeach +
+ @if($aboProviderStats['stripe_share'] !== null) +
+
+ Anteil Stripe + {{ $aboProviderStats['stripe_share'] }}% von {{ $aboProviderStats['total'] }} aktiven Abos +
+
+
+
+
+ @endif + @endif +
+
diff --git a/resources/views/admin/payment-dashboard/_partials/stats-cards.blade.php b/resources/views/admin/payment-dashboard/_partials/stats-cards.blade.php index e86c6fe7..555854a6 100644 --- a/resources/views/admin/payment-dashboard/_partials/stats-cards.blade.php +++ b/resources/views/admin/payment-dashboard/_partials/stats-cards.blade.php @@ -22,9 +22,12 @@
-
PAYONE (30 Tage)
+
Incidents (30 Tage)
- {{ $stats['payone_incidents_30d'] }} + {{ $stats['stripe_incidents_30d'] + $stats['payone_incidents_30d'] }} +
+
+ Stripe {{ $stats['stripe_incidents_30d'] }} · PAYONE {{ $stats['payone_incidents_30d'] }}
diff --git a/resources/views/admin/payment-dashboard/index.blade.php b/resources/views/admin/payment-dashboard/index.blade.php index 8adf8e38..890a4237 100644 --- a/resources/views/admin/payment-dashboard/index.blade.php +++ b/resources/views/admin/payment-dashboard/index.blade.php @@ -69,6 +69,9 @@ @endforeach
+ {{-- Abos je Provider (Auslauf-Fortschritt) --}} + @include('admin.payment-dashboard._partials.abo-provider-stats') + {{-- Tabs --}} diff --git a/resources/views/admin/payment-dashboard/logs.blade.php b/resources/views/admin/payment-dashboard/logs.blade.php index 65a9e7a4..2d1eef75 100644 --- a/resources/views/admin/payment-dashboard/logs.blade.php +++ b/resources/views/admin/payment-dashboard/logs.blade.php @@ -7,10 +7,17 @@ Zurück - PAYONE Log-Viewer + Provider-Log-Viewer - @if(count($availableDates) > 1) -
+ + + @if(count($availableDates) > 1) -
- @endif + @endif + @if(count($entries) === 0) @@ -29,7 +36,7 @@ @if(count($availableDates) > 0) Verfügbare Daten: {{ implode(', ', array_map(fn($d) => \Carbon\Carbon::parse($d)->format('d.m.Y'), $availableDates)) }} @else - Der Log-Kanal payone hat noch keine Einträge geschrieben. + Der Log-Kanal {{ $selectedChannel }} hat noch keine Einträge geschrieben. @endif @else diff --git a/resources/views/admin/payment-dashboard/management.blade.php b/resources/views/admin/payment-dashboard/management.blade.php index fb5cde83..4323f0d7 100644 --- a/resources/views/admin/payment-dashboard/management.blade.php +++ b/resources/views/admin/payment-dashboard/management.blade.php @@ -76,12 +76,20 @@ @endif
{{ $provider['total_30d'] }} Incidents (30d)
+ @if(isset($transactionStats['providers'][$key])) +
+ Zahlungsquote: {{ $transactionStats['providers'][$key]['success_rate'] }}% +
+ @endif @endforeach + {{-- Abos je Provider (Auslauf-Fortschritt) --}} + @include('admin.payment-dashboard._partials.abo-provider-stats') + {{-- Aktive Störungen --}} @if($openIncidents->count() > 0)
diff --git a/resources/views/admin/payment-dashboard/payments.blade.php b/resources/views/admin/payment-dashboard/payments.blade.php index b8e8c967..abc7cb7e 100644 --- a/resources/views/admin/payment-dashboard/payments.blade.php +++ b/resources/views/admin/payment-dashboard/payments.blade.php @@ -69,6 +69,29 @@
+ {{-- Umsatz je Provider --}} + @if(count($paymentStats['providers']) > 0) +
+ @foreach($paymentStats['providers'] as $providerKey => $providerRow) +
+
+
+
+ {{ $providerRow['label'] }} + + {{ $providerRow['paid'] }}/{{ $providerRow['total'] }} bezahlt + +
+
+ Umsatz (bezahlt): {{ number_format($providerRow['paid_amount'], 2, ',', '.') }} € +
+
+
+
+ @endforeach +
+ @endif + {{-- Filter --}}
@@ -79,6 +102,13 @@ @endforeach + + + +