diff --git a/resources/views/livewire/customer/press-releases/show.blade.php b/resources/views/livewire/customer/press-releases/show.blade.php
index b324a0e..1a74a34 100644
--- a/resources/views/livewire/customer/press-releases/show.blade.php
+++ b/resources/views/livewire/customer/press-releases/show.blade.php
@@ -1,10 +1,15 @@
getMyPR();
+ $this->authorize('view', $pr);
+
+ try {
+ app(BoostService::class)->boost(auth()->user(), $pr, $days);
+ Flux::toast(
+ heading: __('Boost gebucht'),
+ text: __('Ihre Pressemitteilung wird jetzt auf Start- und Branchenseite hervorgehoben.'),
+ variant: 'success',
+ );
+ } catch (InsufficientCreditsException $e) {
+ Flux::toast(
+ heading: __('Nicht genügend Credits'),
+ text: __('Der Boost kostet :need Credits, Ihr Guthaben beträgt :have. Laden Sie unter „Buchungen & Add-ons" auf.', ['need' => $e->required, 'have' => $e->available]),
+ variant: 'warning',
+ duration: 8000,
+ );
+ } catch (BoostNotAllowedException $e) {
+ Flux::toast(text: $e->getMessage(), variant: 'warning', duration: 8000);
+ }
+ }
+
+ /**
+ * Veröffentlichungsnachweis-PDF kaufen (3 Credits, einmalig je PM). Danach
+ * steht der Download über die Nachweis-Route bereit.
+ */
+ public function buyProof(): void
+ {
+ $pr = $this->getMyPR();
+ $this->authorize('downloadProof', $pr);
+
+ try {
+ app(ProofPdfService::class)->purchase(auth()->user(), $pr);
+ Flux::toast(
+ heading: __('Nachweis gekauft'),
+ text: __('Der Veröffentlichungsnachweis steht jetzt zum Download bereit.'),
+ variant: 'success',
+ );
+ } catch (InsufficientCreditsException $e) {
+ Flux::toast(
+ heading: __('Nicht genügend Credits'),
+ text: __('Der Nachweis kostet :need Credits, Ihr Guthaben beträgt :have.', ['need' => $e->required, 'have' => $e->available]),
+ variant: 'warning',
+ duration: 8000,
+ );
+ }
+ }
+
public function with(): array
{
$pr = $this->getMyPR();
@@ -101,8 +160,18 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
$company = $pr->company?->loadCount(['pressReleases', 'contacts']);
+ $boostService = app(BoostService::class);
+ $proofService = app(ProofPdfService::class);
+
return [
'pr' => $pr,
+ 'creditBalance' => $user->creditBalance(),
+ 'canBoost' => $boostService->canBoost($pr),
+ 'isBoosted' => $boostService->isBoosted($pr),
+ 'boostedUntil' => $boostService->activeUntil($pr),
+ 'boostOptions' => app(CreditPricingService::class)->boostOptions(),
+ 'proofPrice' => $proofService->priceCredits(),
+ 'proofPurchased' => $proofService->hasPurchased($user, $pr),
'companyLogoUrl' => $company?->logoUrl(),
'companyInitials' => $this->companyInitials($company?->name),
'companyMetaLine' => $this->companyMetaLine($company),
@@ -349,6 +418,82 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
@endif
+ {{-- ============== ADD-ONS (Boost + Veröffentlichungsnachweis) ============== --}}
+ @if ($pr->status === PressReleaseStatus::Published)
+
+
+ {{ __('Add-ons') }}
+ {{ __(':n Credits Guthaben', ['n' => $creditBalance]) }}
+
+
+ {{-- Boost --}}
+
+
+
+
+
+
+
{{ __('Boost — Platzierung') }}
+
+ {{ __('Hervorhebung auf Start- und Branchenseite.') }}
+
+
+
+
+ @if ($isBoosted)
+
+ {{ __('Aktiv bis :date', ['date' => $boostedUntil?->copy()->setTimezone(\App\Models\PressRelease::DISPLAY_TIMEZONE)->format('d.m.Y H:i')]) }}
+
+ @endif
+
+ @if ($canBoost)
+
+ @foreach ($boostOptions as $days => $credits)
+
+ {{ __(':days Tage · :credits Credits', ['days' => $days, 'credits' => $credits]) }}
+
+ @endforeach
+
+ @if ($isBoosted)
+
{{ __('Ein weiterer Kauf verlängert die Laufzeit.') }}
+ @endif
+ @else
+
+ {{ __('Nur grün geprüfte Pressemitteilungen sind boostbar.') }}
+
+ @endif
+
+
+ {{-- Veröffentlichungsnachweis --}}
+
+
+
+
+
+
+
{{ __('Veröffentlichungsnachweis') }}
+
+ {{ __('PDF mit Datum, Portal und URL — :credits Credits.', ['credits' => $proofPrice]) }}
+
+
+
+
+ @if ($proofPurchased)
+
+ {{ __('Nachweis herunterladen') }}
+
+ @else
+
+ {{ __('Für :credits Credits kaufen', ['credits' => $proofPrice]) }}
+
+ @endif
+
+
+
+ @endif
+
@if ($pr->status === PressReleaseStatus::Review)
diff --git a/tests/Feature/Billing/BookingsPageTest.php b/tests/Feature/Billing/BookingsPageTest.php
index f241a74..3c19a60 100644
--- a/tests/Feature/Billing/BookingsPageTest.php
+++ b/tests/Feature/Billing/BookingsPageTest.php
@@ -5,6 +5,7 @@ use App\Models\Plan;
use App\Models\SinglePurchase;
use App\Models\User;
use App\Models\UserPaymentOption;
+use App\Services\Billing\CreditWalletService;
use App\Services\Billing\StripeCheckoutService;
use Database\Seeders\RolesAndPermissionsSeeder;
use Livewire\Volt\Volt as LivewireVolt;
@@ -162,3 +163,48 @@ test('the checkout success banner is shown after returning from stripe', functio
->assertOk()
->assertSee('Vielen Dank für Ihre Buchung!');
});
+
+test('the wallet block shows the balance and the credit pack topup links', function () {
+ /** @var TestCase $this */
+ $user = bookingsTestCustomer();
+ app(CreditWalletService::class)->credit($user, 30);
+
+ $this->actingAs($user);
+
+ LivewireVolt::test('customer.bookings')
+ ->assertSee('Credit-Wallet & Add-ons')
+ ->assertSee('30')
+ ->assertSee('27 Credits') // Bonus-Paket
+ ->assertSee(route('me.checkout.credit-topup', ['pack' => 'p25']), false)
+ ->assertSee('Prüfkontingent')
+ ->assertSee('4 / 4'); // Einzel-Tier: 4 Freiprüfungen
+});
+
+test('buying an extra pm from the wallet succeeds with enough credits', function () {
+ /** @var TestCase $this */
+ $user = bookingsTestCustomer();
+ app(CreditWalletService::class)->credit($user, 25);
+
+ $this->actingAs($user);
+
+ LivewireVolt::test('customer.bookings')
+ ->call('buyExtraPm')
+ ->assertSee('Extra-Pressemitteilung gebucht');
+
+ expect($user->fresh()->creditBalance())->toBe(6); // 25 - 19 (Einzel-Tier)
+ expect($user->singlePurchases()->grantingSubmission()->count())->toBe(1);
+});
+
+test('buying an extra pm without enough credits shows the mini checkout hint', function () {
+ /** @var TestCase $this */
+ $user = bookingsTestCustomer();
+ app(CreditWalletService::class)->credit($user, 8);
+
+ $this->actingAs($user);
+
+ LivewireVolt::test('customer.bookings')
+ ->call('buyExtraPm')
+ ->assertSee('Bitte laden Sie mindestens 11 Credits nach');
+
+ expect($user->singlePurchases()->count())->toBe(0);
+});
diff --git a/tests/Feature/PressReleaseShowAddonsTest.php b/tests/Feature/PressReleaseShowAddonsTest.php
new file mode 100644
index 0000000..4bb5713
--- /dev/null
+++ b/tests/Feature/PressReleaseShowAddonsTest.php
@@ -0,0 +1,100 @@
+seed(RolesAndPermissionsSeeder::class);
+ $this->wallet = app(CreditWalletService::class);
+});
+
+/**
+ * @return array{customer: User, pr: PressRelease}
+ */
+function publishedGreenPrForOwner(): array
+{
+ $customer = User::factory()->create(['is_active' => true]);
+ $customer->assignRole('customer');
+
+ $company = Company::factory()->presseecho()->create();
+ $customer->companies()->attach($company->id, ['role' => 'owner']);
+
+ $pr = PressRelease::factory()->published()->create([
+ 'user_id' => $customer->id,
+ 'company_id' => $company->id,
+ 'category_id' => Category::factory()->create()->id,
+ 'portal' => $company->portal->value,
+ 'classification' => PressReleaseClassification::Green->value,
+ ]);
+
+ return compact('customer', 'pr');
+}
+
+test('a published green release shows the boost and proof add-ons', 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])
+ ->assertSee('Add-ons')
+ ->assertSee('Boost — Platzierung')
+ ->assertSee('7 Tage · 12 Credits')
+ ->assertSee('Veröffentlichungsnachweis')
+ ->assertSee('Für 3 Credits kaufen');
+});
+
+test('booking a boost from the detail page debits the wallet and activates it', 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('buyBoost', 14);
+
+ expect($customer->fresh()->creditBalance())->toBe(20); // 40 - 20 (14 Tage)
+ expect($pr->fresh()->isBoosted())->toBeTrue();
+});
+
+test('buying a proof from the detail page unlocks the download', 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('buyProof')
+ ->assertSee('Nachweis herunterladen');
+
+ expect(app(ProofPdfService::class)->hasPurchased($customer->fresh(), $pr))->toBeTrue();
+ expect($customer->fresh()->creditBalance())->toBe(7);
+});
+
+test('a non-green release cannot be boosted from the detail page', function () {
+ /** @var TestCase $this */
+ ['customer' => $customer, 'pr' => $pr] = publishedGreenPrForOwner();
+ $pr->update(['classification' => PressReleaseClassification::Yellow->value]);
+ $this->wallet->credit($customer, 40);
+
+ $this->actingAs($customer);
+
+ LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
+ ->assertSee('Nur grün geprüfte Pressemitteilungen sind boostbar')
+ ->call('buyBoost', 7);
+
+ expect($customer->fresh()->creditBalance())->toBe(40); // nichts abgebucht
+ expect($pr->fresh()->isBoosted())->toBeFalse();
+});