option('dry-run'); $limit = max(1, (int) $this->option('limit')); $now = now(); // Gelb und Grün gehen zum Termin automatisch live (Decision-Update // §5.0); nur Rot wird abgelehnt. Unklassifizierte PMs bleiben als // Fallback in der manuellen Queue. $candidates = PressRelease::withoutGlobalScope(PortalScope::class) ->where('status', PressReleaseStatus::Review->value) ->whereIn('classification', [ PressReleaseClassification::Green->value, PressReleaseClassification::Yellow->value, ]) ->whereNotNull('scheduled_at') ->where('scheduled_at', '<=', $now) ->orderBy('scheduled_at') ->limit($limit) ->get(); if ($candidates->isEmpty()) { $this->info('Keine fälligen geplanten Pressemitteilungen gefunden.'); return self::SUCCESS; } $this->info(sprintf( '%d fällige Pressemitteilung(en) gefunden.%s', $candidates->count(), $dryRun ? ' (Dry-Run)' : '', )); $published = 0; $rejected = 0; $failed = 0; $rechecked = 0; foreach ($candidates as $pressRelease) { $line = sprintf( ' #%d scheduled_at=%s title="%s"', $pressRelease->id, $pressRelease->scheduled_at?->format('Y-m-d H:i') ?? '-', Str::limit($pressRelease->title, 60), ); if ($dryRun) { $this->line($line.' [DRY]'); continue; } if ($this->classificationIsStale($pressRelease)) { ClassifyPressRelease::dispatch($pressRelease->id, route: true, contentHash: $pressRelease->content_hash)->onQueue('classification'); $rechecked++; $this->warn($line.' [RECHECK: Inhalt nach der Prüfung geändert]'); continue; } try { $service->publish($pressRelease, source: 'scheduler'); $published++; $this->line($line.' [OK]'); } catch (BlacklistViolationException $e) { $rejected++; $this->warn($line.' [REJECT: '.$e->word.']'); } catch (StaleClassificationException) { $rechecked++; $this->warn($line.' [RECHECK: Inhalt unter der Sperre abweichend, Prüfung neu eingeplant]'); } catch (Throwable $e) { $failed++; $this->error($line.' [FAIL: '.$e->getMessage().']'); report($e); } } if (! $dryRun) { $this->newLine(); $this->info(sprintf( 'Fertig: %d veröffentlicht, %d wegen Blacklist abgelehnt, %d neu zur Prüfung, %d fehlgeschlagen.', $published, $rejected, $rechecked, $failed, )); } return $failed > 0 ? self::FAILURE : self::SUCCESS; } /** * Jüngste gültige Klassifikation mit Prüfsumme, die nicht zum aktuellen * Inhalt passt. Altbestand ohne Prüfsumme (vor der Bindung) gilt weiter. */ private function classificationIsStale(PressRelease $pressRelease): bool { $latestHash = KiAudit::query() ->where('press_release_id', $pressRelease->id) ->where('type', KiAudit::TYPE_CLASSIFICATION) ->whereNull('discarded_at') ->orderByDesc('id') ->value('content_hash'); return $latestHash !== null && $pressRelease->content_hash !== null && $latestHash !== $pressRelease->content_hash; } }