Incentive: Schalter "Abos beruecksichtigen", Recruiting Challenge ohne Abos

Die Recruiting Challenge erfasste Eigen- und Kundenabos mit 0 Punkten
(Live-Kopie: 175 Eigenabos). Sie tauchten in Details und Admin auf und
brachten Berater ohne Neupartner in die Rangliste.

- incentives.include_abos (Standard an); Migration schaltet Incentives
  ohne jede Abo-Wertung aus (0 Abo-Punkte, 0 Mindestabos, ohne Umsatzpunkte)
- aus: kein Abo-Tracking in Tracker, Neuaufbau und Reparatur;
  incentive:calculate raeumt vorhandene Abo-Zeilen ab
- Admin: Checkbox im Formular, Abo-Punkte/Mindestabos werden dann auf 0
  gesetzt; Abo-Spalte und -Bereiche in Admin, Details und Teilnahme-Hinweis
  entfallen
- Seeder der Recruiting Challenge setzt den Schalter aus

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-09-24 11:37:32 +00:00
parent 773d523bf7
commit 11afdb11d1
19 changed files with 306 additions and 34 deletions

View file

@ -97,6 +97,7 @@ class IncentiveCalculate extends Command
'tracking_partner_added' => 0,
'tracking_partner_removed' => 0,
'tracking_abo_added' => 0,
'tracking_abo_removed' => 0,
'repair_partner_fk' => 0,
'repair_abo_fk' => 0,
'repair_onetime_partner_fk' => 0,
@ -130,6 +131,7 @@ class IncentiveCalculate extends Command
$stats['repair_onetime_abo_fk'] += $r['onetime_abo_fk'];
$stats['tracking_partner_removed'] += $repairService->removeStaleSponsorTrackings($participant);
$stats['tracking_partner_added'] += $repairService->syncMissingTrackingPartners($participant);
$stats['tracking_abo_removed'] += $repairService->removeExcludedAboTrackings($participant);
$stats['tracking_abo_added'] += $repairService->syncMissingTrackingAbos($participant);
$stats['sv_logs_added'] += $repairService->syncMissingSalesVolumeLogs($participant);
}
@ -186,6 +188,7 @@ class IncentiveCalculate extends Command
$tableRows[] = ['Neupartner-Trackings nachgezogen', (string) $stats['tracking_partner_added']];
$tableRows[] = ['Neupartner-Trackings entfernt (Sponsorwechsel)', (string) $stats['tracking_partner_removed']];
$tableRows[] = ['Neuabo-Trackings nachgezogen', (string) $stats['tracking_abo_added']];
$tableRows[] = ['Abo-Trackings entfernt (Incentive ohne Abos)', (string) $stats['tracking_abo_removed']];
$tableRows[] = ['FK Partner (akkum.) repariert', (string) $stats['repair_partner_fk']];
$tableRows[] = ['FK Abo (akkum.) repariert', (string) $stats['repair_abo_fk']];
$tableRows[] = ['FK Partner (Einmal) repariert', (string) $stats['repair_onetime_partner_fk']];

View file

