hasColumn('products', 'slug')) { Schema::connection('sqlite')->table('products', function ($table) { $table->string('slug')->nullable(); }); } }); function makeUser(?int $sponsorId = null): User { $user = User::forceCreate([ 'email' => 'u-'.uniqid('', true).'@example.com', 'password' => bcrypt('secret'), 'lang' => 'de', 'm_level' => 1, 'm_sponsor' => $sponsorId, 'payment_account' => now()->addYear(), ]); return $user; } function makeAbo(int $userId, int $memberId, string $isFor, string $startDate, int $points = 0): UserAbo { $abo = UserAbo::create([ 'user_id' => $userId, 'member_id' => $memberId, 'shopping_user_id' => 0, 'email' => 'a-'.uniqid('', true).'@example.com', 'is_for' => $isFor, 'payone_userid' => 0, 'clearingtype' => 'cc', 'amount' => 5000, 'active' => true, 'status' => 2, 'abo_interval' => 5, 'start_date' => $startDate, 'last_date' => $startDate, 'next_date' => '2026-12-10', ]); if ($points > 0) { $product = Product::forceCreate([ 'name' => 'P-'.uniqid('', true), 'title' => 'Test', 'price' => 10.0, 'tax' => 19.0, 'active' => true, 'pos' => 1, 'weight' => 100, 'points' => $points, 'slug' => 'p-'.uniqid('', true), ]); UserAboItem::create([ 'user_abo_id' => $abo->id, 'product_id' => $product->id, 'comp' => 0, 'qty' => 1, 'status' => 1, ]); } return $abo->fresh(); } it('gruppiert Team-Abos nach Ebenen mit Eigen- und Kundenabos', function (): void { $root = makeUser(); // 1. Ebene: zwei direkte Partner $anna = makeUser($root->id); $lena = makeUser($root->id); // 2. Ebene: ein Partner unter Anna $lisa = makeUser($anna->id); // Anna hat ein Eigen-Abo (50 Punkte) und 2 Kundenabos makeAbo($anna->id, $anna->id, 'me', '2026-06-05', 50); makeAbo($anna->id, $anna->id, 'ot', '2026-06-10', 20); makeAbo($anna->id, $anna->id, 'ot', '2026-05-10', 30); // Lena hat kein Eigen-Abo, aber 1 Kundenabo makeAbo($lena->id, $lena->id, 'ot', '2026-06-12', 15); // Lisa (Ebene 2) hat ein Eigen-Abo makeAbo($lisa->id, $lisa->id, 'me', '2026-06-01', 40); $service = app(TeamAboOverviewService::class); $overview = $service->linesOverview($root, 6, 2026); expect($overview['lines'])->toHaveKey(1) ->and($overview['lines'])->toHaveKey(2); $line1 = $overview['lines'][1]; expect($line1['consultants'])->toBe(2) ->and($line1['members_with_own_abo'])->toBe(1) // nur Anna ->and($line1['own_abos'])->toBe(1) ->and($line1['customer_abos'])->toBe(3) // Anna 2 + Lena 1 ->and($line1['customer_abos_new'])->toBe(2) // im Juni: Anna 1 + Lena 1 (Mai zählt nicht) ->and($line1['own_abos_new'])->toBe(1); $line2 = $overview['lines'][2]; expect($line2['consultants'])->toBe(1) ->and($line2['own_abos'])->toBe(1) ->and($line2['customer_abos'])->toBe(0); $totals = $overview['totals']; expect($totals['own_abos'])->toBe(2) ->and($totals['customer_abos'])->toBe(3) ->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); makeAbo($anna->id, $anna->id, 'me', '2026-06-05', 50); makeAbo($anna->id, $anna->id, 'ot', '2026-06-10', 20); $service = app(TeamAboOverviewService::class); $overview = $service->linesOverview($root, 6, 2026); $member = collect($overview['lines'][1]['members'])->firstWhere('user_id', $anna->id); expect($member['has_own_abo'])->toBeTrue() ->and($member['own_abo_points'])->toBe(50.0) ->and($member['customer_abo_count'])->toBe(1) ->and($member['customer_abo_points'])->toBe(20.0) ->and($member['total_abos'])->toBe(2); }); it('rendert die Team-Abo-Linienansicht mit Ebenen und Summen', function (): void { URL::resolveMissingNamedRoutesUsing(fn (string $name, array $parameters) => '/'.$name.'/'.implode('/', $parameters)); $root = makeUser(); $anna = makeUser($root->id); makeAbo($anna->id, $anna->id, 'me', '2026-06-05', 50); makeAbo($anna->id, $anna->id, 'ot', '2026-06-10', 20); $this->actingAs($root); $service = app(TeamAboOverviewService::class); $overview = $service->linesOverview($root, 6, 2026); $growth = $service->monthlyGrowth($root, 2026); $html = view('user.team.abos', [ 'currentMonthName' => HTMLHelper::getTransMonths()[6], 'overview' => $overview, 'growth' => $growth, 'growthChart' => [ 'labels' => array_map(fn ($r) => $r['label'], $growth['months']), 'own' => array_map(fn ($r) => $r['own_new'], $growth['months']), 'customer' => array_map(fn ($r) => $r['customer_new'], $growth['months']), 'step' => 1, 'suggested_max' => 5, ], 'growthYear' => 2026, 'filterYears' => HTMLHelper::getYearRange(2022), ])->render(); expect($html) ->toContain(__('abo.team_by_level')) ->toContain(__('abo.team_total_abos')) ->toContain(__('abo.team_growth')) ->toContain('aboGrowthChart') ->toContain($anna->email); }); it('berechnet das monatliche Abo-Wachstum je Jahr', function (): void { $root = makeUser(); $anna = makeUser($root->id); $lena = makeUser($root->id); // Juni 2026: 1 Eigen-Abo + 2 Kundenabos makeAbo($anna->id, $anna->id, 'me', '2026-06-05'); makeAbo($anna->id, $anna->id, 'ot', '2026-06-10'); makeAbo($lena->id, $lena->id, 'ot', '2026-06-20'); // März 2026: 1 Kundenabo makeAbo($anna->id, $anna->id, 'ot', '2026-03-15'); // Anderes Jahr: zählt nicht makeAbo($anna->id, $anna->id, 'ot', '2025-06-01'); $growth = app(TeamAboOverviewService::class)->monthlyGrowth($root, 2026); expect($growth['year'])->toBe(2026) ->and($growth['months'])->toHaveCount(12); $june = collect($growth['months'])->firstWhere('month', 6); $march = collect($growth['months'])->firstWhere('month', 3); expect($june['own_new'])->toBe(1) ->and($june['customer_new'])->toBe(2) ->and($june['total_new'])->toBe(3) ->and($march['customer_new'])->toBe(1) ->and($march['own_new'])->toBe(0); expect($growth['totals']['own_new'])->toBe(1) ->and($growth['totals']['customer_new'])->toBe(3) ->and($growth['totals']['total_new'])->toBe(4); }); it('liefert leichtgewichtige Dashboard-KPIs inkl. 1000-Punkte-Shop', function (): void { $root = makeUser(); $anna = makeUser($root->id); $lena = makeUser($root->id); // Abos makeAbo($anna->id, $anna->id, 'me', '2026-06-05'); // 1 Eigen-Abo makeAbo($anna->id, $anna->id, 'ot', '2026-06-10'); // 1 Kundenabo makeAbo($lena->id, $lena->id, 'ot', '2026-06-12'); // 1 Kundenabo // Anna erreicht 1000 Punkte Kundenumsatz im Juni 2026 UserSalesVolume::create([ 'user_id' => $anna->id, 'month' => 6, 'year' => 2026, 'month_KP_points' => 600, 'month_shop_points' => 500, 'points' => 1100, 'status' => 1, ]); $kpis = app(TeamAboOverviewService::class)->teamKpis($root, 6, 2026); expect($kpis['team_partner_abos'])->toBe(1) ->and($kpis['team_customer_abos'])->toBe(2) ->and($kpis['team_total_abos'])->toBe(3) ->and($kpis['shop_1000'])->toBe(1); }); it('liefert die Abo-Zusammenfassung eines Team-Mitglieds inkl. Ausführungen', function (): void { $root = makeUser(); $anna = makeUser($root->id); $ownAbo = makeAbo($anna->id, $anna->id, 'me', '2026-06-05', 50); makeAbo($anna->id, $anna->id, 'ot', '2026-06-10', 20); makeAbo($anna->id, $anna->id, 'ot', '2026-05-01', 30); // Zwei ausgeführte (bezahlte) Lieferungen für das Eigen-Abo UserAboOrder::create(['user_abo_id' => $ownAbo->id, 'shopping_order_id' => 0, 'status' => 2, 'paid' => true]); UserAboOrder::create(['user_abo_id' => $ownAbo->id, 'shopping_order_id' => 0, 'status' => 2, 'paid' => true]); $summary = app(TeamAboOverviewService::class)->memberAboSummary($root, $anna->id); expect($summary)->not->toBeNull() ->and($summary['email'])->toBe($anna->email) ->and($summary['own_abo'])->not->toBeNull() ->and($summary['own_abo']['points'])->toBe(50.0) ->and($summary['own_abo']['executions'])->toBe(2) ->and($summary['customer_abos']['count'])->toBe(2) ->and($summary['customer_abos']['total_points'])->toBe(50.0) ->and($summary['customer_abos']['items'])->toHaveCount(2); }); it('liefert null, wenn das Mitglied nicht im Team liegt', function (): void { $root = makeUser(); $stranger = makeUser(); // kein Sponsor → nicht in der Downline $summary = app(TeamAboOverviewService::class)->memberAboSummary($root, $stranger->id); expect($summary)->toBeNull(); });