forceFill([ 'admin' => $admin, 'lang' => 'de', 'active' => 1, 'blocked' => 0, 'wizard' => 100, ]); } function makeBackofficeStatisticsRequest(User $user): Request { $request = Request::create('/user/backoffice/statistics', 'GET'); $request->setUserResolver(fn () => $user); return $request; } function makeBackofficeStatisticsController(?BackofficeDashboardService $dashboardService = null, ?BackofficeDrilldownService $drilldownService = null): BackofficeStatisticsController { $dashboardService ??= Mockery::mock(BackofficeDashboardService::class); $drilldownService ??= Mockery::mock(BackofficeDrilldownService::class); return new BackofficeStatisticsController($dashboardService, $drilldownService); } function backofficeStatisticsStreamedContent(StreamedResponse $response): string { ob_start(); $response->sendContent(); return (string) ob_get_clean(); } it('zeigt die Backoffice-Statistik fuer VIP-User', function () { $vip = makeBackofficeStatisticsUser(1); $dashboardService = Mockery::mock(BackofficeDashboardService::class); $dashboardService ->shouldReceive('overview') ->once() ->andReturn([ 'month' => now()->month, 'year' => now()->year, 'metric_labels' => [], 'lines' => [], 'totals' => [], '_meta' => [ 'source_label' => 'Live', 'calculated_at' => null, ], ]); $controller = makeBackofficeStatisticsController($dashboardService); $response = $controller->index(makeBackofficeStatisticsRequest($vip)); expect($response->getName())->toBe('user.backoffice.statistics.index'); expect($response->getData())->toHaveKeys(['selectedMonth', 'selectedYear', 'statistics', 'performance']); expect($response->getData()['performance']['source_label'])->toBe('Live'); }); it('behaelt den zuletzt gewaehlten Statistik-Zeitraum in der Session', function () { $vip = makeBackofficeStatisticsUser(1); $dashboardService = Mockery::mock(BackofficeDashboardService::class); $dashboardService ->shouldReceive('overview') ->twice() ->andReturn([ 'month' => 4, 'year' => 2026, 'metric_labels' => [], 'lines' => [], 'totals' => [], '_meta' => [ 'source_label' => 'Snapshot', 'calculated_at' => '17.05.2026 04:45', ], ]); $controller = makeBackofficeStatisticsController($dashboardService); $firstRequest = makeBackofficeStatisticsRequest($vip); $firstRequest->query->set('month', 4); $firstRequest->query->set('year', 2026); $controller->index($firstRequest); $response = $controller->index(makeBackofficeStatisticsRequest($vip)); expect($response->getData()['selectedMonth'])->toBe(4); expect($response->getData()['selectedYear'])->toBe(2026); expect($response->getData()['performance']['source_label'])->toBe('Snapshot'); }); it('blockiert die Backoffice-Statistik fuer normale aktive User', function () { $user = makeBackofficeStatisticsUser(0); $controller = makeBackofficeStatisticsController(); expect(fn () => $controller->index(makeBackofficeStatisticsRequest($user))) ->toThrow(NotFoundHttpException::class); }); it('erstellt einen CSV-Export fuer die Statistik-Uebersicht', function () { $vip = makeBackofficeStatisticsUser(1); $dashboardService = Mockery::mock(BackofficeDashboardService::class); $dashboardService ->shouldReceive('overview') ->once() ->andReturn([ 'month' => 5, 'year' => 2026, 'metric_labels' => [], 'lines' => [ [ 'label' => 'Linie 1', 'consultants' => 2, 'new_partners' => 1, 'team_partner_abos' => 1, 'team_partner_abos_new' => 1, 'team_customer_abos' => 1, 'team_customer_abos_new' => 1, 'own_points' => 400, 'external_points' => 700, 'customer_abo_points' => 700, 'customer_single_order_points' => 0, 'customer_other_points' => 0, 'total_points' => 1100, 'shop_1000' => 1, 'turnover_net' => 300, ], ], 'totals' => [ 'label' => 'Summe', 'consultants' => 2, 'new_partners' => 1, 'team_partner_abos' => 1, 'team_partner_abos_new' => 1, 'team_customer_abos' => 1, 'team_customer_abos_new' => 1, 'own_points' => 400, 'external_points' => 700, 'customer_abo_points' => 700, 'customer_single_order_points' => 0, 'customer_other_points' => 0, 'total_points' => 1100, 'shop_1000' => 1, 'turnover_net' => 300, ], ]); $controller = makeBackofficeStatisticsController($dashboardService); $request = makeBackofficeStatisticsRequest($vip); $request->query->set('month', 5); $request->query->set('year', 2026); $response = $controller->overviewExport($request); $content = backofficeStatisticsStreamedContent($response); expect($response)->toBeInstanceOf(StreamedResponse::class); expect($response->headers->get('content-disposition'))->toContain('backoffice-statistik-uebersicht-05-2026.csv'); expect($content)->toContain('Linie;Berater;Neupartner;Teamabos'); expect($content)->toContain('Neue Teamabos'); expect($content)->toContain('"Linie 1";2;1;1;1;1;1;400;700;700;0;0;1100;1;300'); expect($content)->toContain('Summe;2;1;1;1;1;1;400;700;700;0;0;1100;1;300'); }); it('erstellt einen CSV-Export fuer Detaildaten mit Karriere-Level und Summenzeile', function () { $vip = makeBackofficeStatisticsUser(1); $drilldownService = Mockery::mock(BackofficeDrilldownService::class); $drilldownService ->shouldReceive('details') ->once() ->with($vip, 0, 'shop_1000', 5, 2026) ->andReturn([ 'metric' => 'shop_1000', 'metric_label' => '1000 Punkte Shop', 'line' => 0, 'line_label' => 'Alle Linien', 'month' => 5, 'year' => 2026, 'rows' => [ [ 'name' => 'Max Mustermann', 'email' => 'max@example.test', 'career_level' => 'Partner', 'own_points' => 400, 'external_points' => 700, 'customer_abo_points' => 700, 'customer_single_order_points' => 0, 'customer_other_points' => 0, 'total_points' => 1100, ], ], 'summary' => [ 'count' => 1, 'points' => 0, 'own_points' => 400, 'external_points' => 700, 'customer_abo_points' => 700, 'customer_single_order_points' => 0, 'customer_other_points' => 0, 'total_points' => 1100, 'deliveries' => 0, ], ]); $controller = makeBackofficeStatisticsController(null, $drilldownService); $request = makeBackofficeStatisticsRequest($vip); $request->query->set('line', 0); $request->query->set('metric', 'shop_1000'); $request->query->set('month', 5); $request->query->set('year', 2026); $response = $controller->export($request); $content = backofficeStatisticsStreamedContent($response); expect($response)->toBeInstanceOf(StreamedResponse::class); expect($response->headers->get('content-disposition'))->toContain('backoffice-statistik-shop_1000-linie-alle-05-2026.csv'); expect($content)->toContain('Name;E-Mail;Karriere-Level;Eigenpunkte;"Externe Punkte"'); expect($content)->toContain('"Max Mustermann";max@example.test;Partner;400;700;700;0;0;1100'); expect($content)->toContain('Summe;"1 Eintraege";;400;700;700;0;0;1100'); }); it('nutzt den gespeicherten Zeitraum fuer Detailansicht und Export', function () { $vip = makeBackofficeStatisticsUser(1); $dashboardService = Mockery::mock(BackofficeDashboardService::class); $drilldownService = Mockery::mock(BackofficeDrilldownService::class); $dashboardService ->shouldReceive('overview') ->once() ->andReturn([ 'month' => 4, 'year' => 2026, 'metric_labels' => [], 'lines' => [], 'totals' => [], ]); $drilldownService ->shouldReceive('details') ->once() ->with($vip, 1, 'consultants', 4, 2026) ->andReturn([ 'metric' => 'consultants', 'metric_label' => 'Berater', 'line' => 1, 'line_label' => 'Linie 1', 'month' => 4, 'year' => 2026, 'rows' => [], 'summary' => ['count' => 0], ]); $drilldownService ->shouldReceive('details') ->once() ->with($vip, 1, 'consultants', 4, 2026) ->andReturn([ 'metric' => 'consultants', 'metric_label' => 'Berater', 'line' => 1, 'line_label' => 'Linie 1', 'month' => 4, 'year' => 2026, 'rows' => [], 'summary' => ['count' => 0], ]); $controller = makeBackofficeStatisticsController($dashboardService, $drilldownService); $indexRequest = makeBackofficeStatisticsRequest($vip); $indexRequest->query->set('month', 4); $indexRequest->query->set('year', 2026); $controller->index($indexRequest); $detailsRequest = makeBackofficeStatisticsRequest($vip); $detailsRequest->query->set('line', 1); $detailsRequest->query->set('metric', 'consultants'); $detailsResponse = $controller->details($detailsRequest); $exportRequest = makeBackofficeStatisticsRequest($vip); $exportRequest->query->set('line', 1); $exportRequest->query->set('metric', 'consultants'); $exportResponse = $controller->export($exportRequest); expect($detailsResponse->getData()['selectedMonth'])->toBe(4); expect($detailsResponse->getData()['selectedYear'])->toBe(2026); expect($exportResponse->headers->get('content-disposition'))->toContain('backoffice-statistik-consultants-linie-1-04-2026.csv'); }); it('rendert eine Suche in der Detailtabelle', function () { $html = file_get_contents(resource_path('views/user/backoffice/statistics/details.blade.php')); expect($html)->toContain('backoffice-statistics-detail-search'); expect($html)->toContain('backoffice-statistics-detail-table'); expect($html)->toContain('data-sortable="true"'); expect($html)->toContain('getSortValue'); expect($html)->toContain('user_backoffice_statistics_export'); expect($html)->toContain('Datenschutz-Hinweis'); expect($html)->toContain('Karriere-Level'); expect($html)->not->toContain('Qualifikation'); $indexHtml = file_get_contents(resource_path('views/user/backoffice/statistics/index.blade.php')); expect($indexHtml)->toContain('user_backoffice_statistics_overview_export'); expect($indexHtml)->toContain('Datenquelle:'); expect($indexHtml)->toContain('Berechnet in'); }); it('erfasst die Herkunft bei Kundenbestellungen im Shop-Checkout', function () { $checkoutView = file_get_contents(resource_path('views/web/templates/checkout.blade.php')); $checkoutController = file_get_contents(app_path('Http/Controllers/Web/CheckoutController.php')); $checkoutRepository = file_get_contents(app_path('Repositories/CheckoutRepository.php')); $orderDetail = file_get_contents(resource_path('views/admin/sales/_detail.blade.php')); expect($checkoutView)->toContain('customer_order_source'); expect($checkoutView)->toContain('Wie bist du auf uns aufmerksam geworden?'); expect($checkoutController)->toContain("Request::get('is_from') === 'shopping'"); expect($checkoutController)->toContain('customer_order_source'); expect($checkoutRepository)->toContain('$shopping_user->is_from === \'shopping\''); expect($checkoutRepository)->toContain('customer_order_source_comment'); expect($orderDetail)->toContain('getCustomerOrderSourceLabel'); $shoppingOrder = (new ShoppingOrder)->forceFill([ 'customer_order_source' => 'social_media', ]); expect($shoppingOrder->getCustomerOrderSourceLabel())->toBe('Social Media'); });