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:
Kevin 2026-07-10 16:57:50 +00:00
parent ce4750fd1a
commit 8e09aaefe8
13 changed files with 600 additions and 45 deletions

View file

@ -0,0 +1,220 @@
<?php
use App\Http\Controllers\Admin\PaymentDashboardController;
use App\Models\PaymentTransaction;
use App\Models\ProviderUptimeLog;
use App\Models\ShoppingPayment;
use App\Models\UserAbo;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
uses(RefreshDatabase::class);
/**
* Plan S7.4 – Payment-Dashboard für Stripe: Provider-Filter, Stats je Provider,
* Abo-Auslauf-Übersicht, Stripe-Deep-Links, Uptime-Check.
*/
function dashboardAdmin(): User
{
return User::forceCreate([
'email' => 'dash-admin-'.uniqid('', true).'@test.com',
'password' => Hash::make('secret'),
'admin' => 2,
'lang' => 'de',
]);
}
function dashboardPayment(array $overrides = []): ShoppingPayment
{
return ShoppingPayment::forceCreate(array_merge([
'shopping_order_id' => 1,
'clearingtype' => 'cc',
'reference' => 'dash-'.uniqid('', true),
'amount' => 5000,
'currency' => 'EUR',
'mode' => 'test',
], $overrides));
}
function dashboardAbo(array $overrides = []): UserAbo
{
$consultant = User::forceCreate([
'email' => 'dash-abo-'.uniqid('', true).'@test.com',
'password' => Hash::make('secret'),
'lang' => 'de',
]);
return UserAbo::create(array_merge([
'user_id' => $consultant->id,
'shopping_user_id' => 1,
'is_for' => 'me',
'email' => $consultant->email,
'payone_userid' => 0,
'clearingtype' => 'cc',
'active' => true,
'status' => 2,
'abo_interval' => 5,
'next_date' => now()->addDays(3),
], $overrides));
}
// ─── Provider-Filter Zahlungen ────────────────────────────────────────────────
it('filtert Zahlungen nach Provider', function () {
$this->actingAs(dashboardAdmin());
$stripe = dashboardPayment(['payment_provider' => 'stripe', 'txaction' => 'paid']);
$payone = dashboardPayment(['payment_provider' => 'payone']);
request()->merge(['provider' => 'stripe', 'days' => 7]);
$view = (new PaymentDashboardController)->payments();
$ids = $view->getData()['payments']->pluck('id');
expect($ids->all())->toBe([$stripe->id]);
request()->merge(['provider' => 'payone']);
$view = (new PaymentDashboardController)->payments();
$ids = $view->getData()['payments']->pluck('id');
expect($ids->all())->toBe([$payone->id]);
});
it('liefert Umsatz und Zahlungen je Provider in den Payment-Stats', function () {
$this->actingAs(dashboardAdmin());
dashboardPayment(['payment_provider' => 'stripe', 'txaction' => 'paid', 'amount' => 10000]);
dashboardPayment(['payment_provider' => 'stripe', 'txaction' => 'failed', 'amount' => 2000]);
dashboardPayment(['payment_provider' => 'payone', 'txaction' => 'paid', 'amount' => 5000]);
$view = (new PaymentDashboardController)->payments();
$providers = $view->getData()['paymentStats']['providers'];
expect($providers['stripe']['total'])->toBe(2)
->and($providers['stripe']['paid'])->toBe(1)
->and($providers['stripe']['paid_amount'])->toBe(100.0)
->and($providers['payone']['paid'])->toBe(1)
->and($providers['payone']['paid_amount'])->toBe(50.0);
});
// ─── Provider-Filter Transaktionen ────────────────────────────────────────────
it('filtert Transaktionen nach Provider und liefert Erfolgsraten je Provider', function () {
$this->actingAs(dashboardAdmin());
$payment = dashboardPayment(['payment_provider' => 'stripe']);
$stripeTx = PaymentTransaction::create([
'shopping_payment_id' => $payment->id, 'provider' => 'stripe',
'request' => 'paid', 'txaction' => 'paid', 'txid' => 'pi_123', 'userid' => 0,
]);
PaymentTransaction::create([
'shopping_payment_id' => $payment->id, 'provider' => 'payone',
'request' => 'failed', 'txaction' => 'failed', 'txid' => '99001', 'userid' => 0,
]);
request()->merge(['provider' => 'stripe', 'days' => 7]);
$view = (new PaymentDashboardController)->transactions();
$data = $view->getData();
expect($data['transactions']->pluck('id')->all())->toBe([$stripeTx->id])
->and($data['transactionStats']['providers']['stripe']['success_rate'])->toBe(100.0)
->and($data['transactionStats']['providers']['payone']['failed'])->toBe(1);
});
// ─── Abos je Provider (Auslauf-Fortschritt) ───────────────────────────────────
it('zeigt aktive Abos je Provider inkl. Stripe-Anteil und Fehlerzählern', function () {
$this->actingAs(dashboardAdmin());
dashboardAbo(['payment_provider' => 'stripe']);
dashboardAbo(['payment_provider' => 'payone']);
dashboardAbo(['payment_provider' => 'payone', 'status' => 3]);
dashboardAbo(['payment_provider' => 'stripe', 'status' => 5]); // beendet → zählt nicht
dashboardAbo(['payment_provider' => 'payone', 'active' => false]); // inaktiv → zählt nicht
$view = (new PaymentDashboardController)->index();
$stats = $view->getData()['aboProviderStats'];
expect($stats['total'])->toBe(3)
->and($stats['providers']['stripe']['total'])->toBe(1)
->and($stats['providers']['payone']['total'])->toBe(2)
->and($stats['providers']['payone']['failed'])->toBe(1)
->and($stats['stripe_share'])->toBe(33.3);
});
it('führt Stripe in den Provider-Status-Karten', function () {
$this->actingAs(dashboardAdmin());
$view = (new PaymentDashboardController)->index();
$data = $view->getData();
expect($data['providerStats'])->toHaveKey('stripe')
->and($data['uptimeStats'])->toHaveKey('stripe')
->and($data['stats'])->toHaveKey('stripe_incidents_30d');
});
// ─── Stripe-Deep-Links ────────────────────────────────────────────────────────
it('baut Stripe-Dashboard-Links nur für Stripe-Zahlungen mit PaymentIntent', function () {
$testMode = dashboardPayment(['payment_provider' => 'stripe', 'external_reference' => 'pi_abc', 'mode' => 'test']);
$liveMode = dashboardPayment(['payment_provider' => 'stripe', 'external_reference' => 'pi_def', 'mode' => 'live']);
$sessionOnly = dashboardPayment(['payment_provider' => 'stripe', 'external_reference' => 'cs_xyz']);
$payone = dashboardPayment(['payment_provider' => 'payone', 'external_reference' => 'pi_fake']);
expect($testMode->stripeDashboardUrl())->toBe('https://dashboard.stripe.com/test/payments/pi_abc')
->and($liveMode->stripeDashboardUrl())->toBe('https://dashboard.stripe.com/payments/pi_def')
->and($sessionOnly->stripeDashboardUrl())->toBeNull()
->and($payone->stripeDashboardUrl())->toBeNull();
});
it('baut Stripe-Dashboard-Links für Stripe-Transaktionen über die txid', function () {
$payment = dashboardPayment(['payment_provider' => 'stripe']);
$stripeTx = PaymentTransaction::create([
'shopping_payment_id' => $payment->id, 'provider' => 'stripe',
'request' => 'paid', 'txaction' => 'paid', 'txid' => 'pi_tx1', 'userid' => 0, 'mode' => 'live',
]);
$sessionTx = PaymentTransaction::create([
'shopping_payment_id' => $payment->id, 'provider' => 'stripe',
'request' => 'appointed', 'txaction' => 'appointed', 'txid' => 'cs_tx1', 'userid' => 0,
]);
$payoneTx = PaymentTransaction::create([
'shopping_payment_id' => $payment->id, 'provider' => 'payone',
'request' => 'paid', 'txaction' => 'paid', 'txid' => '123456', 'userid' => 0,
]);
expect($stripeTx->stripeDashboardUrl())->toBe('https://dashboard.stripe.com/payments/pi_tx1')
->and($sessionTx->stripeDashboardUrl())->toBeNull()
->and($payoneTx->stripeDashboardUrl())->toBeNull();
});
// ─── Log-Viewer ───────────────────────────────────────────────────────────────
it('unterstützt im Log-Viewer stripe- und payone-Kanäle mit stripe als Default', function () {
$this->actingAs(dashboardAdmin());
$view = (new PaymentDashboardController)->logs();
expect($view->getData()['selectedChannel'])->toBe('stripe')
->and($view->getData()['availableChannels'])->toBe(['stripe', 'payone']);
request()->merge(['channel' => 'payone']);
$view = (new PaymentDashboardController)->logs();
expect($view->getData()['selectedChannel'])->toBe('payone');
request()->merge(['channel' => '../etc/passwd']);
$view = (new PaymentDashboardController)->logs();
expect($view->getData()['selectedChannel'])->toBe('stripe');
});
// ─── Uptime-Check ─────────────────────────────────────────────────────────────
it('prüft die Erreichbarkeit von Stripe und PAYONE im Uptime-Check', function () {
Http::fake([
'api.stripe.com/*' => Http::response('', 401),
'api.pay1.de/*' => Http::response('', 200),
]);
$this->artisan('payment:check-uptime')->assertSuccessful();
expect(ProviderUptimeLog::where('provider', 'stripe')->where('is_up', true)->count())->toBe(1)
->and(ProviderUptimeLog::where('provider', 'payone')->where('is_up', true)->count())->toBe(1);
});