'hms-'.uniqid('', true).'@example.com', 'password' => bcrypt('secret'), 'lang' => 'de', 'm_level' => 1, 'm_sponsor' => $sponsorId, 'active_date' => '2026-01-10', 'partner_paid_at' => '2026-01-10', 'payment_account' => $paymentAccount === 'future' ? now()->addYear() : $paymentAccount, ]); } function hmsMonthlyStats(User $user, int $month = 8, int $year = 2026): array { Request::merge(['stats_month' => $month, 'stats_year' => $year]); $method = (new ReflectionClass(HomeController::class))->getMethod('buildMonthlyStats'); $method->setAccessible(true); return $method->invoke(new HomeController, $user); } it('zaehlt in der Firstlines-Kachel nur aktive Berater, konsistent zu "Teampartner gesamt"', function (): void { $root = hmsMakeUser(); hmsMakeUser($root->id); hmsMakeUser($root->id); // Abgelaufenes, aber gesetztes Abrechnungskonto: zaehlt wie in der // "Berater"-Spalte weiterhin als Berater (BackofficeDashboardService::isActiveConsultant). hmsMakeUser($root->id, now()->subMonth()->toDateString()); // Gar kein Abrechnungskonto: kein Berater hmsMakeUser($root->id, null); $stats = hmsMonthlyStats($root); // Beide Kacheln stehen im selben Cluster und muessen dieselbe Basis nutzen. expect($stats['firstline_count'])->toBe(3) ->and($stats['team_partner_count'])->toBe(3); }); it('zaehlt Neupartner weiterhin ueber alle Firstlines, nicht nur ueber die Kachel-Menge', function (): void { $root = hmsMakeUser(); // Im laufenden Monat aktiv geworden, Konto gueltig -> Neupartner hmsMakeUser($root->id)->forceFill(['active_date' => '2026-08-05', 'partner_paid_at' => '2026-08-05'])->save(); // Im laufenden Monat aktiv geworden, Konto abgelaufen -> kein Neupartner, // aber weiterhin Berater in der Firstlines-Kachel hmsMakeUser($root->id, now()->subDay()->toDateString()) ->forceFill(['active_date' => '2026-08-06', 'partner_paid_at' => '2026-08-06'])->save(); // Ohne Abrechnungskonto: weder Neupartner noch Berater hmsMakeUser($root->id, null)->forceFill(['active_date' => '2026-08-07', 'partner_paid_at' => '2026-08-07'])->save(); $stats = hmsMonthlyStats($root); expect($stats['direct_new_partners'])->toBe(1) ->and($stats['firstline_count'])->toBe(2); }); /** * Legt ein Karriere-Level an und haengt den User daran. Ohne Monatsqualifikation * ist genau das die Ebenenzahl, mit der die beiden Payline-Kacheln rechnen. */ function hmsMakeLevel(int $pos, int $paylines, string $name = 'Testlevel'): UserLevel { return UserLevel::forceCreate([ 'name' => $name.' '.$pos, 'qual_kp' => 150, 'qual_pp' => 0, 'paylines' => $paylines, 'pos' => $pos, 'active' => true, ]); } it('zeigt die Payline-Kacheln am Monatsanfang nach dem Karriere-Level, statt auf 0 zu fallen', function (): void { // Ohne Monatsquali liefert der Live-Lauf paylines = 0. Die beiden // Struktur-Kacheln muessen dann auf die Ebenen des Karriere-Levels // zurueckfallen — die Teamstruktur aendert sich zum Monatswechsel nicht. $level = hmsMakeLevel(pos: 3, paylines: 2); $root = hmsMakeUser(); $root->forceFill(['m_level' => $level->id])->save(); $line1 = hmsMakeUser($root->id); $line2 = hmsMakeUser($line1->id); // Ebene 3 liegt ausserhalb der zwei bezahlten Ebenen des Karriere-Levels. hmsMakeUser($line2->id); $stats = hmsMonthlyStats($root->fresh()); expect($stats['payline_is_career_fallback'])->toBeTrue() ->and($stats['payline_member_count'])->toBe(2) ->and($stats['team_partner_count'])->toBe(3); }); it('haelt eine leere Payline nicht im 6-h-Cache fest', function (): void { // Ohne Karriere-Level bleibt die Tiefe 0. Das darf NICHT gecacht werden: // der Zustand loest sich im Monatsverlauf von selbst auf. $root = hmsMakeUser(); hmsMakeUser($root->id); $service = app(PaylineForecastService::class); $context = $service->paylineContext($root, 8, 2026); expect($context['paylines'])->toBe(0) ->and($context['member_ids'])->toBe([]) ->and(Cache::has("payline_context_{$root->id}_8_2026"))->toBeFalse(); }); it('cacht den Payline-Kontext, sobald eine Ebenenzahl feststeht', function (): void { $level = hmsMakeLevel(pos: 3, paylines: 2); $root = hmsMakeUser(); $root->forceFill(['m_level' => $level->id])->save(); hmsMakeUser($root->id); $service = app(PaylineForecastService::class); $context = $service->paylineContext($root->fresh(), 8, 2026); expect($context['paylines'])->toBe(2) ->and(Cache::has("payline_context_{$root->id}_8_2026"))->toBeTrue(); }); it('blendet den Level-Block ohne Monatsqualifikation nicht aus, sondern zeigt die fehlenden Punkte', function (): void { // setFirstQualLevel() liefert das Level auf pos 1 als next_level mit — // daraus muss "dir fehlen noch X Punkte persoenliches Volumen" entstehen. hmsMakeLevel(pos: 1, paylines: 3, name: 'Junior Berater'); $root = hmsMakeUser(); $root->forceFill(['m_level' => UserLevel::where('pos', 1)->value('id')])->save(); $level = app(LiveCommissionService::class) ->levelProgress($root->fresh(), 8, 2026); expect($level)->not->toBeNull() ->and($level['is_qualified'])->toBeFalse() ->and($level['name'])->toBe('') ->and($level['next_name'])->toBe('Junior Berater 1') ->and($level['missing_personal'])->toBe(150.0); }); it('rechnet Bestand und "+N neu" der Payline-Kachel nach derselben Regel', function (): void { // Regression: der Bestand kam aus der Monatsend-Rueckrechnung, das Badge aus // activeAbo(). In derselben Kachel standen damit zwei Zaehlweisen. $level = hmsMakeLevel(pos: 3, paylines: 2); $root = hmsMakeUser(); $root->forceFill(['m_level' => $level->id])->save(); $anna = hmsMakeUser($root->id); $abo = UserAbo::create([ 'user_id' => $anna->id, 'member_id' => $anna->id, 'shopping_user_id' => 0, 'email' => 'hms-abo@example.com', 'is_for' => 'ot', 'payone_userid' => 0, 'clearingtype' => 'cc', 'amount' => 5000, 'active' => false, 'status' => 2, 'abo_interval' => 5, 'start_date' => '2026-07-10', 'next_date' => '2026-08-25', ]); // Erst im August beendet -> zaehlt im Juli sowohl im Bestand als auch als "neu". $abo->update(['cancel_date' => '2026-08-05']); $stats = hmsMonthlyStats($root->fresh(), month: 7, year: 2026); expect($stats['payline_abo_count'])->toBe(1) ->and($stats['payline_abos_new'])->toBe(1); });