presseportale/routes/console.php
Kevin Adametz e77d9e7d09 feat: API-Zugang endet mit der Buchung, optionales Ablaufdatum für API-Tokens
Bisher blieb ein einmal erstellter Token gültig, auch wenn Abo bzw. Buchung
längst beendet waren. EnsureApiAccessIsBooked prüft jetzt bei jeder Anfrage
(v1 und /service/…-Altpfade), ob das Konto API-Zugang hat
(ApiAccessEligibilityService::hasApiAccess), sonst 403. Die Tokens bleiben
erhalten und funktionieren nach erneuter Buchung wieder.

Kunden können beim Erstellen optional ein Ablaufdatum setzen (bis 5 Jahre);
die Token-Liste zeigt „Gültig bis“ und einen Hinweis, wenn der Zugang
pausiert ist. sanctum:prune-expired räumt abgelaufene Tokens nach 30 Tagen
täglich ab. API-Doku (public/api/v1.yml) ergänzt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:41:24 +00:00

272 lines
11 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Console\Commands\AggregatePressReleaseViews;
use App\Console\Commands\ArchiveExportTombstones;
use App\Console\Commands\ArchivePurge;
use App\Console\Commands\ArchiveRetryPending;
use App\Console\Commands\ArchiveSweepOrphans;
use App\Console\Commands\CheckIndexStatus;
use App\Console\Commands\CheckMailDomains;
use App\Console\Commands\ExpireGrandfathering;
use App\Console\Commands\FetchMailBounces;
use App\Console\Commands\GenerateManualInvoices;
use App\Console\Commands\MonitoringHeartbeat;
use App\Console\Commands\PublishScheduledPressReleases;
use App\Console\Commands\PurgeConversionEvents;
use App\Console\Commands\PurgeExpiredPressReleaseDrafts;
use App\Console\Commands\PurgeMagicLinks;
use App\Console\Commands\PurgePressReleaseEvents;
use App\Console\Commands\PurgeRedactionSnapshots;
use App\Console\Commands\RefreshMarketData;
use App\Console\Commands\ResetMonthlyPressReleaseQuota;
use App\Console\Commands\SweepOrphanPressReleaseOriginals;
use App\Models\FailedLoginTally;
use App\Support\OperationalAlert;
use Illuminate\Console\Scheduling\Event;
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
/**
* Betriebs-Alert bei fehlgeschlagenem Scheduler-Task (R5.3): meldet den Task
* per Log + Best-Effort-Mail, damit ein stiller Ausfall (z. B. Rechnungslauf,
* Kontingent-Reset) nicht unbemerkt bleibt.
*/
$alertOnFailure = fn (string $task) => function (Event $event) use ($task): void {
OperationalAlert::send("Geplanter Task fehlgeschlagen: {$task}", [
'exit_code' => $event->exitCode,
]);
};
// Sperren von withoutOverlapping() (Audit K-09): Ohne Angabe hält Laravel
// sie 24 h. Stürzt ein Lauf ab (OOM, Deploy, Container-Neustart), würde der
// Task bis zu einen Tag still übersprungen. Jede Sperre endet deshalb kurz
// nach der erwarteten Höchstlaufzeit: häufige Tasks nach ihrem Intervall,
// nächtliche nach 30–180 Minuten je nach Datenmenge.
// ========================================
// Tägliche Bereinigungen
// ========================================
// Bounce-Postfach: Rückläufer lesen und unzustellbare Adressen sperren
// (Roadmap 1b.6). Alle zehn Minuten auf UNSEEN; ohne konfiguriertes Konto
// beendet sich der Befehl sofort.
Schedule::command(FetchMailBounces::class)
->everyTenMinutes()
->withoutOverlapping(10)
->runInBackground()
->onFailure($alertOnFailure('mail:fetch-bounces'));
// Kontakterreichbarkeit: abgelaufene DNS-Befunde der Mail-Domains erneuern.
// Nur Auffrischung — neue Adressen prüft der Observer beim Anlegen. Ein
// Befund darf nicht unbegrenzt als Domainsperre stehen bleiben.
Schedule::command(CheckMailDomains::class, ['--stale', '--limit=500'])
->dailyAt('04:20')
->withoutOverlapping(60)
->runInBackground()
->onFailure($alertOnFailure('mail:check-domains'));
// Magic-Links: Abgelaufene / verbrauchte Tokens entfernen (täglich 03:00)
Schedule::command(PurgeMagicLinks::class, ['--days=30'])
->dailyAt('03:00')
->withoutOverlapping(30)
->runInBackground()
->onFailure($alertOnFailure('magic-links:purge'));
// Meldeverfahren: Schwärzungs-Snapshots nach Löschfrist entfernen (täglich 03:10)
Schedule::command(PurgeRedactionSnapshots::class)
->dailyAt('03:10')
->withoutOverlapping(30)
->runInBackground()
->onFailure($alertOnFailure('snapshots:purge'));
// Beweisarchiv (Bildkennzeichnung Block F.3): fällige Einträge löschen (03:20),
// Tombstone-Stand exportieren (03:30), damit er einen Restore überlebt.
Schedule::command(ArchivePurge::class)
->dailyAt('03:20')
->withoutOverlapping(120)
->runInBackground()
->onFailure($alertOnFailure('archive:purge'));
// Bewusst eigener Termin statt ->then() an archive:purge (OPT-6, K-09):
// Als Anhängsel liefe der Export nach einem gescheiterten Purge gar nicht
// (onSuccess) bzw. ohne eigenen Failure-Alarm (then). So bleibt der Export
// auch dann aktuell und meldet sich selbst. Die Sperren enden nach 2 h bzw.
// 1 h; ein Purge, der länger als 10 Minuten läuft, überschneidet sich mit dem
// Export — der Export liest nur und nimmt den nächsten Stand am Folgetag mit.
Schedule::command(ArchiveExportTombstones::class)
->dailyAt('03:30')
->withoutOverlapping(60)
->runInBackground()
->onFailure($alertOnFailure('archive:export-tombstones'));
// Fehlgeschlagene Archiveinträge nachholen (03:35) und Payloads ohne Zeile
// nach 24 h Karenz entfernen (03:40) — Umsetzungsaudit 25.09.2026, N11.
Schedule::command(ArchiveRetryPending::class)
->dailyAt('03:35')
->withoutOverlapping(60)
->runInBackground()
->onFailure($alertOnFailure('archive:retry-pending'));
Schedule::command(ArchiveSweepOrphans::class)
->dailyAt('03:40')
->withoutOverlapping(120)
->runInBackground()
->onFailure($alertOnFailure('archive:sweep-orphans'));
// PM-Entwürfe: Zombie-Drafts nach 180 Tagen Inaktivität archivieren (wöchentlich)
Schedule::command(PurgeExpiredPressReleaseDrafts::class, ['--days=180'])
->weekly()
->sundays()
->at('04:00')
->withoutOverlapping(180)
->runInBackground()
->onFailure($alertOnFailure('press-releases:purge-drafts'));
// Beweisoriginale ohne Bildzeile (Umsetzungsaudit 29.09.2026, B-11): Reste
// eines hart abgebrochenen Uploads. 48 h Karenz schützt laufende Uploads;
// während eines Restore-Abgleichs (Archiv-Sweep pausiert) löscht er nichts.
Schedule::command(SweepOrphanPressReleaseOriginals::class, ['--execute', '--hours=48'])
->weekly()
->sundays()
->at('04:45')
->withoutOverlapping(60)
->runInBackground()
->onFailure($alertOnFailure('press-release-images:sweep-orphan-originals'));
// ========================================
// Geplante PM-Veröffentlichung
// ========================================
// PM mit scheduled_at <= now & Status review automatisch veröffentlichen.
// Läuft alle 5 Min — passt zum FormRule "scheduled_at min. 5 Min in Zukunft".
Schedule::command(PublishScheduledPressReleases::class)
->everyFiveMinutes()
->withoutOverlapping(10)
->runInBackground()
->onFailure($alertOnFailure('press-releases:publish-scheduled'));
// ========================================
// Aufruf-Zähler
// ========================================
// Gezählte Aufrufe aus dem Ereignis-Log in press_releases.hits verbuchen.
// Bewusst außerhalb des Requests: der Schreibzugriff auf die große Tabelle
// darf keine Web-Worker blockieren (siehe AggregatePressReleaseViews).
Schedule::command(AggregatePressReleaseViews::class)
->everyMinute()
->withoutOverlapping(15)
->runInBackground()
->onFailure($alertOnFailure('press-releases:aggregate-views'));
// Ereignis-Log auf den Aufbewahrungszeitraum begrenzen (täglich 03:50 —
// nicht mehr zeitgleich mit archive:purge um 03:20, Audit K-09).
// Die Tabelle nimmt jeden Seitenaufruf auf und wuchs als einzige Log-Tabelle
// des Systems unbegrenzt. Gelöscht wird nur unterhalb der Wasserstandsmarke —
// noch nicht in `hits` verbuchte Aufrufe bleiben unangetastet.
Schedule::command(PurgePressReleaseEvents::class)
->dailyAt('03:50')
->withoutOverlapping(120)
->runInBackground()
->onFailure($alertOnFailure('press-releases:purge-events'));
// Indexierungsstatus aus der Search Console (Roadmap 1c.4), täglich 05:10.
// Ohne GSC_ENABLED/Schlüsseldatei beendet sich der Befehl sofort.
Schedule::command(CheckIndexStatus::class)
->dailyAt('05:10')
->withoutOverlapping(60)
->runInBackground()
->onFailure($alertOnFailure('press-releases:check-index-status'));
// Konversions-Ereignisse auf den Aufbewahrungszeitraum begrenzen (täglich 03:25).
// Frist zusammen mit der Tabelle festgelegt (395 Tage), siehe config/conversion.php.
Schedule::command(PurgeConversionEvents::class)
->dailyAt('03:25')
->withoutOverlapping(60)
->runInBackground()
->onFailure($alertOnFailure('conversion:purge-events'));
// ========================================
// Manueller Rechnungskreis (MAN-) — Legacy-Zahlungen
// ========================================
// Bestandsschutz mit überschrittenem Legacy-Vertragsende beenden (Audit
// F06) — vor dem Rechnungslauf, damit kein beendeter Vertrag mehr
// abgerechnet wird.
Schedule::command(ExpireGrandfathering::class)
->dailyAt('04:15')
->withoutOverlapping(30)
->runInBackground()
->onFailure($alertOnFailure('billing:expire-grandfathering'));
// Fällige Rechnungen für laufende Legacy-Zahlungsvereinbarungen ausstellen
// (Periodenende erreicht), wie im Altsystem. Neue Abschlüsse laufen über
// Stripe (STR-Kreis) und werden hier nicht angefasst.
Schedule::command(GenerateManualInvoices::class)
->dailyAt('04:30')
->withoutOverlapping(60)
->runInBackground()
->onFailure($alertOnFailure('billing:generate-manual-invoices'));
// ========================================
// PM-Kontingent (Kalendermonats-Reset des Tarif-Modells)
// ========================================
// Monatlicher Reset des verbrauchten PM-Kontingents (am 1. um 00:05).
Schedule::command(ResetMonthlyPressReleaseQuota::class)
->monthlyOn(1, '00:05')
->withoutOverlapping(30)
->runInBackground()
->onFailure($alertOnFailure('press-releases:reset-monthly-quota'));
// ========================================
// Börsendaten für den Web-Ticker
// ========================================
// Kurse (DAX, EUR/USD, Brent) alle 15 Minuten aktualisieren; die Views lesen
// nur aus dem Cache — ein API-Ausfall leert die Anzeige nicht sofort.
Schedule::command(RefreshMarketData::class)
->everyFifteenMinutes()
->withoutOverlapping(15)
->runInBackground()
->onFailure($alertOnFailure('market:refresh'));
// ========================================
// Betriebsüberwachung (Monitoring-Plan Phase 1)
// ========================================
// Herzschlag: Fällt der Scheduler komplett aus (Cron weg, Server hängt),
// greift kein onFailure mehr — dann meldet /health den fehlenden Herzschlag.
Schedule::command(MonitoringHeartbeat::class)
->everyMinute()
->withoutOverlapping(2)
->onFailure($alertOnFailure('monitoring:heartbeat'));
// Tageszähler fehlgeschlagener Anmeldungen nach der Löschfrist entfernen
// (täglich 04:05, config monitoring.failed_logins.retention_days).
Schedule::command('model:prune', ['--model' => [FailedLoginTally::class]])
->dailyAt('04:05')
->withoutOverlapping(30)
->runInBackground()
->onFailure($alertOnFailure('model:prune failed_login_tallies'));
// Fehlgeschlagene Jobs nach 30 Tagen entfernen (täglich 03:55). Bis dahin
// sind sie im Systemstatus sichtbar und per `queue:retry` wiederholbar.
Schedule::command('queue:prune-failed', ['--hours=720'])
->dailyAt('03:55')
->withoutOverlapping(30)
->runInBackground()
->onFailure($alertOnFailure('queue:prune-failed'));
// Abgelaufene API-Tokens (optionales Ablaufdatum, 08.10.2026) 30 Tage nach
// Ablauf entfernen (täglich 04:10). Bis dahin zeigt die Token-Seite sie als
// „abgelaufen“ an; Sanctum lehnt sie ab dem Ablauf ohnehin ab.
Schedule::command('sanctum:prune-expired', ['--hours=720'])
->dailyAt('04:10')
->withoutOverlapping(30)
->runInBackground()
->onFailure($alertOnFailure('sanctum:prune-expired'));