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:
parent
2a9be7a9af
commit
198be381d6
16 changed files with 522 additions and 7 deletions
|
|
@ -1,9 +1,14 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Enums\SinglePurchaseStatus;
|
||||
use App\Enums\SinglePurchaseType;
|
||||
use App\Exceptions\BoostNotAllowedException;
|
||||
use App\Exceptions\InsufficientCreditsException;
|
||||
use App\Models\BookingConfirmation;
|
||||
use App\Models\LegalDocument;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\SinglePurchase;
|
||||
use App\Services\Auth\MagicLinkGenerator;
|
||||
use App\Services\Billing\CreditPricingService;
|
||||
use App\Services\Billing\ProofPdfService;
|
||||
|
|
@ -28,6 +33,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
|
||||
public ?string $shareExpiresAt = null;
|
||||
|
||||
public bool $depublicationConfirmed = false;
|
||||
|
||||
public function mount(int $id): void
|
||||
{
|
||||
$this->id = $id;
|
||||
|
|
@ -142,6 +149,30 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unternehmer-Pflicht-Checkbox vor dem Depublikations-Checkout (T1neu):
|
||||
* protokolliert wie im Buchungs-Modal — der Checkout-Controller verlangt
|
||||
* eine zeitnahe Bestätigung zusätzlich serverseitig.
|
||||
*/
|
||||
public function updatedDepublicationConfirmed(bool $value): void
|
||||
{
|
||||
if (! $value) {
|
||||
return;
|
||||
}
|
||||
|
||||
$agb = LegalDocument::current(LegalDocument::TYPE_AGB);
|
||||
|
||||
BookingConfirmation::query()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'booking_type' => 'depublication',
|
||||
'booking_ref' => (string) $this->id,
|
||||
'legal_document_id' => $agb?->id,
|
||||
'agb_version' => $agb?->version ?? (string) config('legal.agb_version'),
|
||||
'ip' => request()->ip(),
|
||||
'confirmed_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
$pr = $this->getMyPR();
|
||||
|
|
@ -189,6 +220,13 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
'latestRejection' => $latestRejection,
|
||||
'contacts' => $pr->contacts,
|
||||
'statusLogs' => $pr->statusLogs,
|
||||
// T12: Depublikation ist config-gated (ohne Preis kein Angebot).
|
||||
'depublicationPriceCents' => (int) config('billing.depublication_price_cents'),
|
||||
'depublicationRequested' => SinglePurchase::query()
|
||||
->where('press_release_id', $pr->id)
|
||||
->where('type', SinglePurchaseType::Depublication->value)
|
||||
->whereIn('status', [SinglePurchaseStatus::Pending->value, SinglePurchaseStatus::Paid->value])
|
||||
->exists(),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -505,6 +543,61 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
</article>
|
||||
@endif
|
||||
|
||||
{{-- ============== DEPUBLIKATION (T12, Meldeverfahren Weg 3) ============== --}}
|
||||
@if ($pr->status === PressReleaseStatus::Published && $depublicationPriceCents > 0)
|
||||
@php
|
||||
$depubNet = number_format($depublicationPriceCents / 100, 2, ',', '.');
|
||||
$depubGross = number_format($depublicationPriceCents * (1 + (float) config('billing.vat_rate')) / 100, 2, ',', '.');
|
||||
@endphp
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Depublikation (Service)') }}</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-9 h-9 rounded-[5px] flex items-center justify-center flex-shrink-0 bg-[color:var(--color-hub-soft)] text-[color:var(--color-hub)]">
|
||||
<flux:icon.eye-slash class="size-[18px]" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h3 class="text-[14px] font-bold text-[color:var(--color-ink)] m-0">{{ __('Aus der öffentlichen Ansicht entfernen') }}</h3>
|
||||
<p class="text-[12.5px] text-[color:var(--color-ink-2)] mt-1 mb-0">
|
||||
{{ __('Freiwillige Entfernung dieser veröffentlichten Mitteilung — die öffentliche URL zeigt danach dauerhaft einen Entfernt-Hinweis. Kopien in Suchmaschinen oder Drittarchiven können zeitweise weiter verfügbar sein. Preis: :net € zzgl. USt. (:gross € inkl. 19 % USt.)', ['net' => $depubNet, 'gross' => $depubGross]) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($depublicationRequested)
|
||||
<div class="rounded-[6px] border border-[color:var(--color-ok)]/30 bg-[color:var(--color-ok-soft)] px-3 py-2 text-[12.5px] text-[color:var(--color-gain-deep)]">
|
||||
{{ __('Für diese Pressemitteilung liegt bereits ein Depublikations-Vorgang vor.') }}
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-2">
|
||||
<flux:switch
|
||||
align="left"
|
||||
wire:model.live="depublicationConfirmed"
|
||||
:label="__('Ich bestelle in Ausübung meiner gewerblichen oder selbständigen beruflichen Tätigkeit (§ 14 BGB) und akzeptiere die AGB.')"
|
||||
/>
|
||||
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">
|
||||
<a href="{{ route('agb', ['edition' => 'de']) }}" target="_blank" rel="noopener" class="underline underline-offset-2 hover:text-[color:var(--color-ink)]">
|
||||
{{ __('AGB (Version :version)', ['version' => \App\Models\LegalDocument::currentAgbVersion()]) }}
|
||||
</a>
|
||||
· {{ __('Gesetzliche Ansprüche (z. B. DSGVO) bleiben davon unberührt und sind immer kostenlos.') }}
|
||||
</p>
|
||||
</div>
|
||||
@if ($depublicationConfirmed)
|
||||
<flux:button size="sm" variant="primary" icon="lock-closed" href="{{ route('me.checkout.depublication', $pr->id) }}">
|
||||
{{ __('Kostenpflichtig depublizieren') }}
|
||||
</flux:button>
|
||||
@else
|
||||
<flux:button size="sm" variant="primary" icon="lock-closed" disabled>
|
||||
{{ __('Kostenpflichtig depublizieren') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
@if ($pr->status === PressReleaseStatus::Review)
|
||||
<article class="panel" style="border-color:var(--color-warn); border-left-width:3px; background:var(--color-warn-soft);">
|
||||
<div class="panel-head">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue