Phase 9 Block 1: Gelb-Routing Direkt-Live, Slot-Verbrauch bei Veroeffentlichung, Submit-Gate

9A — Gelb geht direkt live (Entscheidung 12.06.2026):
- routeByClassification(): Gelb durchlaeuft denselben Auto-Publish-Pfad
  wie Gruen (autoPublishApproved); nur Rot wird abgelehnt
- Scheduler publiziert faellige gelbe + gruene PMs; unklassifizierte
  bleiben als Fallback in der manuellen Queue

9B — Slot-Verbrauch bei Veroeffentlichung (Decision-Update 3.2):
- Increment aus submitForReview() entfernt; publish() und
  changeStatusFromAdmin() zaehlen idempotent beim ersten
  published-Uebergang (Pruefung ueber Status-Logs); Rot kostet nichts
- Submit-Guard: Einreichen erfordert freien Slot
  (QuotaExceededException, API 422)

9C — Submit-Gate vorbereitet (Decision-Update 5.1):
- User::hasActiveBooking()-Stub hinter config/billing.php
  (enforce_booking, Default aus); Tarif-Modul ersetzt nur den Rumpf
- Einreichungs-Modal zeigt ohne Buchung einen Buchungs-Hinweis;
  Server-Guard (BookingRequiredException), API antwortet 402
- Fix: Customer-Create legte PMs bei "Zur Pruefung senden" direkt mit
  Status review an (vorbei an Blacklist/Quota/KI/Status-Log) — laeuft
  jetzt immer ueber submitForReview()

Suite: 451 passed, 4 skipped (9 neue Tests). Pint clean.
Plan: docs/PHASE-9-FLOW-UND-TARIFE-PLAN.md (Block 2 nach Review-Stopp).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-12 09:47:06 +00:00
parent 8d8d957884
commit 4419d9ff43
21 changed files with 551 additions and 114 deletions

View file

@ -8,7 +8,11 @@ use App\Models\Company;
use App\Models\Contact;
use App\Models\PressRelease;
use App\Services\Customer\CustomerCompanyContext;
use App\Services\PressRelease\BlacklistViolationException;
use App\Services\PressRelease\BookingRequiredException;
use App\Services\PressRelease\PressReleaseHtmlSanitizer;
use App\Services\PressRelease\PressReleaseService;
use App\Services\PressRelease\QuotaExceededException;
use Flux\Flux;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
@ -423,17 +427,17 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
$this->portal = $company->portal?->value ?? Portal::Presseecho->value;
$status = $submitStatus === 'review' ? PressReleaseStatus::Review : PressReleaseStatus::Draft;
$slug = (new PressRelease)->generateUniqueSlug($this->title, [
'portal' => $this->portal,
'language' => $this->language,
]);
// Immer als Entwurf anlegen — der Weg nach "review" führt ausschließlich
// über submitForReview() (Blacklist, Gate, Quota, Status-Log, KI).
$pr = PressRelease::query()->create([
'uuid' => (string) Str::uuid(),
'user_id' => $user->id,
'status' => $status->value,
'status' => PressReleaseStatus::Draft->value,
...$this->pressReleaseAttributes($slug),
]);
@ -441,15 +445,47 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
$pr->contacts()->sync([$contact->id]);
}
Flux::toast(
heading: $status === PressReleaseStatus::Review
? __('Eingereicht')
: __('Entwurf gespeichert'),
text: $status === PressReleaseStatus::Review
? __('Die Redaktion prüft die Pressemitteilung typischerweise innerhalb von 24 h.')
: __('Deine Änderungen sind gesichert. Du kannst die PM jederzeit weiter bearbeiten.'),
variant: 'success',
);
if ($submitStatus === 'review') {
$this->authorize('submitForReview', $pr);
try {
app(PressReleaseService::class)->submitForReview($pr);
} catch (BlacklistViolationException $e) {
Flux::toast(
heading: __('Automatisch abgelehnt'),
text: __('Unzulässiges Wort gefunden: ":word". Bitte überarbeiten.', ['word' => $e->word]),
variant: 'danger',
duration: 8000,
);
$this->redirect(route('me.press-releases.show', $pr->id), navigate: true);
return;
} catch (BookingRequiredException|QuotaExceededException $e) {
Flux::toast(
heading: __('Als Entwurf gespeichert'),
text: $e->getMessage(),
variant: 'warning',
duration: 8000,
);
$this->redirect(route('me.press-releases.show', $pr->id), navigate: true);
return;
}
Flux::toast(
heading: __('Eingereicht'),
text: __('Die Redaktion prüft die Pressemitteilung typischerweise innerhalb von 24 h.'),
variant: 'success',
);
} else {
Flux::toast(
heading: __('Entwurf gespeichert'),
text: __('Deine Änderungen sind gesichert. Du kannst die PM jederzeit weiter bearbeiten.'),
variant: 'success',
);
}
$this->redirect(route('me.press-releases.show', $pr->id), navigate: true);
}