[]]); }); afterEach(function () { Carbon::setTestNow(); }); function recMakeUser(string $firstName, string $lastName = 'Partner', ?int $sponsorId = null, ?string $partnerPaidAt = null): User { $account = UserAccount::forceCreate([ 'first_name' => $firstName, 'last_name' => $lastName, ]); return User::forceCreate([ 'email' => 'rec-'.uniqid('', true).'@example.com', 'password' => bcrypt('secret'), 'lang' => 'de', 'm_level' => 1, 'm_sponsor' => $sponsorId, 'payment_account' => now()->addYear(), 'active_date' => $partnerPaidAt, 'partner_paid_at' => $partnerPaidAt, 'account_id' => $account->id, ]); } function recAddVolume(int $userId, float $points, int $status, int $month = 9, ?int $shoppingOrderId = null): void { UserSalesVolume::forceCreate([ 'user_id' => $userId, 'month' => $month, 'year' => 2026, 'points' => $points, 'status' => $status, 'shopping_order_id' => $shoppingOrderId, ]); } function recAddCustomerAboPoints(int $userId, float $points, int $month = 9): void { $order = ShoppingOrder::forceCreate([ 'shopping_user_id' => 0, 'auth_user_id' => 0, 'country_id' => 0, 'user_shop_id' => 0, 'member_id' => $userId, 'is_abo' => true, ]); recAddVolume($userId, $points, 2, $month, $order->id); } function recMakeCustomerAbo(int $consultantId, string $startDate): UserAbo { return UserAbo::create([ 'user_id' => 0, 'member_id' => $consultantId, 'shopping_user_id' => 0, 'email' => 'rec-abo-'.uniqid('', true).'@example.com', 'is_for' => 'ot', 'payone_userid' => 0, 'clearingtype' => 'cc', 'amount' => 5000, 'active' => true, 'status' => 2, 'abo_interval' => 5, 'start_date' => $startDate, 'next_date' => '2026-11-05', ]); } /** * @param array> $podium * @return array}> */ function recPlaces(array $podium): array { return array_map(fn (array $place) => [ 'place' => $place['place'], 'value' => $place['value'], 'names' => array_map(fn (array $member) => $member['first_name'], $place['members']), ], $podium); } it('rankt alle fuenf Kategorien firmenweit, unabhaengig vom Team', function (): void { $anna = recMakeUser('Anna'); $ben = recMakeUser('Ben'); recMakeUser('Neu1', 'Partner', $anna->id, '2026-09-10'); recMakeUser('Neu2', 'Partner', $anna->id, '2026-09-20'); recMakeUser('Alt', 'Partner', $anna->id, '2026-08-20'); recMakeCustomerAbo($ben->id, '2026-09-05'); recMakeCustomerAbo($ben->id, '2026-09-15'); recMakeCustomerAbo($ben->id, '2026-06-01'); recMakeCustomerAbo($anna->id, '2026-09-02'); recAddCustomerAboPoints($ben->id, 120.5); recAddCustomerAboPoints($anna->id, 80); recAddVolume($anna->id, 900, 1); recAddVolume($ben->id, 50, 1, 8); $categories = app(RecognitionService::class)->recognitions(9, 2026)['categories']; expect(recPlaces($categories['recruiters']))->toBe([['place' => 1, 'value' => 2, 'names' => ['Anna']]]) ->and(recPlaces($categories['new_customer_abos']))->toBe([ ['place' => 1, 'value' => 2, 'names' => ['Ben']], ['place' => 2, 'value' => 1, 'names' => ['Anna']], ]) ->and(recPlaces($categories['customer_abos_total']))->toBe([ ['place' => 1, 'value' => 3, 'names' => ['Ben']], ['place' => 2, 'value' => 1, 'names' => ['Anna']], ]) ->and(recPlaces($categories['customer_abo_points']))->toBe([ ['place' => 1, 'value' => 120.5, 'names' => ['Ben']], ['place' => 2, 'value' => 80.0, 'names' => ['Anna']], ]) ->and(recPlaces($categories['total_points']))->toBe([ ['place' => 1, 'value' => 980.0, 'names' => ['Anna']], ['place' => 2, 'value' => 120.5, 'names' => ['Ben']], ]); }); it('schliesst standardmaessig Firmenkonto, Ruth Krummel und Go4 Values aus', function (): void { expect((require config_path('mivita.php'))['recognition_excluded_user_ids'])->toBe([3, 6, 486]); }); it('fuehrt Firmenkonto und Grosskunden nie auf', function (): void { $company = recMakeUser('Firma', 'GmbH'); $bigCustomer = recMakeUser('Ruth', 'Gross'); $anna = recMakeUser('Anna'); config(['mivita.recognition_excluded_user_ids' => [$company->id, $bigCustomer->id]]); recAddVolume($company->id, 5000, 2); recAddVolume($bigCustomer->id, 7000, 1); recAddVolume($anna->id, 300, 1); $service = app(RecognitionService::class); expect(recPlaces($service->recognitions(9, 2026)['categories']['total_points'])) ->toBe([['place' => 1, 'value' => 300.0, 'names' => ['Anna']]]) ->and(array_column($service->liveRankings(10, 2026)['categories']['total_points'], 'user_id')) ->not->toContain($company->id, $bigCustomer->id); }); it('teilt Gleichstaende einen Platz und vergibt die Plaetze lueckenlos', function (): void { foreach (['Carla' => 500, 'Anna' => 400, 'Ben' => 400, 'Dora' => 300, 'Emil' => 200, 'Fritz' => 100] as $name => $points) { recAddVolume(recMakeUser($name)->id, $points, 1); } $categories = app(RecognitionService::class)->recognitions(9, 2026)['categories']; // 1.000-Punkte-Shop mit Platz 4 (wie in der Mail), danach ist Schluss. expect(recPlaces($categories['total_points']))->toBe([ ['place' => 1, 'value' => 500.0, 'names' => ['Carla']], ['place' => 2, 'value' => 400.0, 'names' => ['Anna', 'Ben']], ['place' => 3, 'value' => 300.0, 'names' => ['Dora']], ['place' => 4, 'value' => 200.0, 'names' => ['Emil']], ]); }); it('platziert Recruiter erst ab zwei Neupartnern', function (): void { $anna = recMakeUser('Anna'); $ben = recMakeUser('Ben'); recMakeUser('Neu1', 'Partner', $anna->id, '2026-09-10'); recMakeUser('Neu2', 'Partner', $anna->id, '2026-09-11'); recMakeUser('Neu3', 'Partner', $ben->id, '2026-09-12'); $service = app(RecognitionService::class); expect(recPlaces($service->recognitions(9, 2026)['categories']['recruiters'])) ->toBe([['place' => 1, 'value' => 2, 'names' => ['Anna']]]); }); it('zeigt Live-Ranglisten mit Wettkampf-Rang und hoechstens zehn Zeilen', function (): void { foreach (range(1, 12) as $index) { recAddVolume(recMakeUser('User'.str_pad((string) $index, 2, '0', STR_PAD_LEFT))->id, $index === 2 ? 1200 : 100 * $index, 1, 10); } $rows = app(RecognitionService::class)->liveRankings(10, 2026)['categories']['total_points']; expect($rows)->toHaveCount(10) ->and(array_column(array_slice($rows, 0, 3), 'rank'))->toBe([1, 1, 3]) ->and(array_column(array_slice($rows, 0, 3), 'first_name'))->toBe(['User02', 'User12', 'User11']) ->and($rows[0])->toHaveKey('profile_image_url'); }); it('liefert vor dem Stichtag den vorlaeufigen Stand und danach den eingefrorenen', function (): void { $anna = recMakeUser('Anna'); recAddVolume($anna->id, 300, 1); Carbon::setTestNow(Carbon::parse('2026-10-02 12:00:00')); $service = app(RecognitionService::class); $preliminary = $service->recognitions(9, 2026); expect($preliminary['is_final'])->toBeFalse() ->and($preliminary['freeze_date']->toDateString())->toBe('2026-10-03'); Carbon::setTestNow(Carbon::parse('2026-10-03 06:15:00')); $this->artisan('recognitions:freeze', ['--month' => 9, '--year' => 2026])->assertSuccessful(); // Spaetere Buchungen veraendern den festgeschriebenen Monat nicht mehr. recAddVolume(recMakeUser('Spaet')->id, 9999, 1); Cache::flush(); $final = $service->recognitions(9, 2026); expect($final['is_final'])->toBeTrue() ->and($final['frozen_at']->toDateString())->toBe('2026-10-03') ->and(recPlaces($final['categories']['total_points']))->toEqual([['place' => 1, 'value' => 300, 'names' => ['Anna']]]); }); it('friert per Command nur festschreibbare Monate ein und ueberschreibt nichts ohne --force', function (): void { $anna = recMakeUser('Anna'); recAddVolume($anna->id, 300, 1); Carbon::setTestNow(Carbon::parse('2026-10-02 06:15:00')); $this->artisan('recognitions:freeze')->assertSuccessful(); expect(RecognitionSnapshot::query()->where('year', 2026)->where('month', 9)->exists())->toBeFalse() ->and(RecognitionSnapshot::query()->where('year', 2026)->where('month', 8)->exists())->toBeTrue() ->and(RecognitionSnapshot::query()->count())->toBe(8); Carbon::setTestNow(Carbon::parse('2026-10-03 06:15:00')); $this->artisan('recognitions:freeze')->assertSuccessful(); $frozenAt = RecognitionSnapshot::query()->where('month', 9)->value('frozen_at'); Carbon::setTestNow(Carbon::parse('2026-10-04 06:15:00')); $this->artisan('recognitions:freeze')->assertSuccessful(); expect(RecognitionSnapshot::query()->count())->toBe(9) ->and(RecognitionSnapshot::query()->where('month', 9)->value('frozen_at'))->toEqual($frozenAt); $this->artisan('recognitions:freeze', ['--month' => 9, '--year' => 2026, '--force' => true])->assertSuccessful(); expect(RecognitionSnapshot::query()->where('month', 9)->first()->frozen_at->toDateString())->toBe('2026-10-04'); });