T12: Depublikations-Checkout mit Tombstone (config-gated)

Kunden koennen eigene veroeffentlichte PMs kostenpflichtig aus der
oeffentlichen Ansicht nehmen: Panel auf der PM-Detailseite (inkl.
T1neu-Unternehmer-Checkbox und Doppelausweis netto/brutto), Stripe-
Ad-hoc-Checkout wie Credit-Topups, Erfuellung im Webhook — die PM
wechselt auf archived + depublished_at, die oeffentliche URL zeigt
dauerhaft einen Tombstone (410 Gone, ohne Inhalte, mit ehrlichem
Drittarchive-Hinweis). Doppelbuchungs-Bremse pro PM; Status-Log mit
Quelle "depublication". Gate: ohne BILLING_DEPUBLICATION_PRICE_CENTS
bleibt der Weg deaktiviert und /melden zeigt die neutrale
Preisformulierung [EXTERN: Preis festlegen, O6].

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-06 09:46:25 +00:00
parent 2a9be7a9af
commit 198be381d6
16 changed files with 522 additions and 7 deletions

View file

@ -121,6 +121,26 @@ class StripeCheckoutService
]);
}
/**
* Stripe-Checkout für eine Depublikation (T12). Ad-hoc-Charge über den
* konfigurierten Netto-Preis (wie Credit-Topups, keine Stripe-Price-ID);
* der Webhook depubliziert die PM nach Zahlungseingang.
*/
public function forDepublication(User $user, SinglePurchase $purchase): Checkout
{
$this->syncTaxIdFromBillingAddress($user);
return $user->checkoutCharge(
$purchase->price_cents,
'Depublikation einer Pressemitteilung',
1,
[
...$this->sessionOptions(),
'metadata' => ['single_purchase_id' => (string) $purchase->id],
],
);
}
/**
* Stripe-Checkout für eine Wallet-Aufladung. Ad-hoc-Charge über den
* Netto-Paketpreis (Stripe Tax ergänzt die USt.); die `credit_topup_id`

View file

@ -321,6 +321,25 @@ class PressReleaseService
$this->logStatusChange($pressRelease, $previous, PressReleaseStatus::Archived, null, 'admin');
}
/**
* Depublikation (T12): kostenpflichtige Entfernung aus der öffentlichen
* Ansicht auf Wunsch des Absenders. Die PM wechselt auf "archived" und
* erhält den Depublikations-Marker — die öffentliche Detail-URL zeigt
* fortan einen Tombstone (410).
*/
public function depublish(PressRelease $pressRelease): void
{
$this->assertStatus($pressRelease, [PressReleaseStatus::Published]);
$previous = $pressRelease->status;
$pressRelease->update([
'status' => PressReleaseStatus::Archived->value,
'depublished_at' => now(),
]);
$this->logStatusChange($pressRelease, $previous, PressReleaseStatus::Archived, 'Depublikation auf Wunsch des Absenders (kostenpflichtige Serviceleistung)', 'depublication');
}
public function changeStatusFromAdmin(PressRelease $pressRelease, PressReleaseStatus $status, ?string $reason = null): void
{
$previous = $pressRelease->status;

View file

@ -41,6 +41,21 @@ class ReleaseDetailPageData
->first();
if (! $release) {
// T12-Tombstone: depublizierte PMs (kostenpflichtige Entfernung
// auf Wunsch des Absenders) melden 410 statt 404 — die URL gab
// es, der Inhalt wurde bewusst entfernt.
$depublished = PressRelease::query()
->withoutGlobalScope(PortalScope::class)
->forPortalValues($portalValues)
->forLanguage($language)
->whereNotNull('depublished_at')
->where('slug', $slug)
->exists();
if ($depublished) {
return ['tombstone' => true];
}
abort(404);
}