diff --git a/app/Services/Abo/TeamAboOverviewService.php b/app/Services/Abo/TeamAboOverviewService.php index 8076b008..dc798389 100644 --- a/app/Services/Abo/TeamAboOverviewService.php +++ b/app/Services/Abo/TeamAboOverviewService.php @@ -57,7 +57,7 @@ class TeamAboOverviewService */ public function teamKpis(User $user, int $month, int $year): array { - $teamUserIds = $this->dashboardService->teamUserIds($user->id); + $teamUserIds = $this->dashboardService->activeTeamUserIds($user->id); if ($teamUserIds === []) { return [ @@ -116,7 +116,7 @@ class TeamAboOverviewService */ public function monthlyGrowth(User $user, int $year): array { - $teamUserIds = $this->dashboardService->teamUserIds($user->id); + $teamUserIds = $this->dashboardService->activeTeamUserIds($user->id); $monthNames = \App\Services\HTMLHelper::getTransMonths(); $ownByMonth = $teamUserIds === [] ? [] : $this->newAbosByMonth($teamUserIds, 'user_id', 'me', $year); @@ -250,6 +250,11 @@ class TeamAboOverviewService */ private function buildLineRow(int $line, Collection $users, int $month, int $year): array { + // Nur aktive Berater zählen/anzeigen (konsistent mit der Backoffice-Statistik). + // Die Baum-Traversierung in lineBuckets() bleibt unberührt, damit auch die + // Downline inaktiver Zwischen-Berater weiterhin erreicht wird. + $users = $users->filter(fn (User $teamUser) => $this->dashboardService->isActiveConsultant($teamUser))->values(); + $userIds = $users->pluck('id')->map(fn ($id) => (int) $id)->all(); if ($userIds === []) { diff --git a/app/Services/Backoffice/BackofficeDashboardService.php b/app/Services/Backoffice/BackofficeDashboardService.php index 96a9312c..ab3d1bbd 100644 --- a/app/Services/Backoffice/BackofficeDashboardService.php +++ b/app/Services/Backoffice/BackofficeDashboardService.php @@ -175,6 +175,31 @@ class BackofficeDashboardService ->all(); } + /** + * IDs der gesamten Downline, aber nur aktive Berater + * (gleiche Definition wie die "Berater"-Spalte der Backoffice-Statistik). + * + * @return int[] + */ + public function activeTeamUserIds(int $rootUserId): array + { + return collect($this->lineBuckets($rootUserId)) + ->flatMap(fn (Collection $users) => $users) + ->filter(fn (User $user) => $this->isActiveConsultant($user)) + ->map(fn (User $user) => (int) $user->id) + ->values() + ->all(); + } + + /** + * Ein aktiver Berater hat eine Mitgliedsebene und ein gesetztes Abrechnungskonto. + * Maßgeblich für die "Berater"-Zählung in den Team-Übersichten. + */ + public function isActiveConsultant(User $user): bool + { + return $user->m_level !== null && $user->payment_account !== null; + } + /** * @param \Illuminate\Support\Collection $users * @return array @@ -261,7 +286,7 @@ class BackofficeDashboardService private function activeConsultants(Collection $users): int { return $users - ->filter(fn (User $user) => $user->m_level !== null && $user->payment_account !== null) + ->filter(fn (User $user) => $this->isActiveConsultant($user)) ->count(); } @@ -324,12 +349,12 @@ class BackofficeDashboardService ->whereIn('user_id', $userIds) ->where('month', $month) ->where('year', $year) - ->selectRaw('COALESCE(SUM(month_KP_points), 0) as own_points') - ->selectRaw('COALESCE(SUM(month_shop_points), 0) as external_points') - ->selectRaw('COALESCE(SUM(CASE WHEN shopping_orders.is_abo = 1 THEN month_shop_points ELSE 0 END), 0) as customer_abo_points') - ->selectRaw('COALESCE(SUM(CASE WHEN shopping_orders.id IS NOT NULL AND (shopping_orders.is_abo = 0 OR shopping_orders.is_abo IS NULL) THEN month_shop_points ELSE 0 END), 0) as customer_single_order_points') - ->selectRaw('COALESCE(SUM(CASE WHEN shopping_orders.id IS NULL THEN month_shop_points ELSE 0 END), 0) as customer_other_points') - ->selectRaw('COALESCE(SUM(month_total_net), 0) + COALESCE(SUM(month_shop_total_net), 0) as turnover_net') + ->selectRaw(SalesVolumePoints::ownPoints().' as own_points') + ->selectRaw(SalesVolumePoints::externalPoints().' as external_points') + ->selectRaw(SalesVolumePoints::customerAboPoints().' as customer_abo_points') + ->selectRaw(SalesVolumePoints::customerSingleOrderPoints().' as customer_single_order_points') + ->selectRaw(SalesVolumePoints::customerOtherPoints().' as customer_other_points') + ->selectRaw(SalesVolumePoints::turnoverNet().' as turnover_net') ->first(); $ownPoints = (float) ($summary->own_points ?? 0); @@ -382,9 +407,9 @@ class BackofficeDashboardService ->where('month', $month) ->where('year', $year) ->select('user_id') - ->selectRaw('COALESCE(SUM(month_KP_points), 0) + COALESCE(SUM(month_shop_points), 0) as total_points') + ->selectRaw(SalesVolumePoints::totalPoints().' as total_points') ->groupBy('user_id') - ->havingRaw('COALESCE(SUM(month_KP_points), 0) + COALESCE(SUM(month_shop_points), 0) >= 1000') + ->havingRaw(SalesVolumePoints::totalPoints().' >= 1000') ->get() ->count(); } diff --git a/tests/Feature/TeamAboOverviewTest.php b/tests/Feature/TeamAboOverviewTest.php index ac3e64c3..cfc093d4 100644 --- a/tests/Feature/TeamAboOverviewTest.php +++ b/tests/Feature/TeamAboOverviewTest.php @@ -123,6 +123,38 @@ it('gruppiert Team-Abos nach Ebenen mit Eigen- und Kundenabos', function (): voi ->and($totals['total_abos'])->toBe(5); }); +it('zählt nur aktive Berater und blendet inaktive/fehlerhafte aus', function (): void { + $root = makeUser(); + + // Aktiver Berater (m_level + gültiges payment_account) + $anna = makeUser($root->id); + + // Inaktiv: kein payment_account + $noPayment = makeUser($root->id); + $noPayment->forceFill(['payment_account' => null])->save(); + + // Inaktiv: keine Mitgliedsebene + $noLevel = makeUser($root->id); + $noLevel->forceFill(['m_level' => null])->save(); + + // Abos der inaktiven Berater dürfen nicht in die Übersicht einfließen + makeAbo($anna->id, $anna->id, 'me', '2026-06-05', 50); + makeAbo($noPayment->id, $noPayment->id, 'me', '2026-06-05', 10); + makeAbo($noLevel->id, $noLevel->id, 'ot', '2026-06-05', 10); + + $service = app(TeamAboOverviewService::class); + $overview = $service->linesOverview($root, 6, 2026); + + $line1 = $overview['lines'][1]; + + expect($line1['consultants'])->toBe(1) + ->and($line1['members'])->toHaveCount(1) + ->and($line1['members'][0]['user_id'])->toBe($anna->id) + ->and($line1['own_abos'])->toBe(1) + ->and($line1['customer_abos'])->toBe(0) + ->and($overview['totals']['consultants'])->toBe(1); +}); + it('liefert pro Mitglied das Eigen-Abo-Flag und Punkte', function (): void { $root = makeUser(); $anna = makeUser($root->id); @@ -229,6 +261,7 @@ it('liefert leichtgewichtige Dashboard-KPIs inkl. 1000-Punkte-Shop', function () 'year' => 2026, 'month_KP_points' => 600, 'month_shop_points' => 500, + 'points' => 1100, 'status' => 1, ]);