mivita/app/Console/Commands/IncentiveCalculate.php
2026-04-10 17:15:27 +02:00

205 lines
8.4 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\Incentive;
use App\Models\IncentiveParticipant;
use App\Services\Incentive\IncentivePointsLogRepairService;
use App\Services\Incentive\IncentiveTracker;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
/**
* Batch-Neuberechnung fuer Incentives.
*
* Abo-Wertung (nicht hier codiert, sondern in {@see IncentiveParticipant::rebuildFromSourceTables}
* und {@see IncentivePointsLogRepairService::syncMissingTrackingAbos}):
*
* - Eigenabo (me): zaehlt auch wenn es vor dem Qualifikationszeitraum abgeschlossen wurde;
* Einmalpunkte wirken dann ab Qualifikationsbeginn (aktiviert_at/Log-Monat auf Start des Zeitraums).
* - Kundenabo (ot): nur wenn im Qualifikationszeitraum neu abgeschlossen (created_at im Zeitraum).
*
* Zu Beginn werden fuer alle Berater (User mit m_level) fehlende Teilnehmerzeilen ohne
* accepted_terms angelegt ({@see IncentiveParticipant::ensureConsultantsForIncentive}), damit
* Punkte ohne Checkbox mitlaufen; die Rangliste blendet Namen erst nach Zustimmung ein.
*/
class IncentiveCalculate extends Command
{
protected $signature = 'incentive:calculate
{incentive_id? : ID des Incentives (leer = alle aktiven)}
{--force : Tracking-Tabellen + Log loeschen und komplett neu aufbauen}
{--skip-repair : Kein Nachziehen von Trackings/FKs/SV-Logs (nur Summen aus bestehendem Log)}
{--verbose-details : Zeigt Details pro Teilnehmer}';
protected $description = 'Incentive-Punkte: fehlende Partner-/Abo-Trackings, FK-Reparatur, fehlende SV-Logs, Summen/Ranking; --force = kompletter Neuaufbau aus Quelldaten';
public function handle(IncentivePointsLogRepairService $repairService): int
{
if ($id = $this->argument('incentive_id')) {
$incentive = Incentive::find($id);
if (! $incentive) {
$this->error("Incentive #{$id} nicht gefunden.");
return self::FAILURE;
}
return $this->processIncentive($incentive, $repairService);
}
$incentives = Incentive::active()->get();
if ($incentives->isEmpty()) {
$this->info('Keine aktiven Incentives gefunden.');
return self::SUCCESS;
}
$exitCode = self::SUCCESS;
foreach ($incentives as $incentive) {
if ($this->processIncentive($incentive, $repairService) !== self::SUCCESS) {
$exitCode = self::FAILURE;
}
}
return $exitCode;
}
private function processIncentive(Incentive $incentive, IncentivePointsLogRepairService $repairService): int
{
$force = $this->option('force');
$skipRepair = $this->option('skip-repair');
$verbose = $this->option('verbose-details');
$this->info("=== {$incentive->name} (ID: {$incentive->id}) ===");
$this->info(" Zeitraum: {$incentive->qualification_start->format('d.m.Y')} - {$incentive->qualification_end->format('d.m.Y')}");
if ($force) {
$this->info(' Modus: FORCE (Tracking + Log aus Quelldaten neu aufbauen)');
} elseif ($skipRepair) {
$this->info(' Modus: Nur Neuberechnung (Summen/Ranking aus bestehendem Log)');
} else {
$this->info(' Modus: Tracking nachziehen + FK-Reparatur + SV-Logs + Neuberechnung');
}
$stubAdded = IncentiveParticipant::ensureConsultantsForIncentive($incentive);
if ($stubAdded > 0) {
$this->info(" Berater-Teilnehmer neu angelegt (ohne Zustimmung): {$stubAdded}");
}
$participants = $incentive->participants()->with('user', 'user.account')->get();
$this->info(" Teilnehmer: {$participants->count()}");
$this->newLine();
$stats = [
'processed' => 0,
'errors' => 0,
'with_points' => 0,
'with_partners' => 0,
'with_abos' => 0,
'tracking_partner_added' => 0,
'tracking_abo_added' => 0,
'repair_partner_fk' => 0,
'repair_abo_fk' => 0,
'repair_onetime_partner_fk' => 0,
'repair_onetime_abo_fk' => 0,
'sv_logs_added' => 0,
];
$errors = [];
$bar = $this->output->createProgressBar($participants->count());
$bar->start();
foreach ($participants as $participant) {
try {
if (! $participant->user) {
$bar->advance();
continue;
}
if ($force) {
$participant->rebuildFromSourceTables()->save();
} else {
if (! $skipRepair) {
$stats['tracking_partner_added'] += $repairService->syncMissingTrackingPartners($participant);
$stats['tracking_abo_added'] += $repairService->syncMissingTrackingAbos($participant);
$r = $repairService->repairForeignKeys($participant);
$stats['repair_partner_fk'] += $r['partner_fk'];
$stats['repair_abo_fk'] += $r['abo_fk'];
$stats['repair_onetime_partner_fk'] += $r['onetime_partner_fk'];
$stats['repair_onetime_abo_fk'] += $r['onetime_abo_fk'];
$stats['sv_logs_added'] += $repairService->syncMissingSalesVolumeLogs($participant);
}
$participant->recalculateFromTrackingTables()->save();
}
$stats['processed']++;
if ($participant->total_points > 0) {
$stats['with_points']++;
}
if ($participant->qualified_partners > 0) {
$stats['with_partners']++;
}
if ($participant->qualified_abos > 0) {
$stats['with_abos']++;
}
if ($verbose && $participant->total_points > 0) {
$name = $participant->user->account
? $participant->user->account->first_name.' '.$participant->user->account->last_name
: ($participant->user->email ?? 'User #'.$participant->user_id);
$bar->clear();
$this->line(" {$name}: {$participant->total_points} Pkt, {$participant->qualified_partners} Partner, {$participant->qualified_abos} Abos");
$bar->display();
}
} catch (\Throwable $e) {
$stats['errors']++;
$errors[] = "Participant #{$participant->id} (User #{$participant->user_id}): {$e->getMessage()}";
Log::error('IncentiveCalculation error for participant '.$participant->id.': '.$e->getMessage());
}
$bar->advance();
}
$bar->finish();
$this->newLine(2);
IncentiveTracker::updateRanking($incentive);
$ranked = IncentiveParticipant::where('incentive_id', $incentive->id)
->whereNotNull('rank')
->count();
$tableRows = [
['Verarbeitet', (string) $stats['processed']],
['Fehler', (string) $stats['errors']],
['Mit Punkten', (string) $stats['with_points']],
['Mit Partnern', (string) $stats['with_partners']],
['Mit Abos', (string) $stats['with_abos']],
['Im Ranking', (string) $ranked],
];
if (! $force) {
$tableRows[] = ['Neupartner-Trackings nachgezogen', (string) $stats['tracking_partner_added']];
$tableRows[] = ['Neuabo-Trackings nachgezogen', (string) $stats['tracking_abo_added']];
$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']];
$tableRows[] = ['FK Abo (Einmal) repariert', (string) $stats['repair_onetime_abo_fk']];
$tableRows[] = ['Neue SV-Log-Eintraege', (string) $stats['sv_logs_added']];
}
$this->table(['Metrik', 'Wert'], $tableRows);
if (! empty($errors)) {
$this->error('Fehler:');
foreach ($errors as $err) {
$this->line(" - {$err}");
}
return self::FAILURE;
}
$this->info('Fertig.');
return self::SUCCESS;
}
}