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();