diff --git a/resources/views/livewire/customer/press-releases/show.blade.php b/resources/views/livewire/customer/press-releases/show.blade.php
index 8743b3ca..bcbd2c7f 100644
--- a/resources/views/livewire/customer/press-releases/show.blade.php
+++ b/resources/views/livewire/customer/press-releases/show.blade.php
@@ -48,15 +48,25 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
*/
public ?array $creditShortfall = null;
+ /**
+ * Kontext fürs Kauf-Bestätigungs-Modal (Entscheidung 10.07.): Add-ons
+ * buchen erst nach explizitem „Kostenpflichtig buchen", damit ein Klick
+ * auf eine Laufzeit nicht sofort Credits abbucht.
+ *
+ * @var array{type: 'boost'|'proof', label: string, credits: int, days: int|null}|null
+ */
+ public ?array $pendingPurchase = null;
+
public function mount(int $id): void
{
$this->id = $id;
$pr = $this->getMyPR();
$this->authorize('view', $pr);
- // KI-Grad vorbelegen (bereits deklariert); die verbindliche
- // Bestätigung muss bei jedem Submit erneut gesetzt werden.
- $this->aiTextDeclaration = $pr->ai_text_declaration?->value;
+ // KI-Grad vorbelegen (bereits deklariert), gemappt auf die zwei
+ // Formular-Optionen; die verbindliche Bestätigung muss bei jedem
+ // Submit erneut gesetzt werden.
+ $this->aiTextDeclaration = $pr->ai_text_declaration?->formOption()->value;
}
public function submitForReview(): void
@@ -99,7 +109,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
Flux::toast(
heading: __('Eingereicht'),
- text: __('Die Inhaltsprüfung ist typischerweise innerhalb von 24 h abgeschlossen.'),
+ text: __('Die automatische Inhaltsprüfung ist in der Regel innerhalb weniger Minuten abgeschlossen.'),
variant: 'success',
);
}
@@ -117,6 +127,67 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
Flux::toast(text: __('Vorschau-Link wurde erzeugt.'), variant: 'success');
}
+ /**
+ * Kauf-Bestätigung für einen Boost öffnen (Abbuchung erst in
+ * buyPendingAddOn).
+ */
+ public function confirmBoost(int $days): void
+ {
+ $options = app(CreditPricingService::class)->boostOptions();
+
+ if (! isset($options[$days])) {
+ return;
+ }
+
+ $this->pendingPurchase = [
+ 'type' => 'boost',
+ 'label' => __('Boost — :days Tage Platzierung', ['days' => $days]),
+ 'credits' => $options[$days],
+ 'days' => $days,
+ ];
+
+ Flux::modal('confirm-addon-purchase')->show();
+ }
+
+ /**
+ * Kauf-Bestätigung für den Veröffentlichungsnachweis öffnen.
+ */
+ public function confirmProof(): void
+ {
+ $this->pendingPurchase = [
+ 'type' => 'proof',
+ 'label' => __('Veröffentlichungsnachweis (PDF)'),
+ 'credits' => app(ProofPdfService::class)->priceCredits(),
+ 'days' => null,
+ ];
+
+ Flux::modal('confirm-addon-purchase')->show();
+ }
+
+ /**
+ * Bestätigten Add-on-Kauf ausführen — der einzige Weg, auf dem die
+ * Buttons der Add-on-Sektion tatsächlich Credits abbuchen.
+ */
+ public function buyPendingAddOn(): void
+ {
+ $pending = $this->pendingPurchase;
+ $this->pendingPurchase = null;
+
+ Flux::modal('confirm-addon-purchase')->close();
+
+ if ($pending === null) {
+ return;
+ }
+
+ if ($pending['type'] === 'boost') {
+ $this->buyBoost($pending['days']);
+
+ return;
+ }
+
+ $this->buyProof();
+ }
+
/**
* Boost (Platzierung) aus der Wallet buchen — nur veröffentlichte, grüne
* PMs sind boostbar (Gate im BoostService).
@@ -571,9 +642,9 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
@if ($canBoost)
@foreach ($boostOptions as $days => $credits)
-
- {{ __(':days Tage · :credits Credits', ['days' => $days, 'credits' => $credits]) }}
+
+ {{ __(':days Tage für :credits Credits buchen', ['days' => $days, 'credits' => $credits]) }}
@endforeach
@@ -607,7 +678,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
{{ __('Nachweis herunterladen') }}
@else
-
+
{{ __('Für :credits Credits kaufen', ['credits' => $proofPrice]) }}
@endif
@@ -890,14 +961,14 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
{{ __('Platzhalter-Titelbild — laden Sie im Editor ein eigenes Bild hoch.') }}
@elseif ($titleImage && ($titleImage->copyright || $titleImage->is_ai_generated || $titleImage->ai_declaration?->publicLabel()))
- {{-- Bildnachweis + KI-Kennzeichnung (Art. 50 EU AI Act) --}}
+ {{-- Bildnachweis + KI-Kennzeichnung (Art. 50 EU AI Act): kurzes
+ Badge („KI-generiert" / „KI-bearbeitet"), kein Satz-Wortlaut. --}}
- @if ($titleImage->is_ai_generated)
- {{ __('KI-generiert') }}
+ @if ($aiImageBadge = $titleImage->ai_declaration?->publicLabel() ?? ($titleImage->is_ai_generated ? __('KI-generiert') : null))
+ {{ $aiImageBadge }}
@endif
- {{ $titleImage->copyright ?? __('Bild: KI-generiert') }}
- @if ($aiImageHint = $titleImage->ai_declaration?->publicLabel())
- · {{ $aiImageHint }}
+ @if ($titleImage->copyright)
+ {{ $titleImage->copyright }}
@endif
@endif
@@ -1004,6 +1075,49 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
@endif
+ {{-- ============== MODAL: ADD-ON-KAUF BESTÄTIGEN ==============
+ Vorgeschaltete Bestätigung (Entscheidung 10.07.): kein Add-on-Klick
+ bucht direkt Credits ab — erst „Kostenpflichtig buchen" kauft. --}}
+
+
+
+ {{ __('Add-on') }}
+ {{ __('Kauf bestätigen') }}
+
+
+ @if ($pendingPurchase)
+
+
+ {{ $pendingPurchase['label'] }}
+ {{ __(':n Credits', ['n' => $pendingPurchase['credits']]) }}
+
+
+ @if ($creditBalance >= $pendingPurchase['credits'])
+ {{ __('Der Betrag wird sofort von Ihrem Guthaben abgebucht: :available Credits → :rest Credits.', [
+ 'available' => $creditBalance,
+ 'rest' => $creditBalance - $pendingPurchase['credits'],
+ ]) }}
+ @else
+ {{ __('Ihr Guthaben (:available Credits) reicht dafür nicht aus — es fehlen :missing Credits.', [
+ 'available' => $creditBalance,
+ 'missing' => $pendingPurchase['credits'] - $creditBalance,
+ ]) }}
+ @endif
+
+
+ @endif
+
+
+
+ {{ __('Abbrechen') }}
+
+
+ {{ __('Kostenpflichtig buchen') }}
+
+
+
+
+
{{-- ============== MODAL: NICHT GENÜGEND CREDITS ==============
Öffnet sich serverseitig, wenn ein Add-on-Kauf (Boost/Nachweis)
am Wallet-Guthaben scheitert — mit Direktlink zum Aufladen. --}}
diff --git a/tests/Feature/PressReleaseShowAddonsTest.php b/tests/Feature/PressReleaseShowAddonsTest.php
index d33f6abe..44b1c109 100644
--- a/tests/Feature/PressReleaseShowAddonsTest.php
+++ b/tests/Feature/PressReleaseShowAddonsTest.php
@@ -49,11 +49,73 @@ test('a published green release shows the boost and proof add-ons', function ()
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertSee('Add-ons')
->assertSee('Boost — Platzierung')
- ->assertSee('7 Tage · 12 Credits')
+ ->assertSee('7 Tage für 12 Credits buchen')
->assertSee('Veröffentlichungsnachweis')
->assertSee('Für 3 Credits kaufen');
});
+test('add-on buttons only debit after the purchase confirmation modal', function () {
+ /** @var TestCase $this */
+ ['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
+ $this->wallet->credit($customer, 40);
+
+ $this->actingAs($customer);
+
+ $component = LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
+ ->call('confirmBoost', 14)
+ ->assertSet('pendingPurchase', [
+ 'type' => 'boost',
+ 'label' => 'Boost — 14 Tage Platzierung',
+ 'credits' => 20,
+ 'days' => 14,
+ ])
+ ->assertSee('Kauf bestätigen')
+ ->assertSee('Kostenpflichtig buchen');
+
+ // Der Klick auf die Laufzeit selbst bucht noch nichts ab.
+ expect($customer->fresh()->creditBalance())->toBe(40);
+ expect($pr->fresh()->isBoosted())->toBeFalse();
+
+ $component->call('buyPendingAddOn')
+ ->assertSet('pendingPurchase', null);
+
+ expect($customer->fresh()->creditBalance())->toBe(20);
+ expect($pr->fresh()->isBoosted())->toBeTrue();
+});
+
+test('the proof purchase runs through the confirmation modal as well', function () {
+ /** @var TestCase $this */
+ ['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
+ $this->wallet->credit($customer, 10);
+
+ $this->actingAs($customer);
+
+ LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
+ ->call('confirmProof')
+ ->assertSet('pendingPurchase.type', 'proof')
+ ->assertSet('pendingPurchase.credits', 3)
+ ->call('buyPendingAddOn')
+ ->assertSee('Nachweis herunterladen');
+
+ expect(app(ProofPdfService::class)->hasPurchased($customer->fresh(), $pr))->toBeTrue();
+ expect($customer->fresh()->creditBalance())->toBe(7);
+});
+
+test('an unknown boost duration does not open the confirmation modal', function () {
+ /** @var TestCase $this */
+ ['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
+ $this->wallet->credit($customer, 40);
+
+ $this->actingAs($customer);
+
+ LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
+ ->call('confirmBoost', 999)
+ ->assertSet('pendingPurchase', null)
+ ->call('buyPendingAddOn');
+
+ expect($customer->fresh()->creditBalance())->toBe(40); // nichts abgebucht
+});
+
test('booking a boost from the detail page debits the wallet and activates it', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();