- /user/abos/ot: Filter nach Eingangsmonat (start_date) ueber neuen UserAbo-Scope - Konsolidierte Team-Abo-Ansicht nach Ebenen/Linien (Bestand, neue Abos im Monat, Eigen-/Kundenabos, Punkte je Ebene + Summen) via TeamAboOverviewService - team/customers leitet auf konsolidierte Ansicht um; Navigation bereinigt - Mitglieder-Detail-Modal mit Abo-Zusammenfassung (Eigen-Abo, Kundenabos, Punkte, Beginn, naechste Ausfuehrung, Ausfuehrungen) - Wachstums-Tabelle (Jahr x Monate) + Liniendiagramm (neue Eigen-/Kundenabos) - Dashboard-KPI-Kachel (Gesamtabos, Kundenabos, 1000-Punkte-Shops) - i18n de/en/es/fr, Tests (Filter, Overview, Wachstum, Modal, Redirect) Co-authored-by: Cursor <cursoragent@cursor.com>
274 lines
9.5 KiB
PHP
274 lines
9.5 KiB
PHP
<?php
|
|
|
|
use App\Models\Product;
|
|
use App\Models\UserAbo;
|
|
use App\Models\UserAboItem;
|
|
use App\Models\UserAboOrder;
|
|
use App\Models\UserSalesVolume;
|
|
use App\Services\Abo\TeamAboOverviewService;
|
|
use App\Services\HTMLHelper;
|
|
use App\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\URL;
|
|
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 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('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,
|
|
'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();
|
|
});
|