Stripe-Integration (S7.4): Payment-Dashboard für Stripe
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
ce4750fd1a
commit
8e09aaefe8
13 changed files with 600 additions and 45 deletions
|
|
@ -0,0 +1,45 @@
|
|||
{{-- Abos je Provider: macht den Auslauf-Fortschritt PAYONE → Stripe ablesbar (Plan S7.4). --}}
|
||||
<div class="card mb-4">
|
||||
<h6 class="card-header">
|
||||
Abos je Anbieter
|
||||
<small class="text-muted">— Auslaufmodell PAYONE → Stripe</small>
|
||||
</h6>
|
||||
<div class="card-body py-3">
|
||||
@if($aboProviderStats['total'] === 0)
|
||||
<div class="text-muted">Keine aktiven Abos vorhanden.</div>
|
||||
@else
|
||||
<div class="row">
|
||||
@foreach($aboProviderStats['providers'] as $providerKey => $providerRow)
|
||||
<div class="col-sm-6 col-md-3 mb-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>{{ $providerRow['label'] }}</strong>
|
||||
<span class="badge badge-{{ $providerKey === 'stripe' ? 'primary' : 'secondary' }}">
|
||||
{{ $providerRow['total'] }} aktiv
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
@if($providerRow['failed'] > 0)
|
||||
<span class="text-danger">{{ $providerRow['failed'] }} mit fehlgeschlagener Zahlung</span>
|
||||
@else
|
||||
keine fehlgeschlagenen Zahlungen
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@if($aboProviderStats['stripe_share'] !== null)
|
||||
<div class="mt-2">
|
||||
<div class="d-flex justify-content-between small text-muted mb-1">
|
||||
<span>Anteil Stripe</span>
|
||||
<span>{{ $aboProviderStats['stripe_share'] }}% von {{ $aboProviderStats['total'] }} aktiven Abos</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 8px">
|
||||
<div class="progress-bar bg-primary" role="progressbar"
|
||||
style="width: {{ $aboProviderStats['stripe_share'] }}%"
|
||||
aria-valuenow="{{ $aboProviderStats['stripe_share'] }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -22,9 +22,12 @@
|
|||
<div class="col-sm-6 col-xl-2">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-muted small">PAYONE (30 Tage)</div>
|
||||
<div class="text-muted small">Incidents (30 Tage)</div>
|
||||
<div class="display-4 font-weight-bold text-info">
|
||||
{{ $stats['payone_incidents_30d'] }}
|
||||
{{ $stats['stripe_incidents_30d'] + $stats['payone_incidents_30d'] }}
|
||||
</div>
|
||||
<div class="text-muted" style="font-size:0.7rem">
|
||||
Stripe {{ $stats['stripe_incidents_30d'] }} · PAYONE {{ $stats['payone_incidents_30d'] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -69,6 +69,9 @@
|
|||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Abos je Provider (Auslauf-Fortschritt) --}}
|
||||
@include('admin.payment-dashboard._partials.abo-provider-stats')
|
||||
|
||||
{{-- Tabs --}}
|
||||
<ul class="nav nav-tabs" id="dashboardTabs">
|
||||
<li class="nav-item">
|
||||
|
|
@ -104,7 +107,7 @@
|
|||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ route('admin.payment-dashboard.logs') }}">
|
||||
<i class="ion ion-md-list"></i> PAYONE Logs
|
||||
<i class="ion ion-md-list"></i> Provider-Logs
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -7,10 +7,17 @@
|
|||
<a href="{{ route('admin.payment-dashboard.index') }}" class="btn btn-sm btn-outline-secondary mr-2">
|
||||
<i class="ion ion-md-arrow-back"></i> Zurück
|
||||
</a>
|
||||
<strong>PAYONE Log-Viewer</strong>
|
||||
<strong>Provider-Log-Viewer</strong>
|
||||
</div>
|
||||
@if(count($availableDates) > 1)
|
||||
<form method="GET" action="{{ route('admin.payment-dashboard.logs') }}">
|
||||
<form method="GET" action="{{ route('admin.payment-dashboard.logs') }}" class="form-inline">
|
||||
<select name="channel" class="custom-select custom-select-sm mr-2" onchange="this.form.submit()">
|
||||
@foreach($availableChannels as $channel)
|
||||
<option value="{{ $channel }}" {{ $selectedChannel === $channel ? 'selected' : '' }}>
|
||||
{{ strtoupper($channel) }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if(count($availableDates) > 1)
|
||||
<select name="date" class="custom-select custom-select-sm" onchange="this.form.submit()">
|
||||
@foreach($availableDates as $date)
|
||||
<option value="{{ $date }}" {{ $selectedDate === $date ? 'selected' : '' }}>
|
||||
|
|
@ -18,8 +25,8 @@
|
|||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
@endif
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@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 <code>payone</code> hat noch keine Einträge geschrieben.
|
||||
Der Log-Kanal <code>{{ $selectedChannel }}</code> hat noch keine Einträge geschrieben.
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
|
|
|
|||
|
|
@ -76,12 +76,20 @@
|
|||
</span>
|
||||
@endif
|
||||
<div class="text-muted small mt-1">{{ $provider['total_30d'] }} Incidents (30d)</div>
|
||||
@if(isset($transactionStats['providers'][$key]))
|
||||
<div class="text-muted small">
|
||||
Zahlungsquote: <strong>{{ $transactionStats['providers'][$key]['success_rate'] }}%</strong>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Abos je Provider (Auslauf-Fortschritt) --}}
|
||||
@include('admin.payment-dashboard._partials.abo-provider-stats')
|
||||
|
||||
{{-- Aktive Störungen --}}
|
||||
@if($openIncidents->count() > 0)
|
||||
<div class="card mb-4 border-danger">
|
||||
|
|
|
|||
|
|
@ -69,6 +69,29 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Umsatz je Provider --}}
|
||||
@if(count($paymentStats['providers']) > 0)
|
||||
<div class="row mb-4">
|
||||
@foreach($paymentStats['providers'] as $providerKey => $providerRow)
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card">
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>{{ $providerRow['label'] }}</strong>
|
||||
<span class="badge badge-{{ $providerKey === 'stripe' ? 'primary' : 'secondary' }}">
|
||||
{{ $providerRow['paid'] }}/{{ $providerRow['total'] }} bezahlt
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-muted small mt-1">
|
||||
Umsatz (bezahlt): <strong>{{ number_format($providerRow['paid_amount'], 2, ',', '.') }} €</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Filter --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2">
|
||||
|
|
@ -79,6 +102,13 @@
|
|||
<option value="{{ $val }}" {{ (int)$days === $val ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<label class="mr-2 small">Anbieter:</label>
|
||||
<select name="provider" class="custom-select custom-select-sm mr-3 mb-1" onchange="this.form.submit()">
|
||||
<option value="">Alle</option>
|
||||
@foreach(['stripe' => 'Stripe', 'payone' => 'PAYONE'] as $val => $label)
|
||||
<option value="{{ $val }}" {{ $providerFilter === $val ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<label class="mr-2 small">Status:</label>
|
||||
<select name="txaction" class="custom-select custom-select-sm mr-3 mb-1" onchange="this.form.submit()">
|
||||
<option value="">Alle</option>
|
||||
|
|
@ -135,6 +165,11 @@
|
|||
</td>
|
||||
<td class="align-middle">
|
||||
<code class="small">{{ $payment->reference }}</code>
|
||||
@if($stripeUrl = $payment->stripeDashboardUrl())
|
||||
<a href="{{ $stripeUrl }}" target="_blank" rel="noopener" class="ml-1" title="Im Stripe-Dashboard öffnen">
|
||||
<i class="ion ion-md-open"></i>
|
||||
</a>
|
||||
@endif
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
@if($payment->shopping_order)
|
||||
|
|
@ -181,8 +216,10 @@
|
|||
</td>
|
||||
<td class="align-middle small">
|
||||
{{ $payment->getPaymentType() }}
|
||||
@if($payment->clearingtype)
|
||||
<span class="badge badge-secondary ml-1">{{ $payment->clearingtype }}</span>
|
||||
@if($payment->payment_provider === 'stripe')
|
||||
<span class="badge badge-primary ml-1">Stripe</span>
|
||||
@else
|
||||
<span class="badge badge-secondary ml-1" title="{{ $payment->clearingtype }}">PAYONE</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="align-middle text-nowrap">
|
||||
|
|
@ -253,7 +290,13 @@
|
|||
<tbody>
|
||||
@foreach($payment->payment_transactions as $tx)
|
||||
<tr class="{{ $tx->txaction === 'failed' ? 'table-danger' : ($tx->txaction === 'paid' ? 'table-success' : '') }}">
|
||||
<td class="pl-4 small">{{ $tx->txid ?? '—' }}</td>
|
||||
<td class="pl-4 small">
|
||||
@if($txStripeUrl = $tx->stripeDashboardUrl())
|
||||
<a href="{{ $txStripeUrl }}" target="_blank" rel="noopener" title="Im Stripe-Dashboard öffnen">{{ $tx->txid }}</a>
|
||||
@else
|
||||
{{ $tx->txid ?? '—' }}
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@php
|
||||
$txaColor = match($tx->txaction) {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,30 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Erfolgsraten je Provider --}}
|
||||
@if(count($transactionStats['providers']) > 0)
|
||||
<div class="card mb-4">
|
||||
<h6 class="card-header">Erfolgsraten je Anbieter (letzte {{ $transactionStats['days'] }} Tage)</h6>
|
||||
<div class="card-body py-2">
|
||||
<div class="row">
|
||||
@foreach($transactionStats['providers'] as $providerKey => $providerRow)
|
||||
<div class="col-sm-6 col-md-3 mb-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>{{ $providerRow['label'] }}</strong>
|
||||
<span class="badge badge-{{ $providerRow['success_rate'] >= 95 ? 'success' : ($providerRow['success_rate'] >= 80 ? 'warning' : 'danger') }}">
|
||||
{{ $providerRow['success_rate'] }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
{{ $providerRow['paid'] }} bezahlt / {{ $providerRow['failed'] }} fehlgeschlagen / {{ $providerRow['total'] }} gesamt
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($transactionStats['error_distribution']->count() > 0)
|
||||
<div class="card mb-4">
|
||||
<h6 class="card-header">Fehlercodes (letzte {{ $transactionStats['days'] }} Tage)</h6>
|
||||
|
|
@ -79,6 +103,13 @@
|
|||
<option value="{{ $val }}" {{ (int)$days === $val ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<label class="mr-2 small">Anbieter:</label>
|
||||
<select name="provider" class="custom-select custom-select-sm mr-3" onchange="this.form.submit()">
|
||||
<option value="">Alle</option>
|
||||
@foreach(['stripe' => 'Stripe', 'payone' => 'PAYONE'] as $val => $label)
|
||||
<option value="{{ $val }}" {{ $providerFilter === $val ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<label class="mr-2 small">Aktion:</label>
|
||||
<select name="txaction" class="custom-select custom-select-sm mr-3" onchange="this.form.submit()">
|
||||
<option value="">Alle</option>
|
||||
|
|
@ -99,6 +130,7 @@
|
|||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Datum</th>
|
||||
<th>Anbieter</th>
|
||||
<th>Aktion</th>
|
||||
<th>TX-ID</th>
|
||||
<th>Referenz</th>
|
||||
|
|
@ -113,6 +145,13 @@
|
|||
<tr class="{{ $tx->txaction === 'failed' ? 'table-danger' : ($tx->txaction === 'paid' ? 'table-success' : '') }}">
|
||||
<td class="text-muted small">{{ $tx->id }}</td>
|
||||
<td class="text-nowrap small">{{ $tx->created_at->format('d.m.Y H:i') }}</td>
|
||||
<td>
|
||||
@if($tx->provider === 'stripe')
|
||||
<span class="badge badge-primary">Stripe</span>
|
||||
@else
|
||||
<span class="badge badge-secondary">PAYONE</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@php
|
||||
$actionColor = match($tx->txaction) {
|
||||
|
|
@ -125,7 +164,13 @@
|
|||
@endphp
|
||||
<span class="badge badge-{{ $actionColor }}">{{ $tx->txaction ?? '—' }}</span>
|
||||
</td>
|
||||
<td class="small">{{ $tx->txid ?? '—' }}</td>
|
||||
<td class="small">
|
||||
@if($txStripeUrl = $tx->stripeDashboardUrl())
|
||||
<a href="{{ $txStripeUrl }}" target="_blank" rel="noopener" title="Im Stripe-Dashboard öffnen">{{ $tx->txid }}</a>
|
||||
@else
|
||||
{{ $tx->txid ?? '—' }}
|
||||
@endif
|
||||
</td>
|
||||
<td class="small">
|
||||
@if($tx->shopping_payment)
|
||||
<code>{{ $tx->shopping_payment->reference }}</code>
|
||||
|
|
@ -160,14 +205,14 @@
|
|||
</tr>
|
||||
@if($tx->transmitted_data)
|
||||
<tr class="collapse" id="tx-data-{{ $tx->id }}">
|
||||
<td colspan="9" class="bg-light">
|
||||
<td colspan="10" class="bg-light">
|
||||
<pre class="mb-0 small" style="max-height:200px; overflow-y:auto">{{ json_encode($tx->transmitted_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="text-center text-muted py-3">Keine Transaktionen gefunden.</td>
|
||||
<td colspan="10" class="text-center text-muted py-3">Keine Transaktionen gefunden.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue