admin: Boost vorzeitig beenden, Berechtigungsliste audit:admins

Admin ▸ Pressemitteilung zeigt laufende Boosts; „Boost beenden“ verlangt
einen Grund und billing:manage, beendet laufende und angeschlossene
Zeiträume, verwirft die Seiten-Caches und protokolliert „Boost beendet“.
Credits werden nicht zurückgebucht.

audit:admins listet alle Konten mit Rechten über den Kundenbereich hinaus
(Super-Admin, Rollen außer customer/api-only, direkte Permissions), auch
gesperrte und gelöschte, als Markdown mit 2FA, Passkeys, Tokens und letztem
Sitzungswiderruf. E-Mail standardmäßig nur mit Domain (Nachweis BN-02).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-10-13 10:16:21 +00:00
parent 2150d4b7ae
commit fce7baea2b
6 changed files with 419 additions and 0 deletions

View file

@ -2,6 +2,7 @@
namespace App\Services\PressRelease;
use App\Enums\ActivityType;
use App\Enums\BoostPlacement;
use App\Enums\Portal;
use App\Enums\PressReleaseClassification;
@ -11,6 +12,7 @@ use App\Models\Boost;
use App\Models\PressRelease;
use App\Models\User;
use App\Scopes\PortalScope;
use App\Services\ActivityLog;
use App\Services\Billing\CreditPricingService;
use App\Services\Billing\CreditWalletService;
use App\Support\WebPageCache;
@ -35,6 +37,7 @@ class BoostService
public function __construct(
private readonly CreditPricingService $pricing,
private readonly CreditWalletService $wallet,
private readonly ActivityLog $activityLog,
) {}
/**
@ -180,6 +183,52 @@ class BoostService
return $boost;
}
/**
* Beendet alle laufenden und bereits angeschlossenen Boosts einer PM
* sofort (Admin ▸ Pressemitteilung), etwa nach einer Test-Buchung oder
* wenn die Platzierung nicht mehr vertretbar ist. Die Zeilen bleiben als
* Buchungsnachweis erhalten; Credits werden nicht zurückgebucht —
* Erstattungen laufen getrennt.
*
* @return int Anzahl der beendeten Boosts
*/
public function endActive(PressRelease $pressRelease, User $endedBy, string $reason): int
{
$now = now();
$boosts = $pressRelease->boosts()->active()->get();
if ($boosts->isEmpty()) {
return 0;
}
foreach ($boosts as $boost) {
$boost->update([
'starts_at' => $boost->starts_at->isFuture() ? $now : $boost->starts_at,
'ends_at' => $now,
]);
}
$boosts->pluck('placement')->unique()->each(
fn (BoostPlacement $placement) => $this->forgetPlacementCaches($pressRelease, $placement),
);
$this->activityLog->record(
ActivityType::BoostEnded,
sprintf(
'%s hat %d gebuchte Platzierung(en) von PM #%d vorzeitig beendet. Grund: %s',
$endedBy->name,
$boosts->count(),
$pressRelease->id,
$reason,
),
user: $endedBy,
subject: $pressRelease,
meta: ['boost_ids' => $boosts->modelKeys(), 'ended_by' => $endedBy->id, 'reason' => $reason],
);
return $boosts->count();
}
/**
* Serialisiert Käufe, die dasselbe sichtbare Slot-Inventar verändern.
*