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
|
|
@ -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<ShoppingPayment>|Builder<PaymentTransaction> $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<string, array{label: string, total: int, failed: int}>, 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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue