Neue Felder amount_per_partner, bonus_rank_1..3 und include_sales_points an incentives. Mit ausgeschalteten Umsatzpunkten zaehlt die Wertung nur die Einmalpunkte je Neupartner, die Rangliste entspricht damit der Anzahl Neupartner mit Starterpaket. IncentivePayoutService rechnet Neupartner mal Betrag plus Rang-Bonus fuer qualifizierte Gewinner; Admin-Detailseite bekommt Praemienspalte, Summen und XLSX-Export fuer die Auszahlung. Sidenav und Dashboard zeigen alle aktiven Incentives, damit Montenegro und die Challenge parallel laufen koennen. Teaser, Rangliste und Details zeigen bei Geldpraemie die Praemien-Kachel und "Deine Praemie bisher" und blenden Abo-Bloecke aus, wenn Abos keine Rolle spielen. Montenegro bleibt unveraendert. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
135 lines
5 KiB
PHP
135 lines
5 KiB
PHP
<?php
|
|
|
|
use App\Models\Incentive;
|
|
use App\Models\IncentiveNewPartner;
|
|
use App\Models\IncentiveParticipant;
|
|
use App\Models\IncentivePointsLog;
|
|
use App\Models\UserSalesVolume;
|
|
use App\Services\Incentive\IncentivePointsLogRepairService;
|
|
use App\Services\Incentive\IncentiveTracker;
|
|
use App\User;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
/**
|
|
* E1: Bei `include_sales_points = 0` (Recruiting-Challenge) zaehlen nur die Einmalpunkte
|
|
* je Neupartner; Umsaetze der Neupartner erzeugen keine Punkte.
|
|
*/
|
|
beforeEach(function () {
|
|
Carbon::setTestNow(Carbon::parse('2026-11-15 12:00:00'));
|
|
});
|
|
|
|
afterEach(fn () => Carbon::setTestNow());
|
|
|
|
function toggleUser(array $overrides = []): User
|
|
{
|
|
return User::forceCreate(array_merge([
|
|
'email' => 'toggle-'.uniqid('', true).'@example.com',
|
|
'password' => bcrypt('secret'),
|
|
'lang' => 'de',
|
|
], $overrides));
|
|
}
|
|
|
|
/**
|
|
* @return array{0: Incentive, 1: IncentiveParticipant, 2: User}
|
|
*/
|
|
function trackedNewPartner(bool $includeSalesPoints): array
|
|
{
|
|
$incentive = Incentive::factory()->create([
|
|
'qualification_start' => '2026-10-01',
|
|
'qualification_end' => '2027-01-31',
|
|
'calculation_end' => '2027-02-28',
|
|
'points_partner_onetime' => 1,
|
|
'points_abo_onetime' => 0,
|
|
'min_direct_partners' => 0,
|
|
'min_customer_abos' => 0,
|
|
'max_winners' => 3,
|
|
'include_sales_points' => $includeSalesPoints,
|
|
]);
|
|
$sponsor = toggleUser();
|
|
$partner = toggleUser(['m_sponsor' => $sponsor->id]);
|
|
$participant = IncentiveParticipant::factory()->create([
|
|
'incentive_id' => $incentive->id,
|
|
'user_id' => $sponsor->id,
|
|
]);
|
|
$tracking = IncentiveNewPartner::create([
|
|
'participant_id' => $participant->id,
|
|
'user_id' => $partner->id,
|
|
'registered_at' => '2026-10-20 10:00:00',
|
|
]);
|
|
IncentivePointsLog::create([
|
|
'participant_id' => $participant->id,
|
|
'type' => 'partner',
|
|
'source_type' => User::class,
|
|
'source_id' => $partner->id,
|
|
'source_label' => 'Neupartner',
|
|
'month' => 10,
|
|
'year' => 2026,
|
|
'points_onetime' => 1,
|
|
'points_accumulated' => 0,
|
|
'incentive_new_partner_id' => $tracking->id,
|
|
]);
|
|
$participant->recalculateFromTrackingTables()->save();
|
|
|
|
return [$incentive, $participant, $partner];
|
|
}
|
|
|
|
function partnerSalesVolume(User $partner, int $points = 120): UserSalesVolume
|
|
{
|
|
return UserSalesVolume::create([
|
|
'user_id' => $partner->id,
|
|
'month' => 11,
|
|
'year' => 2026,
|
|
'date' => '15.11.2026',
|
|
'points' => $points,
|
|
'total_net' => 150,
|
|
'status_points' => 1,
|
|
'status' => 1,
|
|
'message' => 'Eigenbestellung Neupartner',
|
|
]);
|
|
}
|
|
|
|
it('schreibt ohne Umsatzpunkte keinen Log fuer Umsaetze des Neupartners', function () {
|
|
[$incentive, $participant, $partner] = trackedNewPartner(includeSalesPoints: false);
|
|
|
|
IncentiveTracker::trackSalesVolume(partnerSalesVolume($partner));
|
|
|
|
expect(IncentivePointsLog::where('participant_id', $participant->id)->whereNotNull('user_sales_volume_id')->count())->toBe(0)
|
|
->and($participant->fresh()->total_points)->toBe(1)
|
|
->and($participant->fresh()->qualified_partners)->toBe(1);
|
|
});
|
|
|
|
it('schreibt mit Umsatzpunkten weiterhin den Log (Montenegro-Verhalten)', function () {
|
|
[$incentive, $participant, $partner] = trackedNewPartner(includeSalesPoints: true);
|
|
|
|
IncentiveTracker::trackSalesVolume(partnerSalesVolume($partner));
|
|
|
|
expect(IncentivePointsLog::where('participant_id', $participant->id)->whereNotNull('user_sales_volume_id')->count())->toBe(1)
|
|
->and($participant->fresh()->total_points)->toBe(121);
|
|
});
|
|
|
|
it('zieht ohne Umsatzpunkte auch bei der Reparatur keine Umsatz-Logs nach', function () {
|
|
[$incentive, $participant, $partner] = trackedNewPartner(includeSalesPoints: false);
|
|
partnerSalesVolume($partner);
|
|
|
|
$synced = (new IncentivePointsLogRepairService)->syncMissingSalesVolumeLogs($participant->fresh());
|
|
|
|
expect($synced)->toBe(0)
|
|
->and(IncentivePointsLog::where('participant_id', $participant->id)->whereNotNull('user_sales_volume_id')->count())->toBe(0);
|
|
});
|
|
|
|
it('haelt die Umsatzpunkte auch beim Umschalten heraus, wenn die Wertung neu berechnet wird', function () {
|
|
[$incentive, $participant, $partner] = trackedNewPartner(includeSalesPoints: true);
|
|
IncentiveTracker::trackSalesVolume(partnerSalesVolume($partner));
|
|
expect($participant->fresh()->total_points)->toBe(121);
|
|
|
|
// Schalter nachtraeglich aus: die bestehenden Umsatz-Logs bleiben, bis --force neu aufbaut;
|
|
// ein Neuaufbau ohne Umsatzpunkte endet nach A/B.
|
|
$incentive->update(['include_sales_points' => false]);
|
|
$rebuilt = $participant->fresh()->load('incentive')->rebuildFromSourceTables();
|
|
|
|
expect(IncentivePointsLog::where('participant_id', $participant->id)->whereNotNull('user_sales_volume_id')->count())->toBe(0)
|
|
->and($rebuilt->total_points)->toBe(0);
|
|
});
|