Wer Ende August registriert und am 1. September bezahlt hat, tauchte in keiner
Neupartner-Zaehlung auf: Der August-Snapshot wurde um 04:45 eingefroren, da
fehlte noch das Abrechnungskonto; im September lag active_date im falschen
Monat. Betroffen sind 2026 acht Partner, auch im Recruiting ihrer Sponsoren.
Kundenentscheidung 16.09.2026: der Zeitpunkt der Zahlung ist massgeblich.
Neue Spalte users.partner_paid_at, gesetzt bei der ersten Kontozahlung
(Payment::paymentStatusPaidAction) und bei manueller Freischaltung durch den
Admin — nur, solange sie leer ist; Verlaengerung und Wiederherstellung aendern
sie nicht. Bestehende Partner werden per Migration aus der ersten bezahlten
Registrierungsbestellung rueckgefuellt (Zeitstempel der Punktebuchung, ersatzweise
Bestelldatum, ohne Bestellung das Aktivierungsdatum). Bewusst nicht
payment_account minus ein Jahr: das ergaebe bei Verlaengerern den Jahrestag der
Erstzahlung.
User::scopeNewPartnerIn() und isNewPartnerIn() lesen jetzt partner_paid_at —
damit Kachel, Recruiter-Ranking, Top 3 und Neupartner-Liste gleichermassen.
Die Liste und der CSV-Export zeigen "Bezahlt am" neben "Registriert am".
Unveraendert: die Teamstruktur haengt weiter an active_date (auch im
Provisionsbaum). Wer ist neu dazugekommen und wer gehoert zur Linie sind zwei
verschiedene Fragen.
Die eingefrorenen Monate Januar bis August enthalten sieben dieser Partner
nicht; ob sie per --force neu gebaut werden, ist eine Entscheidung — siehe
dev/2026-07-22/neupartner-zahlungsmonat.md.
Mit drin: die handgebaute SQLite-Tabelle in BackofficeDashboardServiceTest
kannte status_reason aus d6efe56 noch nicht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
629 lines
25 KiB
PHP
629 lines
25 KiB
PHP
<?php
|
|
|
|
use App\Models\Product;
|
|
use App\Models\ShoppingOrder;
|
|
use App\Models\ShoppingOrderItem;
|
|
use App\Models\UserAbo;
|
|
use App\Models\UserAboItem;
|
|
use App\Models\UserLevel;
|
|
use App\Models\UserSalesVolume;
|
|
use App\Services\Backoffice\BackofficeDashboardService;
|
|
use App\Services\Backoffice\BackofficeDrilldownService;
|
|
use App\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
uses(TestCase::class, RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
if (! Schema::connection('sqlite')->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);
|
|
});
|