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 */ 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; } }