press-releases: Mail "wird persoenlich geprueft" beim Block, Blade-Guard fuer @php-Bloecke (Roadmap 1d, Nachtrag)
Der Autor bekommt beim Block nach dem letzten Rot (oder Gelb mit eingeschalteter Queue) eine kurze Mail statt Stillstand: was passiert ist, dass nichts zu tun ist, und dass das Ergebnis per Mail kommt. Laeuft ueber notifyAuthor() wie Veroeffentlichung und Ablehnung (API-Regel, Sperrliste, adressbezogene Abmeldung, Bounce-Referenz); Opt-out am Typ "abgelehnt". Neue ConversionSource mail_review fuer den Link ins Konto. BladePhpBlockGuardTest wendet das Compiler-Muster auf alle Views an und faengt inline @php( vor einem spaeteren @endphp ab — fand den eigenen Erklaer-Kommentar im Modal, der nur zufaellig funktionierte. Regel in CLAUDE.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
35db30e019
commit
4fd1db7825
11 changed files with 278 additions and 24 deletions
|
|
@ -18,6 +18,9 @@ enum ConversionSource: string
|
|||
|
||||
case MailReject = 'mail_reject';
|
||||
|
||||
/** Hinweis „ein Mensch prüft jetzt" nach erschöpfter Automatik (Roadmap 1d, E3). */
|
||||
case MailReview = 'mail_review';
|
||||
|
||||
case ProfileClaim = 'profile_claim';
|
||||
|
||||
case Organic = 'organic';
|
||||
|
|
@ -27,6 +30,7 @@ enum ConversionSource: string
|
|||
return match ($this) {
|
||||
self::MailPublish => 'Veröffentlichungsmail',
|
||||
self::MailReject => 'Ablehnungsmail',
|
||||
self::MailReview => 'Prüfhinweis-Mail',
|
||||
self::ProfileClaim => 'Hinweis auf der Newsroom-Seite',
|
||||
self::Organic => 'ohne zugeordnete Herkunft',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ enum EmailNotificationType: string
|
|||
{
|
||||
return match ($this) {
|
||||
self::PressReleasePublished => 'Benachrichtigung, sobald eine von Ihnen eingereichte Pressemitteilung geprüft und veröffentlicht wurde.',
|
||||
self::PressReleaseRejected => 'Benachrichtigung, wenn eine eingereichte Pressemitteilung abgelehnt wurde — inklusive Ablehnungsgrund.',
|
||||
self::PressReleaseRejected => 'Benachrichtigung, wenn eine eingereichte Pressemitteilung abgelehnt wurde (inklusive Ablehnungsgrund) oder zur persönlichen Prüfung zurückgehalten wird.',
|
||||
self::PressReleaseDepublished => 'Bestätigung, wenn eine beauftragte Depublikation durchgeführt wurde.',
|
||||
self::PressReleaseCorrected => 'Bestätigung, wenn eine Korrektur oder kostenpflichtige Änderung an einer veröffentlichten Pressemitteilung übernommen wurde.',
|
||||
self::Newsletter => 'Gelegentliche Informationen zur Plattform: Neuigkeiten, neue Funktionen und wichtige Hinweise zu Ihrem Konto.',
|
||||
|
|
@ -52,7 +52,7 @@ enum EmailNotificationType: string
|
|||
{
|
||||
return match ($this) {
|
||||
self::PressReleasePublished => 'Sie wurden als Pressekontakt hinterlegt und werden daher über die Veröffentlichung Ihrer Pressemitteilungen informiert.',
|
||||
self::PressReleaseRejected => 'Sie wurden als Pressekontakt hinterlegt und werden daher über die Ablehnung Ihrer eingereichten Pressemitteilungen informiert.',
|
||||
self::PressReleaseRejected => 'Sie wurden als Pressekontakt hinterlegt und werden daher über das Prüfergebnis Ihrer eingereichten Pressemitteilungen informiert.',
|
||||
self::PressReleaseDepublished => 'Sie haben die Depublikation dieser Pressemitteilung beauftragt und erhalten daher diese Bestätigung.',
|
||||
self::PressReleaseCorrected => 'Sie haben die Korrektur bzw. Änderung dieser Pressemitteilung beauftragt und erhalten daher diese Bestätigung.',
|
||||
self::Newsletter => 'Sie haben ein Benutzerkonto bei pressekonto bzw. den Portalen presseecho und businessportal24 und erhalten daher gelegentlich Informationen zur Plattform.',
|
||||
|
|
@ -66,7 +66,7 @@ enum EmailNotificationType: string
|
|||
{
|
||||
return match ($this) {
|
||||
self::PressReleasePublished => 'Benachrichtigungen über das Veröffentlichen Ihrer Pressemitteilungen',
|
||||
self::PressReleaseRejected => 'Benachrichtigungen über die Ablehnung Ihrer Pressemitteilungen',
|
||||
self::PressReleaseRejected => 'Benachrichtigungen über Ablehnung und Prüfung Ihrer Pressemitteilungen',
|
||||
self::PressReleaseDepublished => 'Bestätigungen über die Depublikation Ihrer Pressemitteilungen',
|
||||
self::PressReleaseCorrected => 'Bestätigungen über Korrekturen und Änderungen Ihrer Pressemitteilungen',
|
||||
self::Newsletter => 'Newsletter und Plattform-Informationen',
|
||||
|
|
|
|||
84
app/Mail/PressReleaseHeldForReview.php
Normal file
84
app/Mail/PressReleaseHeldForReview.php
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Enums\ConversionSource;
|
||||
use App\Enums\EmailNotificationType;
|
||||
use App\Mail\Concerns\Unsubscribable;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
/**
|
||||
* „Ein Mensch prüft jetzt" (Roadmap 1d, E3): Die Meldung ist nach dem
|
||||
* letzten erlaubten Rot — oder bei Gelb mit eingeschalteter Queue — nicht
|
||||
* abgelehnt, sondern zur persönlichen Prüfung zurückgehalten. Der Autor
|
||||
* muss nichts tun; das Ergebnis kommt als Veröffentlichungs- oder
|
||||
* Ablehnungsmail. Opt-out läuft über den Typ „abgelehnt": Wer keine
|
||||
* Prüfergebnisse will, will auch diesen Zwischenstand nicht.
|
||||
*/
|
||||
class PressReleaseHeldForReview extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels, Unsubscribable;
|
||||
|
||||
public string $showUrl;
|
||||
|
||||
/**
|
||||
* Name für die Anrede — bei API-Einlieferungen der Pressekontakt bzw.
|
||||
* die Firma, nie der Kontoname des API-Einlieferers.
|
||||
*/
|
||||
public string $recipientName;
|
||||
|
||||
public int $maxAttempts;
|
||||
|
||||
public function __construct(
|
||||
public readonly User $user,
|
||||
public readonly PressRelease $pressRelease,
|
||||
public readonly bool $automationExhausted,
|
||||
?string $recipientName = null,
|
||||
?string $recipientEmail = null,
|
||||
) {
|
||||
$this->recipientName = $recipientName ?? $user->name;
|
||||
$this->maxAttempts = max(1, (int) config('scoring.classification.max_automatic_attempts', 3));
|
||||
|
||||
$showUrl = $user->canAccessAdmin()
|
||||
? route('admin.press-releases.show', $pressRelease->id)
|
||||
: route('me.press-releases.show', $pressRelease->id);
|
||||
|
||||
$this->showUrl = $showUrl.(str_contains($showUrl, '?') ? '&' : '?').http_build_query([
|
||||
'src' => ConversionSource::MailReview->value,
|
||||
'pr' => $pressRelease->id,
|
||||
]);
|
||||
|
||||
if ($recipientEmail !== null && mb_strtolower(trim($recipientEmail)) !== mb_strtolower((string) $user->email)) {
|
||||
$this->configureAddressUnsubscribe($recipientEmail, EmailNotificationType::PressReleaseRejected);
|
||||
} else {
|
||||
$this->configureUnsubscribe($user, EmailNotificationType::PressReleaseRejected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounce-Referenz für den DSN-Parser (Roadmap 1b.6), gesetzt über den
|
||||
* gemeinsamen headers() des Unsubscribable-Traits.
|
||||
*/
|
||||
public function bounceReference(): string
|
||||
{
|
||||
return sprintf('pm:%d:company:%s', $this->pressRelease->id, $this->pressRelease->company_id ?? '-');
|
||||
}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Ihre Pressemitteilung wird persönlich geprüft: '.$this->pressRelease->title,
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(view: 'emails.press-release.held-for-review');
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ use App\Enums\PressReleaseStatus;
|
|||
use App\Enums\SinglePurchaseStatus;
|
||||
use App\Jobs\ClassifyPressRelease;
|
||||
use App\Jobs\ScorePressRelease;
|
||||
use App\Mail\PressReleaseHeldForReview;
|
||||
use App\Mail\PressReleasePublished;
|
||||
use App\Mail\PressReleaseRejected;
|
||||
use App\Models\AdminPreset;
|
||||
|
|
@ -209,7 +210,7 @@ class PressReleaseService
|
|||
$pressRelease->auto_check_attempts,
|
||||
$max,
|
||||
$reason ?: 'Rot ohne Begründung.',
|
||||
));
|
||||
), automationExhausted: true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -222,7 +223,7 @@ class PressReleaseService
|
|||
// Gelb geht durch (Entscheidung 16.09.2026) — außer der Schalter für
|
||||
// die manuelle Queue ist bewusst eingeschaltet.
|
||||
if ($classification === PressReleaseClassification::Yellow && config('scoring.classification.yellow_to_manual_queue')) {
|
||||
$this->holdForManualReview($pressRelease, 'Gelb — manuelle Prüfung laut Konfiguration.');
|
||||
$this->holdForManualReview($pressRelease, 'Gelb — manuelle Prüfung laut Konfiguration.', automationExhausted: false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -232,10 +233,11 @@ class PressReleaseService
|
|||
|
||||
/**
|
||||
* Meldung bleibt in `review`, wird als „Prüfung nötig" markiert und
|
||||
* wartet auf die Entscheidung im Admin. Kein Slot-Verbrauch, keine Mail
|
||||
* an den Autor — der sieht den Stand in seinem Konto.
|
||||
* wartet auf die Entscheidung im Admin. Kein Slot-Verbrauch. Der Autor
|
||||
* bekommt eine kurze Mail „ein Mensch prüft jetzt" — ohne sie sähe er
|
||||
* nach dem letzten Rot nur Stillstand statt einer Ablehnung.
|
||||
*/
|
||||
private function holdForManualReview(PressRelease $pressRelease, string $reason): void
|
||||
private function holdForManualReview(PressRelease $pressRelease, string $reason, bool $automationExhausted): void
|
||||
{
|
||||
$pressRelease->update(['manual_review_required_at' => now()]);
|
||||
$this->logStatusChange($pressRelease, PressReleaseStatus::Review, PressReleaseStatus::Review, $reason, 'ki');
|
||||
|
|
@ -246,6 +248,8 @@ class PressReleaseService
|
|||
subject: $pressRelease,
|
||||
actorLabel: 'ki',
|
||||
);
|
||||
|
||||
$this->notifyAuthor($pressRelease, 'held', automationExhausted: $automationExhausted);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -723,7 +727,7 @@ class PressReleaseService
|
|||
$this->adminCache->forgetPressReleaseStats();
|
||||
}
|
||||
|
||||
private function notifyAuthor(PressRelease $pressRelease, string $event, ?string $reason = null): void
|
||||
private function notifyAuthor(PressRelease $pressRelease, string $event, ?string $reason = null, bool $automationExhausted = false): void
|
||||
{
|
||||
/** @var User|null $user */
|
||||
$user = $pressRelease->user;
|
||||
|
|
@ -778,10 +782,11 @@ class PressReleaseService
|
|||
// Opt-out-Gate: Benachrichtigungs-Mails sind je Typ abbestellbar
|
||||
// (One-Click-Unsubscribe aus der Mail bzw. Präferenzen im Profil).
|
||||
// Greift nur, wenn der Einlieferer selbst der Empfänger ist —
|
||||
// Pressekontakt/Firma haben kein Konto mit Präferenzen.
|
||||
// Pressekontakt/Firma haben kein Konto mit Präferenzen. Der
|
||||
// Zwischenstand „ein Mensch prüft" hängt am Typ „abgelehnt".
|
||||
$notificationType = match ($event) {
|
||||
'published' => EmailNotificationType::PressReleasePublished,
|
||||
'rejected' => EmailNotificationType::PressReleaseRejected,
|
||||
'rejected', 'held' => EmailNotificationType::PressReleaseRejected,
|
||||
default => null,
|
||||
};
|
||||
|
||||
|
|
@ -793,9 +798,11 @@ class PressReleaseService
|
|||
return;
|
||||
}
|
||||
|
||||
$mailable = $notificationType === EmailNotificationType::PressReleasePublished
|
||||
? new PressReleasePublished($user, $pressRelease, $recipientName, $recipient)
|
||||
: new PressReleaseRejected($user, $pressRelease, $reason, $recipientName, $recipient);
|
||||
$mailable = match ($event) {
|
||||
'published' => new PressReleasePublished($user, $pressRelease, $recipientName, $recipient),
|
||||
'held' => new PressReleaseHeldForReview($user, $pressRelease, $automationExhausted, $recipientName, $recipient),
|
||||
default => new PressReleaseRejected($user, $pressRelease, $reason, $recipientName, $recipient),
|
||||
};
|
||||
|
||||
Mail::to($recipient)->queue($mailable);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue