$firstName, 'last_name' => 'Berater']); return User::forceCreate([ 'email' => 'payout-'.uniqid('', true).'@example.com', 'password' => bcrypt('secret'), 'lang' => 'de', 'account_id' => $account->id, 'payment_account' => now()->addYear(), ]); } function recruitingChallenge(array $overrides = []): Incentive { return Incentive::factory()->create(array_merge([ 'name' => 'Recruiting-Challenge', '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, 'amount_per_partner' => 50, 'bonus_rank_1' => 300, 'bonus_rank_2' => 200, 'bonus_rank_3' => 100, 'include_sales_points' => false, ], $overrides)); } describe('Incentive-Modell: Praemienfelder', function () { it('erkennt eine Geldpraemie und die Challenge-Wertung nach Neupartnern', function () { $challenge = recruitingChallenge(); $montenegro = Incentive::factory()->create(); expect($challenge->hasCashPayout())->toBeTrue() ->and($challenge->rankBonuses())->toBe([1 => 300.0, 2 => 200.0, 3 => 100.0]) ->and($challenge->includesSalesPoints())->toBeFalse() ->and($challenge->hasMinimumRequirements())->toBeFalse() ->and($challenge->usesAbos())->toBeFalse() ->and($challenge->ranksByPartnerCount())->toBeTrue() ->and($montenegro->hasCashPayout())->toBeFalse() ->and($montenegro->includesSalesPoints())->toBeTrue() ->and($montenegro->ranksByPartnerCount())->toBeFalse(); }); it('gibt den Rang-Bonus nur innerhalb der Gewinnerplaetze', function () { $challenge = recruitingChallenge(['max_winners' => 2]); expect($challenge->rankBonus(1))->toBe(300.0) ->and($challenge->rankBonus(2))->toBe(200.0) ->and($challenge->rankBonus(3))->toBe(0.0) ->and($challenge->rankBonus(null))->toBe(0.0); }); it('liefert aktive Incentives fuer die Navigation in Reihenfolge des Starts', function () { $later = recruitingChallenge(); $earlier = Incentive::factory()->create(['qualification_start' => '2026-04-01']); Incentive::factory()->closed()->create(); Incentive::factory()->draft()->create(); expect(Incentive::activeForNavigation()->pluck('id')->all())->toBe([$earlier->id, $later->id]); }); }); describe('IncentivePayoutService', function () { it('rechnet Neupartner mal Betrag plus Rang-Bonus fuer qualifizierte Gewinner', function () { $challenge = recruitingChallenge(); $service = new IncentivePayoutService; $first = IncentiveParticipant::factory()->create([ 'incentive_id' => $challenge->id, 'user_id' => payoutUser()->id, 'qualified_partners' => 4, 'total_points' => 4, 'is_qualified' => true, 'rank' => 1, ]); $fourth = IncentiveParticipant::factory()->create([ 'incentive_id' => $challenge->id, 'user_id' => payoutUser()->id, 'qualified_partners' => 1, 'total_points' => 1, 'is_qualified' => true, 'rank' => 4, ]); $notQualified = IncentiveParticipant::factory()->create([ 'incentive_id' => $challenge->id, 'user_id' => payoutUser()->id, 'qualified_partners' => 2, 'total_points' => 2, 'is_qualified' => false, 'rank' => 2, ]); expect($service->forParticipant($challenge, $first))->toBe([ 'partners' => 4, 'per_partner' => 50.0, 'partner_amount' => 200.0, 'rank_bonus' => 300.0, 'total' => 500.0, ])->and($service->forParticipant($challenge, $fourth)['total'])->toBe(50.0) ->and($service->forParticipant($challenge, $notQualified))->toMatchArray(['rank_bonus' => 0.0, 'total' => 100.0]); }); it('listet nur Teilnehmer mit Praemie, nach Rang, und summiert', function () { $challenge = recruitingChallenge(); $service = new IncentivePayoutService; $second = IncentiveParticipant::factory()->create([ 'incentive_id' => $challenge->id, 'user_id' => payoutUser('Bea')->id, 'qualified_partners' => 2, 'total_points' => 2, 'is_qualified' => true, 'rank' => 2, ]); $first = IncentiveParticipant::factory()->create([ 'incentive_id' => $challenge->id, 'user_id' => payoutUser('Anna')->id, 'qualified_partners' => 3, 'total_points' => 3, 'is_qualified' => true, 'rank' => 1, ]); IncentiveParticipant::factory()->create([ 'incentive_id' => $challenge->id, 'user_id' => payoutUser('Null')->id, 'qualified_partners' => 0, 'total_points' => 0, 'is_qualified' => true, 'rank' => null, ]); $rows = $service->rows($challenge); expect($rows->pluck('participant.id')->all())->toBe([$first->id, $second->id]) ->and($service->totals($rows))->toBe([ 'recipients' => 2, 'partners' => 5, 'partner_amount' => 250.0, 'rank_bonus' => 500.0, 'total' => 750.0, ]); $export = $service->exportRows($challenge); expect($export)->toHaveCount(2) ->and($export[0])->toHaveCount(count($service->exportHeaders())) ->and($export[0][0])->toBe(1) ->and($export[0][1])->toBe($first->user_id) ->and($export[0][2])->toBe('Anna Berater') ->and($export[0][5])->toBe(3) ->and($export[0][6])->toBe(50.0) ->and($export[0][7])->toBe(150.0) ->and($export[0][8])->toBe(300.0) ->and($export[0][9])->toBe(450.0); }); it('zahlt ohne Betrag je Neupartner nur den Rang-Bonus', function () { $challenge = recruitingChallenge(['amount_per_partner' => null]); $participant = IncentiveParticipant::factory()->create([ 'incentive_id' => $challenge->id, 'user_id' => payoutUser()->id, 'qualified_partners' => 5, 'total_points' => 5, 'is_qualified' => true, 'rank' => 3, ]); expect($challenge->hasCashPayout())->toBeTrue() ->and((new IncentivePayoutService)->forParticipant($challenge, $participant))->toMatchArray([ 'partner_amount' => 0.0, 'rank_bonus' => 100.0, 'total' => 100.0, ]); }); }); describe('Ansichten', function () { beforeEach(function () { URL::resolveMissingNamedRoutesUsing(fn (string $name, array $parameters) => '/'.$name.'/'.implode('/', array_map('strval', $parameters))); }); it('zeigt die Praemien-Kachel mit Betrag und Boni', function () { $html = view('user.incentive._payout_tile', ['incentive' => recruitingChallenge()])->render(); expect($html)->toContain('id="incentive-payout-tile"') ->toContain('50,00 €') ->toContain(__('incentive.payout_rank_bonus_label', ['n' => 1])) ->toContain('300,00 €'); }); it('rendert im Dashboard je aktivem Incentive eine Karte mit eigenen Modal-IDs', function () { $montenegro = Incentive::factory()->create(['name' => 'Montenegro', 'qualification_start' => '2026-04-01']); $challenge = recruitingChallenge(); $user = payoutUser(); // Noch unbestaetigt: dann erscheint auch fuer die Challenge das Teilnahme-Modal. $participant = IncentiveParticipant::factory()->unconfirmed()->create([ 'incentive_id' => $challenge->id, 'user_id' => $user->id, 'qualified_partners' => 2, 'total_points' => 2, 'is_qualified' => true, 'rank' => 1, ]); $html = view('dashboard._incentive', [ 'activeIncentives' => Incentive::activeForNavigation(), 'incentiveParticipants' => collect([$challenge->id => $participant]), ])->render(); expect($html)->toContain('Montenegro') ->toContain('Recruiting-Challenge') ->toContain('id="incParticipateModal-'.$montenegro->id.'"') ->toContain('id="incParticipateModal-'.$challenge->id.'"') ->toContain(__('incentive.badge_challenge')) ->toContain(__('incentive.payout_so_far')) ->toContain('400,00 €') ->and(substr_count($html, '