Legacy-API-Paritaet S1+S2: connektar-Flow verhaltensgleich + KI-Schalter

Eigene Compat-Controller (Api/Legacy) fuer die 5 real genutzten
/service-Endpoints (Prod-Log-Beleg): Company-/Kontakt-Create (liefen
410!), PM-Create, Bild-Upload, published-only-GET — verschachtelte
Alt-Params, 201+Location+{}, Alt-Fehlerformat, Hit-Zaehlung,
Legacy-Land-Mapping.

- Anlegen = Einreichen (Alt-Verhalten); Sofort-Publish via neuer
  Permission api:immediate-publication (WS-7: connektar zuweisen)
- Admin-Schalter 'KI-Pruefung Legacy-API' (app_settings-Tabelle,
  Toggle auf /admin/press-releases): an = review + KI-Routing
- Admin-PM-Liste: KI-Spalte (Ampel-Badge) + manueller Pruef-Button
  ohne Routing fuer ungepruefte PMs
- Mails bei API-Einlieferern an Pressekontakt, sonst Firmen-Mail,
  Fallback Einlieferer (User-Entscheidung 10.07.)
- Tests: LegacyServiceParityTest (connektar-Flow end-to-end);
  Suite 1182 gruen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-10 14:09:40 +00:00
parent fef45733df
commit a3fd361ece
15 changed files with 906 additions and 24 deletions

View file

@ -2,18 +2,22 @@
namespace App\Services\PressRelease;
use App\Enums\EmailNotificationType;
use App\Enums\PressReleaseClassification;
use App\Enums\PressReleaseStatus;
use App\Enums\RegistrationType;
use App\Enums\SinglePurchaseStatus;
use App\Jobs\ClassifyPressRelease;
use App\Jobs\ScorePressRelease;
use App\Mail\PressReleasePublished;
use App\Mail\PressReleaseRejected;
use App\Models\AdminPreset;
use App\Models\Company;
use App\Models\PressRelease;
use App\Models\PressReleaseAiDeclaration;
use App\Models\PressReleaseImage;
use App\Models\PressReleaseStatusLog;
use App\Models\User;
use App\Services\Admin\AdminPerformanceCache;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
@ -478,12 +482,45 @@ class PressReleaseService
private function notifyAuthor(PressRelease $pressRelease, string $event, ?string $reason = null): void
{
/** @var User|null $user */
$user = $pressRelease->user;
if (! $user || ! $user->email) {
return;
}
// API-Einlieferer (z. B. connektar) nicht zuspammen (Entscheidung
// 10.07.2026): Statt-Mail an den Pressekontakt der PM, sonst an die
// Firmen-E-Mail; nur ohne beides an den Einlieferer selbst.
$recipient = $user->email;
$isApiSubmitter = $user->registration_type === RegistrationType::ApiUser;
if ($isApiSubmitter) {
// Scope-frei auflösen: der Mail-Versand läuft auch aus Jobs/
// Kontexten ohne passenden Portal-Host (PortalScope).
$recipient = $pressRelease->contacts()->withoutGlobalScopes()->whereNotNull('email')->value('email')
?? Company::withoutGlobalScopes()->find($pressRelease->company_id)?->email
?: $user->email;
}
// 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.
$notificationType = match ($event) {
'published' => EmailNotificationType::PressReleasePublished,
'rejected' => EmailNotificationType::PressReleaseRejected,
default => null,
};
if (! $notificationType) {
return;
}
if ($recipient === $user->email && ! $user->wantsEmailNotification($notificationType)) {
return;
}
$mailable = match ($event) {
'published' => new PressReleasePublished($user, $pressRelease),
'rejected' => new PressReleaseRejected($user, $pressRelease, $reason),
@ -491,7 +528,7 @@ class PressReleaseService
};
if ($mailable) {
Mail::to($user->email)->queue($mailable);
Mail::to($recipient)->queue($mailable);
}
}
}