14-04-2026
This commit is contained in:
parent
f58c709945
commit
0f82fea88a
72 changed files with 7414 additions and 148 deletions
|
|
@ -1,6 +1,5 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\User\IncentiveController;
|
||||
use App\Models\Incentive;
|
||||
use App\Models\IncentiveParticipant;
|
||||
use App\User;
|
||||
|
|
@ -113,7 +112,7 @@ it('sortiert bei Punktgleichstand Teilnehmer mit Klarnamen (bestaetigte Teilnahm
|
|||
expect($orderedUserIds)->toBe([$confirmed->user_id, $anonymous->user_id]);
|
||||
});
|
||||
|
||||
it('begrenzt die User-Rangliste auf 30 Plaetze (Gewinner-Zone bleibt max_winners)', function () {
|
||||
it('zeigt alle Teilnehmer mit Aktivitaet in der User-Rangliste (kein Limit)', function () {
|
||||
$incentive = Incentive::factory()->create(['max_winners' => 20]);
|
||||
|
||||
$makeUser = fn () => User::forceCreate([
|
||||
|
|
@ -130,13 +129,21 @@ it('begrenzt die User-Rangliste auf 30 Plaetze (Gewinner-Zone bleibt max_winners
|
|||
]);
|
||||
}
|
||||
|
||||
// Teilnehmer ohne Punkte soll nicht erscheinen
|
||||
IncentiveParticipant::factory()->create([
|
||||
'incentive_id' => $incentive->id,
|
||||
'user_id' => $makeUser()->id,
|
||||
'total_points' => 0,
|
||||
'qualified_partners' => 0,
|
||||
'qualified_abos' => 0,
|
||||
]);
|
||||
|
||||
$ranking = IncentiveParticipant::where('incentive_id', $incentive->id)
|
||||
->withRankingActivity()
|
||||
->orderByIncentiveLeaderboard()
|
||||
->limit(IncentiveController::USER_RANKING_DISPLAY_LIMIT)
|
||||
->get();
|
||||
|
||||
expect($ranking)->toHaveCount(30);
|
||||
expect($ranking)->toHaveCount(35);
|
||||
});
|
||||
|
||||
it('blendet in der User-Ranglogik Teilnehmer ohne Partner, Abo und Punkte aus', function () {
|
||||
|
|
|
|||
112
tests/Feature/PaymentDashboard/CheckPaymentUptimeCommandTest.php
Normal file
112
tests/Feature/PaymentDashboard/CheckPaymentUptimeCommandTest.php
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
use App\Mail\PaymentIncidentAlert;
|
||||
use App\Models\IncidentActivity;
|
||||
use App\Models\PaymentIncident;
|
||||
use App\Models\ProviderUptimeLog;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
// ─── Uptime-Command: Erfolgsfall ──────────────────────────────────────────────
|
||||
|
||||
it('schreibt einen positiven ProviderUptimeLog wenn PAYONE erreichbar ist', function () {
|
||||
Http::fake([
|
||||
'api.pay1.de/*' => Http::response('', 200),
|
||||
]);
|
||||
|
||||
$this->artisan('payment:check-uptime')->assertSuccessful();
|
||||
|
||||
$log = ProviderUptimeLog::where('provider', 'payone')->latest()->first();
|
||||
expect($log)->not->toBeNull();
|
||||
expect($log->is_up)->toBeTrue();
|
||||
expect($log->error_message)->toBeNull();
|
||||
});
|
||||
|
||||
it('schreibt einen negativen ProviderUptimeLog und legt Incident an wenn PAYONE nicht erreichbar ist', function () {
|
||||
Http::fake([
|
||||
'api.pay1.de/*' => Http::response('Server Error', 503),
|
||||
]);
|
||||
|
||||
$this->artisan('payment:check-uptime')->assertSuccessful();
|
||||
|
||||
$log = ProviderUptimeLog::where('provider', 'payone')->latest()->first();
|
||||
expect($log)->not->toBeNull();
|
||||
expect($log->is_up)->toBeFalse();
|
||||
|
||||
$incident = PaymentIncident::where('provider', 'payone')->where('type', 'outage')->first();
|
||||
expect($incident)->not->toBeNull();
|
||||
expect($incident->severity)->toBe('critical');
|
||||
expect($incident->status)->toBe('open');
|
||||
});
|
||||
|
||||
it('erstellt keine doppelten Outage-Incidents bei mehrfachem Ausfall in derselben Stunde', function () {
|
||||
Http::fake([
|
||||
'api.pay1.de/*' => Http::response('', 503),
|
||||
]);
|
||||
|
||||
$this->artisan('payment:check-uptime');
|
||||
$this->artisan('payment:check-uptime');
|
||||
|
||||
expect(PaymentIncident::where('provider', 'payone')->where('type', 'outage')->count())->toBe(1);
|
||||
});
|
||||
|
||||
it('löst offene Outage-Incidents automatisch auf wenn PAYONE wieder erreichbar ist', function () {
|
||||
PaymentIncident::create([
|
||||
'title' => 'Automatischer Ausfall',
|
||||
'provider' => 'payone',
|
||||
'type' => 'outage',
|
||||
'severity' => 'critical',
|
||||
'status' => 'open',
|
||||
'detected_at' => now()->subMinutes(10),
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'api.pay1.de/*' => Http::response('', 200),
|
||||
]);
|
||||
|
||||
$this->artisan('payment:check-uptime')->assertSuccessful();
|
||||
|
||||
$incident = PaymentIncident::where('provider', 'payone')->where('type', 'outage')->first();
|
||||
expect($incident->fresh()->status)->toBe('resolved');
|
||||
expect($incident->fresh()->resolved_at)->not->toBeNull();
|
||||
|
||||
$activity = IncidentActivity::where('incident_id', $incident->id)
|
||||
->where('type', 'status_change')
|
||||
->first();
|
||||
expect($activity)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('verarbeitet Verbindungsfehler als Ausfall', function () {
|
||||
Http::fake([
|
||||
'api.pay1.de/*' => fn () => throw new \Exception('Connection refused'),
|
||||
]);
|
||||
|
||||
$this->artisan('payment:check-uptime')->assertSuccessful();
|
||||
|
||||
$log = ProviderUptimeLog::where('provider', 'payone')->latest()->first();
|
||||
expect($log->is_up)->toBeFalse();
|
||||
expect($log->error_message)->toContain('Connection refused');
|
||||
});
|
||||
|
||||
// ─── Mailable ────────────────────────────────────────────────────────────────
|
||||
|
||||
it('sendet PaymentIncidentAlert-Mail wenn critical Incident angelegt wird', function () {
|
||||
Mail::fake();
|
||||
|
||||
$incident = PaymentIncident::create([
|
||||
'title' => 'Kritischer Test-Incident',
|
||||
'provider' => 'payone',
|
||||
'type' => 'outage',
|
||||
'severity' => 'critical',
|
||||
'detected_at' => now(),
|
||||
]);
|
||||
|
||||
Mail::to(config('app.exception_mail'))->queue(new PaymentIncidentAlert($incident));
|
||||
|
||||
Mail::assertQueued(PaymentIncidentAlert::class, function (PaymentIncidentAlert $mail) use ($incident) {
|
||||
return $mail->incident->id === $incident->id;
|
||||
});
|
||||
});
|
||||
101
tests/Feature/PaymentDashboard/CheckoutFunnelTrackingTest.php
Normal file
101
tests/Feature/PaymentDashboard/CheckoutFunnelTrackingTest.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
use App\Models\CheckoutFunnelEvent;
|
||||
use App\Services\CheckoutFunnelTracker;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('speichert ein checkout_visited Ereignis', function () {
|
||||
CheckoutFunnelTracker::visitedCheckout(consultantUserId: 42, metadata: ['is_from' => 'shopping']);
|
||||
|
||||
expect(CheckoutFunnelEvent::count())->toBe(1);
|
||||
|
||||
$event = CheckoutFunnelEvent::first();
|
||||
expect($event->event)->toBe('checkout_visited');
|
||||
expect($event->consultant_user_id)->toBe(42);
|
||||
expect($event->metadata)->toMatchArray(['is_from' => 'shopping']);
|
||||
});
|
||||
|
||||
it('speichert ein form_submitted Ereignis', function () {
|
||||
CheckoutFunnelTracker::submittedForm(
|
||||
shoppingUserId: 1,
|
||||
shoppingOrderId: 10,
|
||||
consultantUserId: 5,
|
||||
paymentMethod: 'elv',
|
||||
amountCents: 4990,
|
||||
);
|
||||
|
||||
$event = CheckoutFunnelEvent::first();
|
||||
expect($event->event)->toBe('form_submitted');
|
||||
expect($event->shopping_user_id)->toBe(1);
|
||||
expect($event->shopping_order_id)->toBe(10);
|
||||
expect($event->payment_method)->toBe('elv');
|
||||
expect($event->amount_cents)->toBe(4990);
|
||||
});
|
||||
|
||||
it('speichert ein payment_initiated Ereignis', function () {
|
||||
CheckoutFunnelTracker::initiatedPayment(
|
||||
shoppingUserId: 1,
|
||||
shoppingOrderId: 10,
|
||||
shoppingPaymentId: 20,
|
||||
consultantUserId: 5,
|
||||
paymentMethod: 'cc',
|
||||
amountCents: 9900,
|
||||
);
|
||||
|
||||
$event = CheckoutFunnelEvent::first();
|
||||
expect($event->event)->toBe('payment_initiated');
|
||||
expect($event->shopping_payment_id)->toBe(20);
|
||||
expect($event->amount_cents)->toBe(9900);
|
||||
});
|
||||
|
||||
it('speichert ein payment_returned Ereignis mit return_status', function () {
|
||||
CheckoutFunnelTracker::returnedFromPayment(
|
||||
shoppingPaymentId: 20,
|
||||
returnStatus: 'cancel',
|
||||
);
|
||||
|
||||
$event = CheckoutFunnelEvent::first();
|
||||
expect($event->event)->toBe('payment_returned');
|
||||
expect($event->return_status)->toBe('cancel');
|
||||
expect($event->shopping_payment_id)->toBe(20);
|
||||
});
|
||||
|
||||
it('speichert ein payment_confirmed Ereignis', function () {
|
||||
CheckoutFunnelTracker::confirmedPayment(
|
||||
shoppingPaymentId: 20,
|
||||
txaction: 'paid',
|
||||
);
|
||||
|
||||
$event = CheckoutFunnelEvent::first();
|
||||
expect($event->event)->toBe('payment_confirmed');
|
||||
expect($event->metadata)->toMatchArray(['txaction' => 'paid']);
|
||||
});
|
||||
|
||||
it('schluckt Exceptions und loggt nur eine Warnung', function () {
|
||||
// Ungültiges event-Enum — soll keinen Fehler werfen
|
||||
expect(fn () => CheckoutFunnelTracker::visitedCheckout())->not->toThrow(\Throwable::class);
|
||||
});
|
||||
|
||||
it('Funnel-View gibt korrekten View zurück', function () {
|
||||
$admin = \App\User::forceCreate([
|
||||
'email' => 'admin-funnel-'.uniqid().'@test.com',
|
||||
'password' => \Illuminate\Support\Facades\Hash::make('secret'),
|
||||
'admin' => 2,
|
||||
'lang' => 'de',
|
||||
]);
|
||||
$this->actingAs($admin);
|
||||
|
||||
CheckoutFunnelTracker::visitedCheckout();
|
||||
CheckoutFunnelTracker::submittedForm(1, 1, null, 'elv', 3990);
|
||||
|
||||
$controller = new \App\Http\Controllers\Admin\PaymentDashboardController;
|
||||
$response = $controller->funnel();
|
||||
|
||||
expect($response->getName())->toBe('admin.payment-dashboard.funnel');
|
||||
$data = $response->getData();
|
||||
expect($data)->toHaveKey('funnelSteps');
|
||||
expect($data['funnelSteps'][0]['count'])->toBe(1);
|
||||
expect($data['funnelSteps'][1]['count'])->toBe(1);
|
||||
});
|
||||
183
tests/Feature/PaymentDashboard/PaymentDashboardAccessTest.php
Normal file
183
tests/Feature/PaymentDashboard/PaymentDashboardAccessTest.php
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\Admin\PaymentDashboardController;
|
||||
use App\Http\Middleware\Admin;
|
||||
use App\Models\PaymentIncident;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function makeAdminUser(): User
|
||||
{
|
||||
return User::forceCreate([
|
||||
'email' => 'admin-'.uniqid().'@test.com',
|
||||
'password' => Hash::make('secret'),
|
||||
'admin' => 2,
|
||||
'lang' => 'de',
|
||||
]);
|
||||
}
|
||||
|
||||
function makeRegularUser(): User
|
||||
{
|
||||
return User::forceCreate([
|
||||
'email' => 'user-'.uniqid().'@test.com',
|
||||
'password' => Hash::make('secret'),
|
||||
'admin' => 0,
|
||||
'lang' => 'de',
|
||||
]);
|
||||
}
|
||||
|
||||
function makeVipUser(): User
|
||||
{
|
||||
return User::forceCreate([
|
||||
'email' => 'vip-'.uniqid().'@test.com',
|
||||
'password' => Hash::make('secret'),
|
||||
'admin' => 1,
|
||||
'lang' => 'de',
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── Admin Middleware Tests ───────────────────────────────────────────────────
|
||||
|
||||
it('Admin-Middleware lässt Admins (admin >= 2) durch', function () {
|
||||
$admin = makeAdminUser();
|
||||
Auth::setUser($admin);
|
||||
|
||||
$request = Request::create('/admin/payment-dashboard');
|
||||
|
||||
$middleware = new Admin;
|
||||
$passed = false;
|
||||
$middleware->handle($request, function () use (&$passed) {
|
||||
$passed = true;
|
||||
});
|
||||
|
||||
expect($passed)->toBeTrue();
|
||||
});
|
||||
|
||||
it('Admin-Middleware blockiert normale Benutzer (admin = 0)', function () {
|
||||
$user = makeRegularUser();
|
||||
$request = Request::create('/admin/payment-dashboard');
|
||||
$request->setUserResolver(fn () => $user);
|
||||
|
||||
$middleware = new Admin;
|
||||
$response = $middleware->handle($request, fn () => null);
|
||||
|
||||
expect($response)->not->toBeNull();
|
||||
expect($response->getStatusCode())->toBe(302);
|
||||
});
|
||||
|
||||
it('Admin-Middleware blockiert VIP-Benutzer (admin = 1)', function () {
|
||||
$vip = makeVipUser();
|
||||
$request = Request::create('/admin/payment-dashboard');
|
||||
$request->setUserResolver(fn () => $vip);
|
||||
|
||||
$middleware = new Admin;
|
||||
$response = $middleware->handle($request, fn () => null);
|
||||
|
||||
expect($response)->not->toBeNull();
|
||||
expect($response->getStatusCode())->toBe(302);
|
||||
});
|
||||
|
||||
// ─── Controller Auth Tests ────────────────────────────────────────────────────
|
||||
|
||||
it('Entwickler-Ansicht gibt View zurück für Admins', function () {
|
||||
$admin = makeAdminUser();
|
||||
$this->actingAs($admin);
|
||||
|
||||
$controller = new PaymentDashboardController;
|
||||
$response = $controller->index();
|
||||
|
||||
expect($response->getName())->toBe('admin.payment-dashboard.index');
|
||||
});
|
||||
|
||||
it('GF-Ansicht gibt View zurück für Super-Admins (admin >= 3)', function () {
|
||||
$superAdmin = User::forceCreate([
|
||||
'email' => 'superadmin-'.uniqid().'@test.com',
|
||||
'password' => Hash::make('secret'),
|
||||
'admin' => 3,
|
||||
'lang' => 'de',
|
||||
]);
|
||||
$this->actingAs($superAdmin);
|
||||
|
||||
$controller = new PaymentDashboardController;
|
||||
$response = $controller->management();
|
||||
|
||||
expect($response->getName())->toBe('admin.payment-dashboard.management');
|
||||
});
|
||||
|
||||
it('GF-Ansicht liefert 403 für normale Admins (admin = 2)', function () {
|
||||
$admin = makeAdminUser();
|
||||
$this->actingAs($admin);
|
||||
|
||||
$controller = new PaymentDashboardController;
|
||||
|
||||
expect(fn () => $controller->management())->toThrow(\Symfony\Component\HttpKernel\Exception\HttpException::class);
|
||||
});
|
||||
|
||||
it('Incident-Detail gibt korrekten View zurück', function () {
|
||||
$admin = makeAdminUser();
|
||||
$this->actingAs($admin);
|
||||
|
||||
$incident = PaymentIncident::create([
|
||||
'title' => 'Test Incident Detail',
|
||||
'provider' => 'payone',
|
||||
'type' => 'payment_failure',
|
||||
'severity' => 'high',
|
||||
'detected_at' => now(),
|
||||
]);
|
||||
|
||||
$controller = new PaymentDashboardController;
|
||||
$response = $controller->show($incident);
|
||||
|
||||
expect($response->getName())->toBe('admin.payment-dashboard.show');
|
||||
expect($response->getData()['incident']->id)->toBe($incident->id);
|
||||
});
|
||||
|
||||
it('Log-Ansicht gibt View zurück', function () {
|
||||
$admin = makeAdminUser();
|
||||
$this->actingAs($admin);
|
||||
|
||||
$controller = new PaymentDashboardController;
|
||||
$response = $controller->logs();
|
||||
|
||||
expect($response->getName())->toBe('admin.payment-dashboard.logs');
|
||||
});
|
||||
|
||||
it('Transaktions-Ansicht gibt View zurück', function () {
|
||||
$admin = makeAdminUser();
|
||||
$this->actingAs($admin);
|
||||
|
||||
$controller = new PaymentDashboardController;
|
||||
$response = $controller->transactions();
|
||||
|
||||
expect($response->getName())->toBe('admin.payment-dashboard.transactions');
|
||||
});
|
||||
|
||||
it('Abbruch-Analyse gibt View zurück', function () {
|
||||
$admin = makeAdminUser();
|
||||
$this->actingAs($admin);
|
||||
|
||||
$controller = new PaymentDashboardController;
|
||||
$response = $controller->abandoned();
|
||||
|
||||
expect($response->getName())->toBe('admin.payment-dashboard.abandoned');
|
||||
});
|
||||
|
||||
it('Abbruch-Analyse enthält die 3 erwarteten Datensätze', function () {
|
||||
$admin = makeAdminUser();
|
||||
$this->actingAs($admin);
|
||||
|
||||
$controller = new PaymentDashboardController;
|
||||
$response = $controller->abandoned();
|
||||
|
||||
$data = $response->getData();
|
||||
expect($data)->toHaveKey('ordersWithoutPayment');
|
||||
expect($data)->toHaveKey('cancelledPayments');
|
||||
expect($data)->toHaveKey('pendingPayments');
|
||||
expect($data)->toHaveKey('abandonedStats');
|
||||
expect($data['abandonedStats'])->toHaveKeys(['no_payment', 'cancelled', 'no_callback']);
|
||||
});
|
||||
298
tests/Feature/PaymentDashboard/PaymentIncidentCrudTest.php
Normal file
298
tests/Feature/PaymentDashboard/PaymentIncidentCrudTest.php
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\Admin\PaymentDashboardController;
|
||||
use App\Http\Requests\PaymentIncident\AddIncidentActivityRequest;
|
||||
use App\Http\Requests\PaymentIncident\StorePaymentIncidentRequest;
|
||||
use App\Http\Requests\PaymentIncident\UpdateIncidentStatusRequest;
|
||||
use App\Models\IncidentActivity;
|
||||
use App\Models\PaymentIncident;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function makeAdmin(): User
|
||||
{
|
||||
return User::forceCreate([
|
||||
'email' => 'admin-crud-'.uniqid().'@test.com',
|
||||
'password' => Hash::make('secret'),
|
||||
'admin' => 2,
|
||||
'lang' => 'de',
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── Model / Scopes ───────────────────────────────────────────────────────────
|
||||
|
||||
it('scopeOpen gibt nur offene, laufende und wartende Incidents zurück', function () {
|
||||
PaymentIncident::create(['title' => 'Offen', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'status' => 'open', 'detected_at' => now()]);
|
||||
PaymentIncident::create(['title' => 'In Bearbeitung', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'status' => 'in_progress', 'detected_at' => now()]);
|
||||
PaymentIncident::create(['title' => 'Wartet', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'status' => 'waiting_provider', 'detected_at' => now()]);
|
||||
PaymentIncident::create(['title' => 'Gelöst', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'status' => 'resolved', 'detected_at' => now()]);
|
||||
|
||||
$open = PaymentIncident::open()->get();
|
||||
|
||||
expect($open)->toHaveCount(3);
|
||||
expect($open->pluck('title')->toArray())->not->toContain('Gelöst');
|
||||
});
|
||||
|
||||
it('scopePayone filtert nach PAYONE', function () {
|
||||
PaymentIncident::create(['title' => 'PAYONE Incident', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'detected_at' => now()]);
|
||||
PaymentIncident::create(['title' => 'Stripe Incident', 'provider' => 'stripe', 'type' => 'other', 'severity' => 'low', 'detected_at' => now()]);
|
||||
|
||||
expect(PaymentIncident::payone()->count())->toBe(1);
|
||||
expect(PaymentIncident::payone()->first()->title)->toBe('PAYONE Incident');
|
||||
});
|
||||
|
||||
it('scopeLastDays filtert nach Zeitraum', function () {
|
||||
PaymentIncident::create(['title' => 'Alt', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'detected_at' => now()->subDays(10)]);
|
||||
PaymentIncident::create(['title' => 'Neu', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'detected_at' => now()]);
|
||||
|
||||
expect(PaymentIncident::lastDays(7)->count())->toBe(1);
|
||||
expect(PaymentIncident::lastDays(7)->first()->title)->toBe('Neu');
|
||||
});
|
||||
|
||||
it('Accessor severity_color gibt Bootstrap-Klasse zurück', function () {
|
||||
$incident = new PaymentIncident(['severity' => 'critical']);
|
||||
expect($incident->severity_color)->toBe('danger');
|
||||
|
||||
$incident = new PaymentIncident(['severity' => 'high']);
|
||||
expect($incident->severity_color)->toBe('warning');
|
||||
|
||||
$incident = new PaymentIncident(['severity' => 'low']);
|
||||
expect($incident->severity_color)->toBe('success');
|
||||
});
|
||||
|
||||
it('Accessor duration berechnet Dauer korrekt', function () {
|
||||
$incident = new PaymentIncident([
|
||||
'detected_at' => now()->subMinutes(90),
|
||||
'resolved_at' => null,
|
||||
]);
|
||||
|
||||
expect($incident->duration)->toContain('h');
|
||||
});
|
||||
|
||||
// ─── Controller: Incident anlegen ─────────────────────────────────────────────
|
||||
|
||||
it('legt einen neuen Incident an und erstellt automatisch eine erste Aktivität', function () {
|
||||
$admin = makeAdmin();
|
||||
$this->actingAs($admin);
|
||||
|
||||
$request = StorePaymentIncidentRequest::create('/admin/payment-dashboard', 'POST', [
|
||||
'title' => 'PAYONE IPN ausgefallen',
|
||||
'provider' => 'payone',
|
||||
'type' => 'ipn_error',
|
||||
'severity' => 'high',
|
||||
'detected_at' => now()->format('Y-m-d H:i:s'),
|
||||
'affected_orders' => 5,
|
||||
'affected_revenue' => '450.00',
|
||||
]);
|
||||
$request->setContainer(app());
|
||||
$request->validateResolved();
|
||||
|
||||
try {
|
||||
(new PaymentDashboardController)->store($request);
|
||||
} catch (\Symfony\Component\Routing\Exception\RouteNotFoundException $e) {
|
||||
// Domain-spezifische Routen sind im Test-Kontext nicht registriert – erwartet.
|
||||
}
|
||||
|
||||
$incident = PaymentIncident::where('title', 'PAYONE IPN ausgefallen')->first();
|
||||
expect($incident)->not->toBeNull();
|
||||
expect($incident->provider)->toBe('payone');
|
||||
expect($incident->status)->toBe('open');
|
||||
expect($incident->affected_orders)->toBe(5);
|
||||
|
||||
expect(IncidentActivity::where('incident_id', $incident->id)->count())->toBe(1);
|
||||
expect(IncidentActivity::where('incident_id', $incident->id)->first()->type)->toBe('note');
|
||||
});
|
||||
|
||||
// ─── FormRequest Validierung ──────────────────────────────────────────────────
|
||||
|
||||
it('validiert Pflichtfelder beim Anlegen eines Incidents', function () {
|
||||
$rules = (new StorePaymentIncidentRequest)->rules();
|
||||
$validator = Validator::make([], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('title'))->toBeTrue();
|
||||
expect($validator->errors()->has('provider'))->toBeTrue();
|
||||
expect($validator->errors()->has('type'))->toBeTrue();
|
||||
expect($validator->errors()->has('severity'))->toBeTrue();
|
||||
expect($validator->errors()->has('detected_at'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validiert ungültige Enum-Werte in StorePaymentIncidentRequest', function () {
|
||||
$rules = (new StorePaymentIncidentRequest)->rules();
|
||||
$validator = Validator::make([
|
||||
'title' => 'Test',
|
||||
'provider' => 'bitcoin',
|
||||
'type' => 'unknown',
|
||||
'severity' => 'extreme',
|
||||
'detected_at' => now()->toDateTimeString(),
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('provider'))->toBeTrue();
|
||||
expect($validator->errors()->has('type'))->toBeTrue();
|
||||
expect($validator->errors()->has('severity'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validiert Pflichtfelder in AddIncidentActivityRequest', function () {
|
||||
$rules = (new AddIncidentActivityRequest)->rules();
|
||||
$validator = Validator::make([], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('type'))->toBeTrue();
|
||||
expect($validator->errors()->has('title'))->toBeTrue();
|
||||
});
|
||||
|
||||
// ─── Controller: Status ändern ────────────────────────────────────────────────
|
||||
|
||||
it('ändert den Status eines Incidents und erstellt eine Aktivität', function () {
|
||||
$admin = makeAdmin();
|
||||
$this->actingAs($admin);
|
||||
|
||||
$incident = PaymentIncident::create([
|
||||
'title' => 'Status-Test',
|
||||
'provider' => 'payone',
|
||||
'type' => 'outage',
|
||||
'severity' => 'critical',
|
||||
'detected_at' => now(),
|
||||
]);
|
||||
|
||||
$request = UpdateIncidentStatusRequest::create('/admin/payment-dashboard/'.$incident->id.'/status', 'PATCH', [
|
||||
'status' => 'resolved',
|
||||
]);
|
||||
$request->setContainer(app());
|
||||
$request->validateResolved();
|
||||
|
||||
$controller = new PaymentDashboardController;
|
||||
$controller->updateStatus($request, $incident);
|
||||
|
||||
$incident->refresh();
|
||||
expect($incident->status)->toBe('resolved');
|
||||
expect($incident->resolved_at)->not->toBeNull();
|
||||
|
||||
$statusActivity = IncidentActivity::where('incident_id', $incident->id)
|
||||
->where('type', 'status_change')
|
||||
->first();
|
||||
expect($statusActivity)->not->toBeNull();
|
||||
expect($statusActivity->title)->toContain('Gelöst');
|
||||
});
|
||||
|
||||
it('setzt resolved_at nicht wenn Status in_progress ist', function () {
|
||||
$admin = makeAdmin();
|
||||
$this->actingAs($admin);
|
||||
|
||||
$incident = PaymentIncident::create([
|
||||
'title' => 'Resolved At Test',
|
||||
'provider' => 'payone',
|
||||
'type' => 'other',
|
||||
'severity' => 'low',
|
||||
'detected_at' => now(),
|
||||
]);
|
||||
|
||||
$request = UpdateIncidentStatusRequest::create('', 'PATCH', ['status' => 'in_progress']);
|
||||
$request->setContainer(app());
|
||||
$request->validateResolved();
|
||||
(new PaymentDashboardController)->updateStatus($request, $incident);
|
||||
|
||||
expect($incident->fresh()->resolved_at)->toBeNull();
|
||||
});
|
||||
|
||||
// ─── Controller: Aktivität hinzufügen ────────────────────────────────────────
|
||||
|
||||
it('fügt eine Aktivität zu einem Incident hinzu', function () {
|
||||
$admin = makeAdmin();
|
||||
$this->actingAs($admin);
|
||||
|
||||
$incident = PaymentIncident::create([
|
||||
'title' => 'Aktivitäts-Test',
|
||||
'provider' => 'payone',
|
||||
'type' => 'payment_failure',
|
||||
'severity' => 'medium',
|
||||
'detected_at' => now(),
|
||||
]);
|
||||
|
||||
$request = AddIncidentActivityRequest::create('', 'POST', [
|
||||
'type' => 'email',
|
||||
'title' => 'Mail an PAYONE gesendet',
|
||||
'content' => 'Ticket eröffnet, Antwort abwarten.',
|
||||
]);
|
||||
$request->setContainer(app());
|
||||
$request->validateResolved();
|
||||
|
||||
(new PaymentDashboardController)->addActivity($request, $incident);
|
||||
|
||||
$activity = IncidentActivity::where('incident_id', $incident->id)
|
||||
->where('type', 'email')
|
||||
->first();
|
||||
expect($activity)->not->toBeNull();
|
||||
expect($activity->title)->toBe('Mail an PAYONE gesendet');
|
||||
expect($activity->author)->toBe($admin->name ?? 'System');
|
||||
});
|
||||
|
||||
it('setzt Incident automatisch auf in_progress wenn Aktivität bei offenem Incident hinzugefügt wird', function () {
|
||||
$admin = makeAdmin();
|
||||
$this->actingAs($admin);
|
||||
|
||||
$incident = PaymentIncident::create([
|
||||
'title' => 'Auto-Status-Test',
|
||||
'provider' => 'payone',
|
||||
'type' => 'other',
|
||||
'severity' => 'low',
|
||||
'status' => 'open',
|
||||
'detected_at' => now(),
|
||||
]);
|
||||
|
||||
$request = AddIncidentActivityRequest::create('', 'POST', ['type' => 'note', 'title' => 'Erste Notiz']);
|
||||
$request->setContainer(app());
|
||||
$request->validateResolved();
|
||||
|
||||
(new PaymentDashboardController)->addActivity($request, $incident);
|
||||
|
||||
expect($incident->fresh()->status)->toBe('in_progress');
|
||||
});
|
||||
|
||||
it('validiert Pflichtfelder in UpdateIncidentStatusRequest', function () {
|
||||
$rules = (new UpdateIncidentStatusRequest)->rules();
|
||||
$validator = Validator::make([], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('status'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validiert ungültigen Status in UpdateIncidentStatusRequest', function () {
|
||||
$rules = (new UpdateIncidentStatusRequest)->rules();
|
||||
$validator = Validator::make(['status' => 'deleted'], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('status'))->toBeTrue();
|
||||
});
|
||||
|
||||
// ─── Cache Invalidierung ──────────────────────────────────────────────────────
|
||||
|
||||
it('invalidiert den Cache nach Incident-Anlage', function () {
|
||||
$admin = makeAdmin();
|
||||
$this->actingAs($admin);
|
||||
Cache::put('open_incident_count', 99, 60);
|
||||
|
||||
$request = StorePaymentIncidentRequest::create('', 'POST', [
|
||||
'title' => 'Cache-Test',
|
||||
'provider' => 'payone',
|
||||
'type' => 'other',
|
||||
'severity' => 'low',
|
||||
'detected_at' => now()->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
$request->setContainer(app());
|
||||
$request->validateResolved();
|
||||
|
||||
try {
|
||||
(new PaymentDashboardController)->store($request);
|
||||
} catch (\Symfony\Component\Routing\Exception\RouteNotFoundException $e) {
|
||||
// Domain-spezifische Routen sind im Test-Kontext nicht registriert – erwartet.
|
||||
}
|
||||
|
||||
expect(Cache::has('open_incident_count'))->toBeFalse();
|
||||
});
|
||||
169
tests/Feature/WizardPaymentCartTest.php
Normal file
169
tests/Feature/WizardPaymentCartTest.php
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Wizard-Payment: Da Radio Buttons nur eine Produktauswahl erlauben,
|
||||
* darf nach dem Absenden des Formulars immer nur exakt ein Produkt
|
||||
* im Warenkorb liegen — auch wenn der Nutzer per Browser-Zurück-Button
|
||||
* mehrfach einreicht.
|
||||
*/
|
||||
|
||||
use App\Models\Product;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Schema::connection('sqlite')->table('products', function ($table) {
|
||||
if (! Schema::connection('sqlite')->hasColumn('products', 'slug')) {
|
||||
$table->string('slug')->nullable();
|
||||
}
|
||||
if (! Schema::connection('sqlite')->hasColumn('products', 'free_shipping_consultant')) {
|
||||
$table->boolean('free_shipping_consultant')->default(false)->nullable();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function makeWizardProduct(string $name, float $price = 69.90, int $pos = 1): Product
|
||||
{
|
||||
return Product::forceCreate([
|
||||
'name' => $name,
|
||||
'title' => $name,
|
||||
'price' => $price,
|
||||
'tax' => 19.00,
|
||||
'active' => true,
|
||||
'is_membership_only' => false,
|
||||
'pos' => $pos,
|
||||
'show_at' => 3,
|
||||
'show_on' => '["7","8"]',
|
||||
'identifier' => 'show_order',
|
||||
'action' => '["0","1"]',
|
||||
'no_commission' => false,
|
||||
'no_free_shipping' => false,
|
||||
'free_shipping_consultant' => false,
|
||||
'weight' => 0,
|
||||
'points' => 0,
|
||||
'slug' => \Illuminate\Support\Str::slug($name).'-'.uniqid(),
|
||||
]);
|
||||
}
|
||||
|
||||
it('entfernt vorhandene Produkte aus dem Wizard-Warenkorb wenn ein neues ausgewaehlt wird', function () {
|
||||
$productA = makeWizardProduct('Starter-Paket A', 49.90, 1);
|
||||
$productB = makeWizardProduct('Starter-Paket B', 99.90, 2);
|
||||
|
||||
$instance = 'shopping';
|
||||
|
||||
\Yard::instance($instance)->destroy();
|
||||
|
||||
// Produkt A ist bereits im Warenkorb (simuliert den Zustand nach erstem Absenden)
|
||||
\Yard::instance($instance)->add(
|
||||
$productA->id,
|
||||
$productA->name,
|
||||
1,
|
||||
$productA->price,
|
||||
false,
|
||||
false,
|
||||
[
|
||||
'slug' => $productA->slug,
|
||||
'weight' => 0,
|
||||
'points' => 0,
|
||||
'no_commission' => false,
|
||||
'no_free_shipping' => false,
|
||||
'free_shipping_consultant' => false,
|
||||
'show_on' => $productA->show_on,
|
||||
]
|
||||
);
|
||||
|
||||
expect(\Yard::instance($instance)->count())->toBe(1);
|
||||
|
||||
// Nutzer geht zurück und wählt Produkt B — die neue Logik soll Produkt A entfernen
|
||||
$cartItemB = \Yard::instance($instance)->add(
|
||||
$productB->id,
|
||||
$productB->name,
|
||||
1,
|
||||
$productB->price,
|
||||
false,
|
||||
false,
|
||||
[
|
||||
'slug' => $productB->slug,
|
||||
'weight' => 0,
|
||||
'points' => 0,
|
||||
'no_commission' => false,
|
||||
'no_free_shipping' => false,
|
||||
'free_shipping_consultant' => false,
|
||||
'show_on' => $productB->show_on,
|
||||
]
|
||||
);
|
||||
|
||||
// Schutzlogik aus WizardController::storePayment()
|
||||
foreach (\Yard::instance($instance)->content() as $existingItem) {
|
||||
if ($existingItem->rowId !== $cartItemB->rowId) {
|
||||
\Yard::instance($instance)->remove($existingItem->rowId);
|
||||
}
|
||||
}
|
||||
|
||||
expect(\Yard::instance($instance)->count())->toBe(1);
|
||||
expect(\Yard::instance($instance)->content()->first()->id)->toBe($productB->id);
|
||||
|
||||
\Yard::instance($instance)->destroy();
|
||||
});
|
||||
|
||||
it('behaelt das Produkt bei qty=1 wenn dasselbe Produkt erneut abgesendet wird', function () {
|
||||
$product = makeWizardProduct('Starter-Paket A', 49.90, 1);
|
||||
|
||||
$instance = 'shopping';
|
||||
|
||||
\Yard::instance($instance)->destroy();
|
||||
|
||||
\Yard::instance($instance)->add(
|
||||
$product->id,
|
||||
$product->name,
|
||||
1,
|
||||
$product->price,
|
||||
false,
|
||||
false,
|
||||
[
|
||||
'slug' => $product->slug,
|
||||
'weight' => 0,
|
||||
'points' => 0,
|
||||
'no_commission' => false,
|
||||
'no_free_shipping' => false,
|
||||
'free_shipping_consultant' => false,
|
||||
'show_on' => $product->show_on,
|
||||
]
|
||||
);
|
||||
|
||||
// Zweites Absenden desselben Produkts
|
||||
$cartItemAgain = \Yard::instance($instance)->add(
|
||||
$product->id,
|
||||
$product->name,
|
||||
1,
|
||||
$product->price,
|
||||
false,
|
||||
false,
|
||||
[
|
||||
'slug' => $product->slug,
|
||||
'weight' => 0,
|
||||
'points' => 0,
|
||||
'no_commission' => false,
|
||||
'no_free_shipping' => false,
|
||||
'free_shipping_consultant' => false,
|
||||
'show_on' => $product->show_on,
|
||||
]
|
||||
);
|
||||
|
||||
if ($cartItemAgain->qty > 1) {
|
||||
\Yard::instance($instance)->update($cartItemAgain->rowId, 1);
|
||||
}
|
||||
|
||||
foreach (\Yard::instance($instance)->content() as $existingItem) {
|
||||
if ($existingItem->rowId !== $cartItemAgain->rowId) {
|
||||
\Yard::instance($instance)->remove($existingItem->rowId);
|
||||
}
|
||||
}
|
||||
|
||||
expect(\Yard::instance($instance)->count())->toBe(1);
|
||||
expect(\Yard::instance($instance)->get($cartItemAgain->rowId)->qty)->toBe(1);
|
||||
|
||||
\Yard::instance($instance)->destroy();
|
||||
});
|
||||
|
|
@ -3,3 +3,4 @@
|
|||
uses(Tests\TestCase::class)->in('Feature/Incentive');
|
||||
uses(Tests\TestCase::class)->in('Feature/Sys');
|
||||
uses(Tests\TestCase::class)->in('Unit/Incentive');
|
||||
uses(Tests\TestCase::class)->in('Feature/PaymentDashboard');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue