assertStatus($pressRelease, [PressReleaseStatus::Draft, PressReleaseStatus::Rejected]); $previous = $pressRelease->status; if ($word = $this->blacklist->findInPressRelease($pressRelease)) { $reason = sprintf('Automatische Ablehnung: unzulässiges Wort "%s" gefunden.', $word); $pressRelease->update(['status' => PressReleaseStatus::Rejected->value]); $this->logStatusChange($pressRelease, $previous, PressReleaseStatus::Rejected, $reason, 'blacklist'); $this->notifyAuthor($pressRelease, 'rejected', $reason); throw new BlacklistViolationException($reason, $word); } $pressRelease->update(['status' => PressReleaseStatus::Review->value]); $this->logStatusChange($pressRelease, $previous, PressReleaseStatus::Review, null, 'customer'); } public function publish(PressRelease $pressRelease, string $source = 'admin'): void { $this->assertStatus($pressRelease, [PressReleaseStatus::Review]); $previous = $pressRelease->status; if ($word = $this->blacklist->findInPressRelease($pressRelease)) { $reason = sprintf('Automatische Ablehnung: unzulässiges Wort "%s" gefunden.', $word); $pressRelease->update(['status' => PressReleaseStatus::Rejected->value]); $this->logStatusChange($pressRelease, $previous, PressReleaseStatus::Rejected, $reason, 'blacklist'); $this->notifyAuthor($pressRelease, 'rejected', $reason); throw new BlacklistViolationException($reason, $word); } $pressRelease->update([ 'status' => PressReleaseStatus::Published->value, 'published_at' => $this->resolvePublishedAt($pressRelease), ]); $this->logStatusChange($pressRelease, $previous, PressReleaseStatus::Published, null, $source); $this->notifyAuthor($pressRelease, 'published'); } /** * Bestimmt das wirksame `published_at` einer PM. * * Reihenfolge: * 1. Bereits gesetztes `published_at` bleibt erhalten (z.B. Re-Publish) * 2. `scheduled_at` (geplanter Veröffentlichungstermin) hat Vorrang vor "jetzt" * 3. `embargo_at` (Sperrfrist) verschiebt zusätzlich nach hinten — egal ob * Scheduled vorhanden ist oder nicht * 4. Fallback: now() * * Damit wirken sowohl Scheduling als auch Embargo automatisch über den * vorhandenen Sichtbarkeits-Filter `where(published_at <= now())` im * öffentlichen Listing. */ private function resolvePublishedAt(PressRelease $pressRelease): Carbon { if ($pressRelease->published_at) { return $pressRelease->published_at; } $base = $pressRelease->scheduled_at ?: now(); if ($pressRelease->embargo_at && $pressRelease->embargo_at->greaterThan($base)) { return $pressRelease->embargo_at; } return $base; } public function reject(PressRelease $pressRelease, ?string $reason = null): void { $this->assertStatus($pressRelease, [PressReleaseStatus::Review]); $previous = $pressRelease->status; $pressRelease->update(['status' => PressReleaseStatus::Rejected->value]); $this->logStatusChange($pressRelease, $previous, PressReleaseStatus::Rejected, $reason, 'admin'); $this->notifyAuthor($pressRelease, 'rejected', $reason); } public function backToDraft(PressRelease $pressRelease): void { $this->assertStatus($pressRelease, [PressReleaseStatus::Review, PressReleaseStatus::Rejected]); $previous = $pressRelease->status; $pressRelease->update(['status' => PressReleaseStatus::Draft->value]); $this->logStatusChange($pressRelease, $previous, PressReleaseStatus::Draft, null, 'admin'); } public function archive(PressRelease $pressRelease): void { $this->assertStatus($pressRelease, [PressReleaseStatus::Published]); $previous = $pressRelease->status; $pressRelease->update(['status' => PressReleaseStatus::Archived->value]); $this->logStatusChange($pressRelease, $previous, PressReleaseStatus::Archived, null, 'admin'); } public function changeStatusFromAdmin(PressRelease $pressRelease, PressReleaseStatus $status, ?string $reason = null): void { $previous = $pressRelease->status; $pressRelease->update([ 'status' => $status->value, 'published_at' => $status === PressReleaseStatus::Published ? ($pressRelease->published_at ?? now()) : $pressRelease->published_at, ]); if ($previous !== $status) { $this->logStatusChange($pressRelease, $previous, $status, $reason, 'admin'); } } public function deleteFromAdmin(PressRelease $pressRelease): void { if ($pressRelease->status === PressReleaseStatus::Published) { $pressRelease->update([ 'status' => PressReleaseStatus::Archived->value, 'text' => AdminPreset::activeValue( AdminPreset::PRESS_RELEASE_DELETED_PUBLISHED_TEXT, "Diese Pressemitteilung wurde entfernt.\n\nDer Inhalt ist nicht mehr verfuegbar." ), 'keywords' => null, 'backlink_url' => null, 'no_export' => true, ]); return; } $pressRelease->delete(); } /** * @param PressReleaseStatus[] $allowed */ private function assertStatus(PressRelease $pressRelease, array $allowed): void { if (! in_array($pressRelease->status, $allowed, true)) { $allowedValues = implode(', ', array_map(fn ($s) => $s->value, $allowed)); $currentStatus = $pressRelease->status instanceof PressReleaseStatus ? $pressRelease->status->value : (string) $pressRelease->status; throw new \LogicException( "Statusübergang nicht erlaubt. Aktueller Status: {$currentStatus}, erwartet: {$allowedValues}" ); } } private function logStatusChange( PressRelease $pressRelease, ?PressReleaseStatus $from, PressReleaseStatus $to, ?string $reason, string $source, ): void { PressReleaseStatusLog::query()->create([ 'press_release_id' => $pressRelease->id, 'changed_by_user_id' => Auth::id(), 'from_status' => $from?->value, 'to_status' => $to->value, 'reason' => $reason, 'source' => $source, 'created_at' => now(), ]); Cache::forget(AdminPerformanceCache::PressReleaseStats); Cache::forget(AdminPerformanceCache::PressReleaseReviewCount); } private function notifyAuthor(PressRelease $pressRelease, string $event, ?string $reason = null): void { $user = $pressRelease->user; if (! $user || ! $user->email) { return; } $mailable = match ($event) { 'published' => new PressReleasePublished($user, $pressRelease), 'rejected' => new PressReleaseRejected($user, $pressRelease, $reason), default => null, }; if ($mailable) { Mail::to($user->email)->queue($mailable); } } }