- Neue Seite Anerkennungen (/user/recognitions, Menue unter News Archiv) fuer alle Berater mit aktivem Konto: oben die Anerkennungen des Vormonats in den fuenf Kategorien der Monatsmail, Filter zurueck bis Januar 2026; darunter Live-Ranglisten (Top 10 je Kategorie) des laufenden Monats, 15 Min. Cache. - Kategorien: Top-Recruiter (ab 2 Neupartnern), neue Kunden-Abos, Kundenabo-Punkte, Kunden-Abos gesamt, Gesamtpunkte (1.000-Punkte-Shop, mit Platz 4). Regeln wie TopResultsService/VIP-Statistik, nur firmenweit. - Firmenkonto (6) und Grosskundin (3) nie aufgefuehrt: config mivita.recognition_excluded_user_ids. - Abgeschlossene Monate sind bis zum 3. des Folgemonats vorlaeufig und werden dann eingefroren: neue Tabelle recognition_snapshots, Command recognitions:freeze (taeglich 06:15, idempotent, holt offene Monate nach). - September-Anerkennungen firmenweit geprueft und korrigiert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
253 lines
9.6 KiB
PHP
253 lines
9.6 KiB
PHP
<?php
|
|
|
|
use App\Models\RecognitionSnapshot;
|
|
use App\Models\ShoppingOrder;
|
|
use App\Models\UserAbo;
|
|
use App\Models\UserAccount;
|
|
use App\Models\UserSalesVolume;
|
|
use App\Services\Team\RecognitionService;
|
|
use App\User;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Tests\TestCase;
|
|
|
|
uses(TestCase::class, RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
Carbon::setTestNow(Carbon::parse('2026-10-09 10:00:00'));
|
|
Cache::flush();
|
|
config(['mivita.recognition_excluded_user_ids' => []]);
|
|
});
|
|
|
|
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<int, array<string, mixed>> $podium
|
|
* @return array<int, array{place: int, value: float|int, names: array<int, string>}>
|
|
*/
|
|
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('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');
|
|
});
|