hasColumn('products', 'slug')) { Schema::connection('sqlite')->table('products', function ($table) { $table->string('slug')->nullable(); }); } }); function bslMakeLevel(string $name, int $pos): UserLevel { return UserLevel::forceCreate([ 'name' => $name, 'pos' => $pos, 'active' => 1, 'default' => 0, ]); } function bslMakeUser(?int $sponsorId = null, ?int $levelId = null, ?string $activeDate = null): User { return User::forceCreate([ 'email' => 'bsl-'.uniqid('', true).'@example.com', 'password' => bcrypt('secret'), 'lang' => 'de', 'm_level' => $levelId ?? 1, 'm_sponsor' => $sponsorId, 'active_date' => $activeDate ?? '2026-01-15', 'partner_paid_at' => $activeDate ?? '2026-01-15', 'payment_account' => now()->addYear(), ]); } function bslMakeAbo(int $userId, string $isFor, string $startDate, int $status = 2): UserAbo { return UserAbo::create([ 'user_id' => $userId, 'member_id' => $userId, 'shopping_user_id' => 0, 'email' => 'bsl-abo-'.uniqid('', true).'@example.com', 'is_for' => $isFor, 'payone_userid' => 0, 'clearingtype' => 'cc', 'amount' => 5000, 'active' => true, 'status' => $status, 'abo_interval' => 5, 'start_date' => $startDate, 'last_date' => $startDate, 'next_date' => '2026-12-10', ]); } function bslMakeAboWithPoints(int $userId, string $isFor, string $startDate, int $points): UserAbo { $abo = bslMakeAbo($userId, $isFor, $startDate); $product = Product::forceCreate([ 'name' => 'BSL-'.uniqid('', true), 'title' => 'Test', 'price' => 10.0, 'tax' => 19.0, 'active' => true, 'pos' => 1, 'weight' => 100, 'points' => $points, 'slug' => 'bsl-'.uniqid('', true), ]); UserAboItem::create([ 'user_abo_id' => $abo->id, 'product_id' => $product->id, 'comp' => 0, 'qty' => 1, 'status' => 1, ]); return $abo->fresh(); } it('rankt neue Kundenabos pro Berater absteigend inklusive Linie', function (): void { $root = bslMakeUser(); $anna = bslMakeUser($root->id); $ben = bslMakeUser($anna->id); bslMakeAbo($anna->id, 'ot', '2026-06-10'); bslMakeAbo($ben->id, 'ot', '2026-06-05'); bslMakeAbo($ben->id, 'ot', '2026-06-20'); bslMakeAbo($ben->id, 'ot', '2026-05-10'); // anderer Monat, zaehlt nicht bslMakeAbo($anna->id, 'me', '2026-06-12'); // Eigenabo, zaehlt nicht $overview = app(BackofficeDashboardService::class)->overview($root, 6, 2026); $ranking = $overview['new_customer_abos']; expect($ranking)->toHaveCount(2) ->and($ranking[0]['user_id'])->toBe($ben->id) ->and($ranking[0]['abo_count'])->toBe(2) ->and($ranking[0]['total_abo_count'])->toBe(3) // inkl. Mai-Abo ->and($ranking[0]['line'])->toBe(2) ->and($ranking[1]['user_id'])->toBe($anna->id) ->and($ranking[1]['abo_count'])->toBe(1) ->and($ranking[1]['total_abo_count'])->toBe(1) ->and($ranking[1]['line'])->toBe(1); }); it('zaehlt Fuehrungskraefte-Abos erst ab Stufe Vertriebsleiter inklusive Linie und Summe', function (): void { $berater = bslMakeLevel('Berater', 3); $vertriebsleiter = bslMakeLevel('Vertriebsleiter', 6); $bronze = bslMakeLevel('Bronze Member', 7); $root = bslMakeUser(); $anna = bslMakeUser($root->id, $vertriebsleiter->id); $ben = bslMakeUser($root->id, $berater->id); $carla = bslMakeUser($anna->id, $bronze->id); bslMakeAbo($anna->id, 'me', '2026-06-01'); bslMakeAbo($anna->id, 'me', '2026-04-01'); bslMakeAbo($anna->id, 'me', '2026-03-01', 4); // storniert, zaehlt nicht bslMakeAbo($anna->id, 'ot', '2026-06-01'); // Kundenabo von Anna bslMakeAbo($carla->id, 'ot', '2026-05-01'); // Kundenabo von Carla bslMakeAbo($ben->id, 'me', '2026-06-01'); // unterhalb Vertriebsleiter $overview = app(BackofficeDashboardService::class)->overview($root, 6, 2026); $leaderAbos = $overview['leader_abos']; expect($leaderAbos['total'])->toBe(4) ->and($leaderAbos['total_own'])->toBe(2) ->and($leaderAbos['total_customer'])->toBe(2) ->and($leaderAbos['rows'])->toHaveCount(2) ->and($leaderAbos['rows'][0]['user_id'])->toBe($anna->id) ->and($leaderAbos['rows'][0]['own_abo_count'])->toBe(2) ->and($leaderAbos['rows'][0]['customer_abo_count'])->toBe(1) ->and($leaderAbos['rows'][0]['line'])->toBe(1) ->and($leaderAbos['rows'][0]['career_level'])->toBe('Vertriebsleiter') ->and($leaderAbos['rows'][1]['user_id'])->toBe($carla->id) ->and($leaderAbos['rows'][1]['own_abo_count'])->toBe(0) ->and($leaderAbos['rows'][1]['customer_abo_count'])->toBe(1) ->and($leaderAbos['rows'][1]['line'])->toBe(2); }); it('liefert eine leere Fuehrungskraefte-Liste, wenn die Stufe Vertriebsleiter nicht existiert', function (): void { $root = bslMakeUser(); bslMakeUser($root->id); $overview = app(BackofficeDashboardService::class)->overview($root, 6, 2026); expect($overview['leader_abos']['total'])->toBe(0) ->and($overview['leader_abos']['rows'])->toBe([]); }); it('ergaenzt die Fuehrungskraefte-Liste um Teamzahlen mit Abschneiden an der naechsten Fuehrungskraft', function (): void { $berater = bslMakeLevel('Berater', 3); $vertriebsleiter = bslMakeLevel('Vertriebsleiter', 6); $root = bslMakeUser(); $leaderA = bslMakeUser($root->id, $vertriebsleiter->id); $member1 = bslMakeUser($leaderA->id, $berater->id); $leaderB = bslMakeUser($member1->id, $vertriebsleiter->id); $member2 = bslMakeUser($leaderB->id, $berater->id); bslMakeAbo($leaderA->id, 'me', '2026-06-01'); // eigenes Abo Leader A bslMakeAbo($member1->id, 'me', '2026-06-01'); // Teamabo von A bslMakeAbo($member1->id, 'ot', '2026-06-01'); // Team-Kundenabo von A bslMakeAbo($leaderB->id, 'ot', '2026-06-01'); // eigenes Kundenabo Leader B bslMakeAbo($member2->id, 'me', '2026-06-01'); // Teamabo von B, zaehlt NICHT bei A $overview = app(BackofficeDashboardService::class)->overview($root, 6, 2026); $leaderAbos = $overview['leader_abos']; $rows = collect($leaderAbos['rows'])->keyBy('user_id'); $a = $rows->get($leaderA->id); $b = $rows->get($leaderB->id); expect($a['own_abo_count'])->toBe(1) ->and($a['customer_abo_count'])->toBe(0) ->and($a['team_abo_count'])->toBe(1) ->and($a['team_customer_abo_count'])->toBe(1) ->and($a['total_abo_count'])->toBe(3) ->and($b['own_abo_count'])->toBe(0) ->and($b['customer_abo_count'])->toBe(1) ->and($b['team_abo_count'])->toBe(1) ->and($b['team_customer_abo_count'])->toBe(0) ->and($b['total_abo_count'])->toBe(2); expect($leaderAbos['total_own'])->toBe(1) ->and($leaderAbos['total_customer'])->toBe(1) ->and($leaderAbos['total_team'])->toBe(2) ->and($leaderAbos['total_team_customer'])->toBe(1) ->and($leaderAbos['total'])->toBe(5); // Sortierung nach Gesamtsumme: Leader A (3) vor Leader B (2) expect($leaderAbos['rows'][0]['user_id'])->toBe($leaderA->id); }); it('liefert Top-10-Kundenabo-Ranglisten nach Anzahl und Umsatz mit Soll und Ist', function (): void { $root = bslMakeUser(); $anna = bslMakeUser($root->id); $lena = bslMakeUser($root->id); bslMakeAboWithPoints($anna->id, 'ot', '2026-06-01', 20); bslMakeAboWithPoints($anna->id, 'ot', '2026-05-01', 30); bslMakeAboWithPoints($lena->id, 'ot', '2026-04-01', 80); bslMakeAbo($lena->id, 'me', '2026-06-01'); // Eigenabo zaehlt nicht // Ist-Umsatz: im Juni abgerechnete Kundenabo-Punkte fuer Lena $order = ShoppingOrder::forceCreate([ 'shopping_user_id' => 0, 'auth_user_id' => $lena->id, 'country_id' => 0, 'user_shop_id' => 0, 'is_abo' => true, 'paid' => true, ]); UserSalesVolume::forceCreate([ 'user_id' => $lena->id, 'shopping_order_id' => $order->id, 'month' => 6, 'year' => 2026, 'points' => 80, 'status' => 2, ]); $overview = app(BackofficeDashboardService::class)->overview($root, 6, 2026); $top = $overview['top_customer_abos']; expect($top['by_count'])->toHaveCount(2) ->and($top['by_count'][0]['user_id'])->toBe($anna->id) ->and($top['by_count'][0]['abo_count'])->toBe(2) ->and($top['by_count'][1]['user_id'])->toBe($lena->id) ->and($top['by_count'][1]['abo_count'])->toBe(1); expect($top['by_points'][0]['user_id'])->toBe($lena->id) ->and($top['by_points'][0]['points_target'])->toBe(80.0) ->and($top['by_points'][0]['points_actual'])->toBe(80.0) ->and($top['by_points'][1]['user_id'])->toBe($anna->id) ->and($top['by_points'][1]['points_target'])->toBe(50.0) ->and($top['by_points'][1]['points_actual'])->toBe(0.0); }); it('haelt die Abo-Bestaende eines abgeschlossenen Monats fest', function (): void { // Kundenentscheidung 2026-09-10: Ein abgeschlossener Monat ist abgerechnet und // wird nicht mehr aus heutigen Daten nachgerechnet. Vorher wurden die // Bestandszahlen hier bewusst live ueberlagert ("Abo-Bestaende tagesaktuell"); // dieser Test hielt die alte Regel fest und beschreibt jetzt die neue. $root = bslMakeUser(); $anna = bslMakeUser($root->id); $abo = bslMakeAbo($anna->id, 'me', '2026-05-15'); $service = app(BackofficeDashboardService::class); $service->storeSnapshot($root, 5, 2026); // Nach dem Snapshot: Teamabo gekuendigt, neues Kundenabo dazugekommen $abo->update(['status' => 4, 'active' => false]); bslMakeAbo($anna->id, 'ot', '2026-07-01'); $overview = $service->overview($root, 5, 2026); expect($overview['_meta']['source'])->toBe('snapshot') ->and($overview)->not->toHaveKey('abo_stock_live') ->and($overview['lines'][1]['team_partner_abos'])->toBe(1) // Stand Mai ->and($overview['lines'][1]['team_customer_abos'])->toBe(0) // Stand Mai ->and($overview['lines'][1]['team_partner_abos_new'])->toBe(1) ->and($overview['totals']['team_partner_abos'])->toBe(1) ->and($overview['totals']['team_customer_abos'])->toBe(0); }); it('nimmt spaeter entstandene Linien nicht mehr in einen abgeschlossenen Monat auf', function (): void { // Genau der Fall, der die Entscheidung ausgeloest hat: Teampartner koennen // dazukommen oder verschoben werden. Ein abgerechneter Monat darf sich dadurch // nicht mehr aendern. $root = bslMakeUser(); $anna = bslMakeUser($root->id); bslMakeAbo($anna->id, 'me', '2026-05-15'); $service = app(BackofficeDashboardService::class); $service->storeSnapshot($root, 5, 2026); // Nach dem Snapshot: Team wird tiefer — Linie 2 entsteht und bringt Abo-Bestand mit $ben = bslMakeUser($anna->id); bslMakeAbo($ben->id, 'me', '2026-07-01'); bslMakeAbo($ben->id, 'ot', '2026-07-02'); $service->flushLineBucketsMemo(); // Struktur hat sich im selben Prozess geändert $overview = $service->overview($root, 5, 2026); expect($overview['_meta']['source'])->toBe('snapshot') ->and($overview['lines'])->not->toHaveKey(2) ->and($overview['totals']['team_partner_abos'])->toBe(1) ->and($overview['totals']['team_customer_abos'])->toBe(0); }); it('nimmt den Statistik-Nutzer selbst in die Kundenabo-Ranglisten auf', function (): void { $root = bslMakeUser(); $anna = bslMakeUser($root->id); bslMakeAboWithPoints($root->id, 'ot', '2026-06-10', 25); bslMakeAboWithPoints($anna->id, 'ot', '2026-06-05', 15); $overview = app(BackofficeDashboardService::class)->overview($root, 6, 2026); $newRanking = collect($overview['new_customer_abos'])->keyBy('user_id'); expect($newRanking->has($root->id))->toBeTrue() ->and($newRanking->get($root->id)['is_self'])->toBeTrue() ->and($newRanking->get($root->id)['line'])->toBe(0) ->and($newRanking->get($anna->id)['is_self'])->toBeFalse(); $byCount = collect($overview['top_customer_abos']['by_count'])->keyBy('user_id'); expect($byCount->has($root->id))->toBeTrue() ->and($byCount->get($root->id)['is_self'])->toBeTrue(); $byPoints = collect($overview['top_customer_abos']['by_points'])->keyBy('user_id'); expect($byPoints->has($root->id))->toBeTrue() ->and($byPoints->get($root->id)['points_target'])->toBe(25.0); }); it('sortiert die Top-10-Umsatzliste nach abgerechneten Ist-Punkten und liefert die Abo-Anzahl mit', function (): void { $root = bslMakeUser(); $anna = bslMakeUser($root->id); // hoher Bestand, nichts abgerechnet $lena = bslMakeUser($root->id); // kleiner Bestand, viel abgerechnet bslMakeAboWithPoints($anna->id, 'ot', '2026-06-01', 100); bslMakeAboWithPoints($lena->id, 'ot', '2026-06-01', 30); bslMakeAboWithPoints($lena->id, 'ot', '2026-06-02', 30); $order = ShoppingOrder::forceCreate([ 'shopping_user_id' => 0, 'auth_user_id' => $lena->id, 'country_id' => 0, 'user_shop_id' => 0, 'is_abo' => true, 'paid' => true, ]); UserSalesVolume::forceCreate([ 'user_id' => $lena->id, 'shopping_order_id' => $order->id, 'month' => 6, 'year' => 2026, 'points' => 90, 'status' => 2, ]); $byPoints = app(BackofficeDashboardService::class)->overview($root, 6, 2026)['top_customer_abos']['by_points']; expect($byPoints[0]['user_id'])->toBe($lena->id) ->and($byPoints[0]['points_actual'])->toBe(90.0) ->and($byPoints[0]['points_target'])->toBe(60.0) ->and($byPoints[0]['abo_count'])->toBe(2) ->and($byPoints[1]['user_id'])->toBe($anna->id) ->and($byPoints[1]['points_target'])->toBe(100.0) ->and($byPoints[1]['abo_count'])->toBe(1); }); it('liefert je Fuehrungskraft zusaetzlich die Teamzahlen der kompletten Downline', function (): void { $berater = bslMakeLevel('Berater', 3); $vertriebsleiter = bslMakeLevel('Vertriebsleiter', 6); $root = bslMakeUser(); $leaderA = bslMakeUser($root->id, $vertriebsleiter->id); $member1 = bslMakeUser($leaderA->id, $berater->id); $leaderB = bslMakeUser($member1->id, $vertriebsleiter->id); $member2 = bslMakeUser($leaderB->id, $berater->id); bslMakeAbo($leaderA->id, 'me', '2026-06-01'); bslMakeAbo($member1->id, 'me', '2026-06-01'); bslMakeAbo($member1->id, 'ot', '2026-06-01'); bslMakeAbo($leaderB->id, 'ot', '2026-06-01'); bslMakeAbo($member2->id, 'me', '2026-06-01'); // zaehlt bei A nur in der Gesamt-Sicht $rows = collect(app(BackofficeDashboardService::class)->overview($root, 6, 2026)['leader_abos']['rows'])->keyBy('user_id'); $a = $rows->get($leaderA->id); $b = $rows->get($leaderB->id); // Direkt (an Leader B abgeschnitten) bleibt unveraendert additiv expect($a['team_abo_count'])->toBe(1) ->and($a['team_customer_abo_count'])->toBe(1) // Gesamt: komplette Downline inkl. Leader B und dessen Team ->and($a['team_abo_count_total'])->toBe(2) ->and($a['team_customer_abo_count_total'])->toBe(2) ->and($a['total_abo_count_full'])->toBe(5) ->and($b['team_abo_count_total'])->toBe(1) ->and($b['total_abo_count_full'])->toBe(2); }); it('summiert Punkte ohne Payline-Wirkung (status_points 2) als eigene Kennzahl', function (): void { $root = bslMakeUser(); $anna = bslMakeUser($root->id); UserSalesVolume::forceCreate([ 'user_id' => $anna->id, 'month' => 6, 'year' => 2026, 'points' => 200, 'status' => 5, 'status_points' => 2, // Neupartner-Einschreibung: nur KP, nicht Payline ]); UserSalesVolume::forceCreate([ 'user_id' => $anna->id, 'month' => 6, 'year' => 2026, 'points' => 150, 'status' => 1, 'status_points' => 1, // normale Bestellung: payline-wirksam ]); $overview = app(BackofficeDashboardService::class)->overview($root, 6, 2026); // K11: „Umsatz (Punkte)" sind die Provisionspunkte — die Nur-KP-Buchung steckt // nicht mehr darin, sondern wird gesondert ausgewiesen. expect($overview['non_payline_points'])->toBe(200.0) ->and($overview['totals']['total_points'])->toBe(150.0) ->and($overview['totals']['own_points'])->toBe(350.0); }); it('ignoriert Abo-Punkte geloeschter Produkte im Soll der Top-10-Umsatzliste', function (): void { $root = bslMakeUser(); $anna = bslMakeUser($root->id); $aboLive = bslMakeAboWithPoints($anna->id, 'ot', '2026-06-01', 40); $aboDeleted = bslMakeAboWithPoints($anna->id, 'ot', '2026-06-02', 60); Product::whereKey(UserAboItem::where('user_abo_id', $aboDeleted->id)->value('product_id'))->first()->delete(); $overview = app(BackofficeDashboardService::class)->overview($root, 7, 2026); $byPoints = $overview['top_customer_abos']['by_points']; expect($byPoints)->toHaveCount(1) ->and($byPoints[0]['user_id'])->toBe($anna->id) ->and($byPoints[0]['points_target'])->toBe(40.0); }); it('rankt Recruiter des Monats nach gesponserten Neupartnern inklusive ich', function (): void { $root = bslMakeUser(); // active_date Januar — selbst kein Neupartner $anna = bslMakeUser($root->id, null, '2026-06-05'); $ben = bslMakeUser($root->id, null, '2026-06-10'); $carla = bslMakeUser($anna->id, null, '2026-06-12'); // von Anna im selben Monat geworben bslMakeUser($anna->id, null, '2026-05-12'); // Mai, zaehlt nicht bslMakeUser($root->id); // Januar-Bestand, zaehlt nicht // Account war schon vor dem Monatsende abgelaufen: zaehlt nicht als Neupartner $expired = bslMakeUser($root->id, null, '2026-06-20'); $expired->forceFill(['payment_account' => '2026-06-25'])->save(); $ranking = app(BackofficeDashboardService::class)->overview($root, 6, 2026)['recruiter_ranking']; expect($ranking)->toHaveCount(2) ->and($ranking[0]['user_id'])->toBe($root->id) ->and($ranking[0]['is_self'])->toBeTrue() ->and($ranking[0]['line'])->toBe(0) ->and($ranking[0]['new_partner_count'])->toBe(2) ->and(collect($ranking[0]['partners'])->pluck('user_id')->sort()->values()->all()) ->toBe(collect([$anna->id, $ben->id])->sort()->values()->all()) ->and($ranking[1]['user_id'])->toBe($anna->id) ->and($ranking[1]['is_self'])->toBeFalse() ->and($ranking[1]['line'])->toBe(1) ->and($ranking[1]['new_partner_count'])->toBe(1) ->and($ranking[1]['partners'][0]['user_id'])->toBe($carla->id); // Carla erscheint in Roots Detailliste eingerueckt unter Anna $annaEntry = collect($ranking[0]['partners'])->firstWhere('user_id', $anna->id); expect($annaEntry['new_partner_count'])->toBe(1) ->and($annaEntry['children'][0]['user_id'])->toBe($carla->id); }); it('summiert das Recruiter-Volumen aus Eigen- und Kundenpunkten inklusive Kettenvolumen', function (): void { $root = bslMakeUser(); $anna = bslMakeUser($root->id, null, '2026-06-05'); // Neupartner von Root $ben = bslMakeUser($anna->id, null, '2026-06-15'); // Neupartner von Anna (Kette) // Anna: Starterpaket (Status 1, Eigenpunkte) + Kundenumsatz (Status 2) UserSalesVolume::forceCreate([ 'user_id' => $anna->id, 'month' => 6, 'year' => 2026, 'points' => 400, 'status' => 1, ]); UserSalesVolume::forceCreate([ 'user_id' => $anna->id, 'month' => 6, 'year' => 2026, 'points' => 100, 'status' => 2, ]); // Ben: nur Starterpaket UserSalesVolume::forceCreate([ 'user_id' => $ben->id, 'month' => 6, 'year' => 2026, 'points' => 200, 'status' => 1, ]); $overview = app(BackofficeDashboardService::class)->overview($root, 6, 2026); $ranking = collect($overview['recruiter_ranking'])->keyBy('user_id'); // Root: Annas 500 (400 eigene + 100 Kunden) + rekursiv Bens 200 expect($ranking->get($root->id)['volume_points'])->toBe(700.0) ->and($ranking->get($root->id)['partners'][0]['total_points'])->toBe(500.0) ->and($ranking->get($root->id)['partners'][0]['children'][0]['total_points'])->toBe(200.0) // Anna selbst: nur Bens Punkte ->and($ranking->get($anna->id)['volume_points'])->toBe(200.0); // Summenzeile: jeder Neupartner genau EINMAL. Die Spaltensumme waere // 700 + 200 = 900, weil Ben im Kettenvolumen von Root steckt und zusaetzlich // in Annas Zeile. Richtig sind Annas 500 + Bens 200 = 700 bei 2 Neupartnern. expect($overview['recruiter_totals'])->toBe([ 'new_partner_count' => 2, 'volume_points' => 700.0, ]); }); it('liefert im Neupartner-Drilldown zusaetzlich die Spalte Punkte gesamt', function (): void { $root = bslMakeUser(); $partner = bslMakeUser($root->id, null, '2026-06-15'); UserSalesVolume::forceCreate([ 'user_id' => $partner->id, 'month' => 6, 'year' => 2026, 'points' => 100, 'status' => 1, // Starterpaket / Eigenpunkte ]); UserSalesVolume::forceCreate([ 'user_id' => $partner->id, 'month' => 6, 'year' => 2026, 'points' => 250, 'status' => 2, // Kundenpunkte ]); $details = app(BackofficeDrilldownService::class)->details($root, 0, 'new_partners', 6, 2026); expect($details['rows'])->toHaveCount(1) ->and($details['rows'][0]['customer_points'])->toBe(250.0) ->and($details['rows'][0]['total_points'])->toBe(350.0) ->and($details['summary']['customer_points'])->toBe(250.0) ->and($details['summary']['total_points'])->toBe(350.0); }); it('teilt die abgerechneten Kundenabo-Punkte in wiederkehrende und einmalig dazubestellte auf', function (): void { // Kundenwunsch 2026-09-07: "1231 abgerechnet - davon 1000 reine Abo-Punkte - // 231 einmalige Bestellungen dazu." $root = bslMakeUser(); $anna = bslMakeUser($root->id); $order = ShoppingOrder::forceCreate([ 'shopping_user_id' => 0, 'auth_user_id' => $anna->id, 'country_id' => 0, 'user_shop_id' => 0, 'is_abo' => true, 'paid' => true, ]); // 2 x 500 wiederkehrend + 1 x 231 einmalig dazu = 1231 ShoppingOrderItem::forceCreate([ 'shopping_order_id' => $order->id, 'product_id' => 1, 'qty' => 2, 'points' => 500, 'is_abo_addon' => false, ]); ShoppingOrderItem::forceCreate([ 'shopping_order_id' => $order->id, 'product_id' => 2, 'qty' => 1, 'points' => 231, 'is_abo_addon' => true, ]); UserSalesVolume::forceCreate([ 'user_id' => $anna->id, 'shopping_order_id' => $order->id, 'month' => 6, 'year' => 2026, 'points' => 1231, 'status' => 2, ]); $row = collect(app(BackofficeDashboardService::class)->overview($root, 6, 2026)['top_customer_abos']['by_points']) ->firstWhere('user_id', $anna->id); expect($row['points_actual'])->toBe(1231.0) ->and($row['points_recurring'])->toBe(1000.0) ->and($row['points_addon'])->toBe(231.0) // Die beiden Spalten muessen die angezeigte Gesamtsumme ergeben. ->and($row['points_recurring'] + $row['points_addon'])->toBe($row['points_actual']); }); it('zaehlt Abo-Bestellungen ohne Einmal-Positionen komplett als wiederkehrend', function (): void { // Bestellungen vor 06/2026 kennen is_abo_addon nicht (Default false) — // die Aufteilung darf dort nichts erfinden. $root = bslMakeUser(); $anna = bslMakeUser($root->id); $order = ShoppingOrder::forceCreate([ 'shopping_user_id' => 0, 'auth_user_id' => $anna->id, 'country_id' => 0, 'user_shop_id' => 0, 'is_abo' => true, 'paid' => true, ]); UserSalesVolume::forceCreate([ 'user_id' => $anna->id, 'shopping_order_id' => $order->id, 'month' => 6, 'year' => 2026, 'points' => 400, 'status' => 2, ]); $row = collect(app(BackofficeDashboardService::class)->overview($root, 6, 2026)['top_customer_abos']['by_points']) ->firstWhere('user_id', $anna->id); expect($row['points_actual'])->toBe(400.0) ->and($row['points_recurring'])->toBe(400.0) ->and($row['points_addon'])->toBe(0.0); });