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:
parent
8d8d957884
commit
4419d9ff43
21 changed files with 551 additions and 114 deletions
17
app/Services/PressRelease/BookingRequiredException.php
Normal file
17
app/Services/PressRelease/BookingRequiredException.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\PressRelease;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Submit-Gate (Decision-Update §5.1): Einreichen zur Prüfung erfordert eine
|
||||
* aktive Buchung. Speichern als Entwurf bleibt immer frei.
|
||||
*/
|
||||
class BookingRequiredException extends RuntimeException
|
||||
{
|
||||
public function __construct(string $message = 'Für das Einreichen zur Prüfung wird eine aktive Buchung benötigt.')
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,22 @@ class PressReleaseService
|
|||
{
|
||||
$this->assertStatus($pressRelease, [PressReleaseStatus::Draft, PressReleaseStatus::Rejected]);
|
||||
|
||||
$user = $pressRelease->user;
|
||||
|
||||
// Submit-Gate (Decision-Update §5.1): Einreichen erfordert eine aktive
|
||||
// Buchung. Bis zum Tarif-Modul steuert billing.enforce_booking den Stub.
|
||||
if ($user && ! $user->hasActiveBooking()) {
|
||||
throw new BookingRequiredException;
|
||||
}
|
||||
|
||||
// Slot-Guard: Der Slot wird erst bei Veröffentlichung verbraucht
|
||||
// (Decision-Update §3.2) — eingereicht werden darf aber nur, wenn
|
||||
// noch ein Slot frei ist, sonst würde eine grüne/gelbe PM ohne
|
||||
// verfügbares Kontingent automatisch veröffentlicht.
|
||||
if ($user && $user->pressReleaseQuotaRemaining() <= 0) {
|
||||
throw new QuotaExceededException;
|
||||
}
|
||||
|
||||
$previous = $pressRelease->status;
|
||||
|
||||
if ($word = $this->blacklist->findInPressRelease($pressRelease)) {
|
||||
|
|
@ -47,10 +63,6 @@ class PressReleaseService
|
|||
$pressRelease->update(['status' => PressReleaseStatus::Review->value]);
|
||||
$this->logStatusChange($pressRelease, $previous, PressReleaseStatus::Review, null, 'customer');
|
||||
|
||||
// Quota-Stub: zählt den Monatsverbrauch des Autors hoch. Wird vom
|
||||
// echten Tarif-Modul später abgelöst (Schnittstelle bleibt stabil).
|
||||
$pressRelease->user?->increment('press_release_quota_used_this_month');
|
||||
|
||||
// KI-Klassifikation asynchron anstoßen (Red Flag). Das Routing anhand
|
||||
// des Ergebnisses übernimmt der Job über routeByClassification().
|
||||
ClassifyPressRelease::dispatch($pressRelease->id)->onQueue('classification');
|
||||
|
|
@ -92,11 +104,14 @@ class PressReleaseService
|
|||
|
||||
/**
|
||||
* Routet eine frisch klassifizierte PM anhand des KI-Ergebnisses
|
||||
* (Konzept §15.1). Wird vom ClassifyPressRelease-Job aufgerufen.
|
||||
* (Decision-Update §5.0, Entscheidung 12.06.2026). Wird vom
|
||||
* ClassifyPressRelease-Job aufgerufen.
|
||||
*
|
||||
* - Rot → Ablehnung mit Begründung an den Autor
|
||||
* - Gelb → bleibt in der manuellen Review-Queue
|
||||
* - Grün → automatische Veröffentlichung (sofort bzw. zum geplanten Termin)
|
||||
* - Rot → Ablehnung mit Begründung an den Autor
|
||||
* - Gelb/Grün → automatische Veröffentlichung (sofort bzw. zum Termin);
|
||||
* Gelb bleibt als interne Markierung erhalten (nicht
|
||||
* boostbar, Admin-Signal), löst aber keine manuelle
|
||||
* Prüfung aus
|
||||
*
|
||||
* Greift nur, solange die PM noch im Status `review` steht; manuelle
|
||||
* Admin-Eingriffe in der Zwischenzeit haben damit Vorrang.
|
||||
|
|
@ -113,22 +128,18 @@ class PressReleaseService
|
|||
return;
|
||||
}
|
||||
|
||||
if ($classification === PressReleaseClassification::Green) {
|
||||
$this->autoPublishGreen($pressRelease);
|
||||
}
|
||||
|
||||
// Gelb: keine Aktion – bleibt zur manuellen Prüfung im Status „review".
|
||||
$this->autoPublishApproved($pressRelease);
|
||||
}
|
||||
|
||||
/**
|
||||
* Veröffentlicht eine grün klassifizierte PM automatisch.
|
||||
* Veröffentlicht eine als Gelb oder Grün klassifizierte PM automatisch.
|
||||
*
|
||||
* Liegt ein Veröffentlichungstermin in der Zukunft, übernimmt der
|
||||
* Scheduler die Publikation zum Termin. Andernfalls wird sofort
|
||||
* publiziert – optional mit einem Sicherheitsfenster
|
||||
* (scoring.classification.green_delay_minutes).
|
||||
*/
|
||||
private function autoPublishGreen(PressRelease $pressRelease): void
|
||||
private function autoPublishApproved(PressRelease $pressRelease): void
|
||||
{
|
||||
if ($pressRelease->scheduled_at && $pressRelease->scheduled_at->isFuture()) {
|
||||
return;
|
||||
|
|
@ -156,6 +167,8 @@ class PressReleaseService
|
|||
throw new BlacklistViolationException($reason, $word);
|
||||
}
|
||||
|
||||
$this->consumePublishSlot($pressRelease);
|
||||
|
||||
$pressRelease->update([
|
||||
'status' => PressReleaseStatus::Published->value,
|
||||
'published_at' => $this->resolvePublishedAt($pressRelease, $publishedAtOverride),
|
||||
|
|
@ -165,6 +178,27 @@ class PressReleaseService
|
|||
$this->notifyAuthor($pressRelease, 'published');
|
||||
}
|
||||
|
||||
/**
|
||||
* Zählt beim ersten Übergang zu „published" einen PM-Slot des Eigentümers
|
||||
* (Decision-Update §3.2: Slot-Verbrauch bei Veröffentlichung; abgelehnte
|
||||
* PMs kosten nichts). Erneutes Publizieren — etwa nach Archivierung —
|
||||
* zählt nicht doppelt; geprüft wird über die Status-Logs. Muss vor dem
|
||||
* Schreiben des neuen Status-Logs aufgerufen werden.
|
||||
*/
|
||||
private function consumePublishSlot(PressRelease $pressRelease): void
|
||||
{
|
||||
$alreadyPublishedOnce = PressReleaseStatusLog::query()
|
||||
->where('press_release_id', $pressRelease->id)
|
||||
->where('to_status', PressReleaseStatus::Published->value)
|
||||
->exists();
|
||||
|
||||
if ($alreadyPublishedOnce) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pressRelease->user?->increment('press_release_quota_used_this_month');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt das wirksame `published_at` einer PM.
|
||||
*
|
||||
|
|
@ -234,6 +268,10 @@ class PressReleaseService
|
|||
{
|
||||
$previous = $pressRelease->status;
|
||||
|
||||
if ($status === PressReleaseStatus::Published && $previous !== PressReleaseStatus::Published) {
|
||||
$this->consumePublishSlot($pressRelease);
|
||||
}
|
||||
|
||||
$pressRelease->update([
|
||||
'status' => $status->value,
|
||||
'published_at' => $status === PressReleaseStatus::Published
|
||||
|
|
|
|||
17
app/Services/PressRelease/QuotaExceededException.php
Normal file
17
app/Services/PressRelease/QuotaExceededException.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\PressRelease;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Das monatliche PM-Kontingent des Autors ist aufgebraucht — Einreichen ist
|
||||
* erst nach Reset/Upgrade (später: Extra-PM) wieder möglich.
|
||||
*/
|
||||
class QuotaExceededException extends RuntimeException
|
||||
{
|
||||
public function __construct(string $message = 'Das monatliche PM-Kontingent ist aufgebraucht.')
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue