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

169 lines
5.7 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\AboChartSnapshot;
use App\Models\UserAbo;
use App\Services\AboHelper;
use App\User;
use Carbon\Carbon;
use Illuminate\Console\Command;
class AboStoreChartSnapshots extends Command
{
protected $signature = 'abo:store-chart-snapshots
{--user= : Nur einen bestimmten User berechnen (user_id)}
{--force : Bereits vorhandene Snapshots überschreiben}';
protected $description = 'Speichert monatliche Abo-Zählungen aller vergangenen Monate in der Datenbank (einmalig je Monat)';
private const SCOPES = ['ot', 'team_abos', 'team_cust_abos'];
private const START_YEAR = 2026;
public function handle(): int
{
$now = Carbon::now();
$force = (bool) $this->option('force');
// Monate die eingefroren werden sollen: von START_YEAR/01 bis letzten Monat
$months = $this->getPastMonths($now);
if (empty($months)) {
$this->info('Keine vergangenen Monate zum Speichern.');
return self::SUCCESS;
}
// User ermitteln
$userQuery = User::whereNotNull('m_level')
->whereNotNull('payment_account')
->where('admin', '<', 4)
->whereNull('deleted_at');
if ($userId = $this->option('user')) {
$userQuery->where('id', $userId);
}
$users = $userQuery->select('id')->get();
$total = $users->count();
$this->info("Berechne Snapshots für {$total} User, ".count($months).' Monate, '.count(self::SCOPES).' Scopes...');
$bar = $this->output->createProgressBar($total);
$bar->start();
$inserted = 0;
$skipped = 0;
foreach ($users as $user) {
// Bereits vorhandene Snapshots für diesen User laden (zum Überspringen)
$existing = AboChartSnapshot::where('user_id', $user->id)
->get()
->keyBy(fn ($s) => "{$s->scope}_{$s->year}_{$s->month}");
$teamUserIds = AboHelper::getTeamUserIds($user->id);
$rows = [];
foreach ($months as [$year, $month]) {
$startOfMonth = Carbon::create($year, $month, 1)->startOfMonth();
$endOfMonth = Carbon::create($year, $month, 1)->endOfMonth();
foreach (self::SCOPES as $scope) {
$key = "{$scope}_{$year}_{$month}";
if (! $force && $existing->has($key)) {
$skipped++;
continue;
}
$count = $this->calculateCount($scope, $user->id, $teamUserIds, $startOfMonth, $endOfMonth);
$rows[] = [
'user_id' => $user->id,
'scope' => $scope,
'year' => $year,
'month' => $month,
'count' => $count,
'calculated_at' => $now,
'created_at' => $now,
'updated_at' => $now,
];
$inserted++;
}
}
if (! empty($rows)) {
if ($force) {
foreach ($rows as $row) {
AboChartSnapshot::updateOrInsert(
['user_id' => $row['user_id'], 'scope' => $row['scope'], 'year' => $row['year'], 'month' => $row['month']],
$row
);
}
} else {
AboChartSnapshot::insertOrIgnore($rows);
}
}
$bar->advance();
gc_collect_cycles();
}
$bar->finish();
$this->newLine();
$this->info("Fertig. Gespeichert: {$inserted}, Übersprungen (bereits vorhanden): {$skipped}");
return self::SUCCESS;
}
/**
* Berechnet die Abo-Anzahl für einen Scope/User/Monat anhand der tatsächlichen Daten zum Zeitpunkt der Berechnung.
*
* @param int[] $teamUserIds
*/
private function calculateCount(string $scope, int $userId, array $teamUserIds, Carbon $startOfMonth, Carbon $endOfMonth): int
{
$terminalStatuses = [4, 5];
$query = match ($scope) {
'ot' => UserAbo::where('member_id', $userId)
->where('is_for', 'ot')
->where('status', '>', 1),
'team_abos' => UserAbo::whereIn('user_id', $teamUserIds)
->where('is_for', 'me')
->where('status', '>', 1),
'team_cust_abos' => UserAbo::whereIn('member_id', $teamUserIds)
->where('is_for', 'ot')
->where('status', '>', 1),
};
return $query
->whereDate('start_date', '<=', $endOfMonth)
->where(function ($q) use ($startOfMonth, $terminalStatuses) {
$q->whereDate('cancel_date', '>=', $startOfMonth)
->orWhere(function ($q2) use ($terminalStatuses) {
$q2->whereNull('cancel_date')
->whereNotIn('status', $terminalStatuses);
});
})
->count();
}
/**
* Alle abgeschlossenen Monate von START_YEAR/01 bis letzten Monat.
*
* @return array<array{int, int}>
*/
private function getPastMonths(Carbon $now): array
{
$months = [];
$cursor = Carbon::create(self::START_YEAR, 1, 1);
$lastMonth = $now->copy()->subMonth()->endOfMonth();
while ($cursor->lte($lastMonth)) {
$months[] = [(int) $cursor->year, (int) $cursor->month];
$cursor->addMonth();
}
return $months;
}
}