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); });