@ -59,6 +59,7 @@ class IncentiveController extends Controller
'bonus_rank_2' => 'nullable|numeric|min:0|max:999999',
'bonus_rank_3' => 'nullable|numeric|min:0|max:999999',
'include_sales_points' => 'nullable|boolean',
'include_abos' => 'nullable|boolean',
'vip_only' => 'nullable|boolean',
'status' => 'required|integer|in:0,1,2',
]);
@ -112,7 +113,9 @@ class IncentiveController extends Controller
}
/**
* Leere Betragsfelder als NULL speichern, Checkbox als bool.
* Leere Betragsfelder als NULL speichern, Checkboxen als bool. Ohne Abos werden
* Abo-Einmalpunkte und Mindestabos auf 0 gesetzt, sonst waere die Qualifikation
* unerreichbar.
*
* @param array<string, mixed> $data
* @return array<string, mixed>
@ -124,6 +127,11 @@ class IncentiveController extends Controller
}
$data['include_sales_points'] = filter_var($data['include_sales_points'] ?? false, FILTER_VALIDATE_BOOLEAN);
$data['vip_only'] = filter_var($data['vip_only'] ?? false, FILTER_VALIDATE_BOOLEAN);
$data['include_abos'] = filter_var($data['include_abos'] ?? false, FILTER_VALIDATE_BOOLEAN);
if (! $data['include_abos']) {
$data['points_abo_onetime'] = 0;
$data['min_customer_abos'] = 0;
}
return $data;
}
@ -159,6 +167,7 @@ class IncentiveController extends Controller
'bonus_rank_2' => 'nullable|numeric|min:0|max:999999',
'bonus_rank_3' => 'nullable|numeric|min:0|max:999999',
'include_sales_points' => 'nullable|boolean',
'include_abos' => 'nullable|boolean',
'vip_only' => 'nullable|boolean',
'status' => 'required|integer|in:0,1,2',
]);

View file

@ -51,7 +51,7 @@ class IncentiveController extends Controller
->paginate(100);
$participateHasTrackableAbos = false;
if (! $participant?->accepted_terms_at) {
if (! $participant?->accepted_terms_at && $incentive->includesAbos()) {
$participateHasTrackableAbos = $this->userHasTrackableAbosForIncentive($user, $incentive);
}

View file

@ -71,6 +71,7 @@ class Incentive extends Model
'bonus_rank_2' => 'float',
'bonus_rank_3' => 'float',
'include_sales_points' => 'bool',
'include_abos' => 'bool',
'status' => 'int',
'vip_only' => 'bool',
'qualification_start' => 'date',
@ -101,6 +102,7 @@ class Incentive extends Model
'bonus_rank_2',
'bonus_rank_3',
'include_sales_points',
'include_abos',
'status',
'vip_only',
];
@ -365,6 +367,15 @@ class Incentive extends Model
return $this->include_sales_points === null || (bool) $this->include_sales_points;
}
/**
* Werden Abos ueberhaupt erfasst? Aus = reine Recruiting-Challenge, weder Kunden-
* noch Eigenabos landen im Tracking.
*/
public function includesAbos(): bool
{
return $this->include_abos === null || (bool) $this->include_abos;
}
/**
* Gibt es eine Mindestqualifikation (direkte Partner oder Kundenabos)?
*/
@ -378,7 +389,7 @@ class Incentive extends Model
*/
public function usesAbos(): bool
{
return $this->points_abo_onetime > 0 || $this->min_customer_abos > 0;
return $this->includesAbos() && ($this->points_abo_onetime > 0 || $this->min_customer_abos > 0);
}
/**

View file

@ -325,7 +325,12 @@ class IncentiveParticipant extends Model
->where('created_at', '<', $qualStart)
->get();
foreach ($customerAbosInPeriod->concat($ownAbosInPeriod)->concat($ownAbosPreExisting) as $abo) {
// Incentive ohne Abos (reine Recruiting-Challenge): nichts erfassen
$abosToTrack = $incentive->includesAbos()
? $customerAbosInPeriod->concat($ownAbosInPeriod)->concat($ownAbosPreExisting)
: collect();
foreach ($abosToTrack as $abo) {
$activatedAt = $abo->created_at;
$logMonth = (int) $abo->created_at->month;
$logYear = (int) $abo->created_at->year;

View file

@ -171,6 +171,23 @@ class IncentivePointsLogRepairService
return $removed;
}
/**
* Incentive ohne Abos: bereits erfasste Abo-Trackings samt Log-Zeilen entfernen
* (z. B. nachdem der Schalter im Admin ausgeschaltet wurde).
*
* @return int Anzahl entfernter Abo-Trackings
*/
public function removeExcludedAboTrackings(IncentiveParticipant $participant): int
{
if (! $participant->incentive || $participant->incentive->includesAbos()) {
return 0;
}
$participant->pointsLog()->where('type', 'abo')->delete();
return $participant->newAbos()->delete();
}
/**
* Fehlende Abo-Tracking-Zeilen anlegen (Kundenabo ot / Eigenabo me, wie IncentiveTracker).
* Zuerst trackAboActivated ueber Erstbestellung; ohne UserAboOrder manuell wie Neuaufbau B.
@ -180,7 +197,7 @@ class IncentivePointsLogRepairService
public function syncMissingTrackingAbos(IncentiveParticipant $participant): int
{
$incentive = $participant->incentive;
if (! $incentive) {
if (! $incentive || ! $incentive->includesAbos()) {
return 0;
}

View file

@ -121,7 +121,8 @@ class IncentiveTracker
$incentives = Incentive::query()
->active()
->qualificationWindowContains($activation_date)
->get();
->get()
->filter(fn (Incentive $incentive) => $incentive->includesAbos());
foreach ($incentives as $incentive) {
$participant = IncentiveParticipant::ensureForIncentiveUser($incentive, $consultant_id);