presseportale/resources/views/livewire/customer/bookings.blade.php
Kevin Adametz c8dc99c3c8 Phase 9E (Abschluss): Checkout-Flows und Plan-Kontingent statt Quota-Stub
- Checkout-Backend: me.checkout.subscription (Tarif-Abo monatlich/jährlich)
  und me.checkout.single-pm (Einzel-PM 19 € netto, pending-Kauf mit
  Webhook-Erfüllung); StripeCheckoutService als mockbarer Stripe-Wrapper;
  Stripe Tax via Cashier::calculateTaxes() (Netto-Preise, USt-ID-Abfrage)
- Slot-Logik: Kontingent aus dem Tarif (plans.press_release_quota) plus
  bezahlte Einmalkäufe; Verbrauch bei Veröffentlichung zuerst aus dem
  Plan-Zähler, danach Einlösung des ältesten Einmalkaufs (consumed +
  PM-Verknüpfung); Grandfathered = unbegrenzt (Entscheidung 12.06.2026,
  Bestandsschutz); Stub-Spalte users.press_release_quota entfernt
- billing:sync-stripe-plans legt zusätzlich das Einzel-PM-Produkt an
  (STRIPE_PRICE_SINGLE_PM); Test-Mode-Sync gelaufen
- Buchungs-Seite: Rückmeldung nach Checkout (erfolg/abbruch/Guard-Hinweis)
- Tests: PressReleaseQuotaTest auf Plan-Semantik neu geschrieben,
  CheckoutFlowTest (8 Tests), Modal-/API-Tests angepasst; Suite 510 passed
- Doku: Billing-und-Rechnungskreise (Kontingent-Tabelle, Checkout-Routen,
  Webhook-Events, Stripe-CLI-Hinweis), PHASE-9-Plan 9E , Checkliste,
  STATUS-ABGLEICH, PROGRESS

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:10:32 +00:00

422 lines
25 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class extends Component
{
public function with(): array
{
return [
// Rückkehr aus dem Stripe-Checkout (?checkout=erfolg|abbruch)
// bzw. Hinweis aus den Checkout-Guards (Session-Flash).
'checkoutResult' => request()->query('checkout'),
'checkoutNotice' => session('checkout-notice'),
'creditSummary' => [
'total' => 17,
'bonus' => 12,
'paid' => 5,
'auto_refill' => __('ab 10 Credits empfohlen'),
'validity' => __('Bonus-Credits verfallen monatlich, gekaufte Credits bleiben 24 Monate gültig.'),
],
'currentPlan' => [
'name' => 'Starter',
'price' => '19 €/Mo.',
'press_releases' => '3 PMs/Monat',
'bonus_credits' => 12,
],
'creditPackages' => [
['name' => 'Test', 'credits' => 10, 'price' => '10 €', 'rate' => '1,00 €', 'saving' => null],
['name' => 'Standard', 'credits' => 50, 'price' => '45 €', 'rate' => '0,90 €', 'saving' => '10 %'],
['name' => 'Plus', 'credits' => 150, 'price' => '120 €', 'rate' => '0,80 €', 'saving' => '20 %'],
['name' => 'Pro', 'credits' => 500, 'price' => '375 €', 'rate' => '0,75 €', 'saving' => '25 %'],
['name' => 'Business', 'credits' => 1500, 'price' => '1.050 €', 'rate' => '0,70 €', 'saving' => '30 %'],
],
'serviceGroups' => [
[
'title' => __('Veröffentlichung'),
'description' => __('Basisleistungen rund um Veröffentlichung, Korrektur und Aktualisierung.'),
'services' => [
['name' => __('Standard-PM (Pay-as-you-go)'), 'credits' => '19', 'meta' => __('1 Veröffentlichung')],
['name' => __('PM-Korrektur'), 'credits' => '8', 'meta' => __('Pfad C')],
['name' => __('PM-Update'), 'credits' => '4', 'meta' => __('im ersten Jahr ggf. kostenlos')],
['name' => __('Depublizierung'), 'credits' => '1925', 'meta' => __('abhängig vom Aufwand')],
],
],
[
'title' => __('Bilder'),
'description' => __('Stock- und KI-Bilder für mehr Sichtbarkeit in Listen und Detailseiten.'),
'services' => [
['name' => __('Free-Stock'), 'credits' => '0', 'meta' => __('Unsplash, Pexels')],
['name' => __('Premium-Stock'), 'credits' => '8', 'meta' => __('Adobe, Shutterstock')],
['name' => __('KI-Bild generieren'), 'credits' => '4', 'meta' => __('neues Motiv')],
['name' => __('KI-Bild Re-Generation'), 'credits' => '2', 'meta' => __('Variante erzeugen')],
],
],
[
'title' => __('KI-Textservices'),
'description' => __('Qualität verbessern, Score-Stufe erreichen und bessere Headlines testen.'),
'services' => [
['name' => __('Quality-Check'), 'credits' => '3', 'meta' => __('Stil und Pressestil')],
['name' => __('Lektorat'), 'credits' => '8', 'meta' => __('sprachliche Prüfung')],
['name' => __('Pressetext-Optimierung'), 'credits' => '15', 'meta' => __('Headlines und SEO')],
['name' => __('Headline-Booster'), 'credits' => '5', 'meta' => __('nur Headlines')],
['name' => __('PM aus Stichworten generieren'), 'credits' => '25', 'meta' => __('Entwurf aus Briefing')],
['name' => __('Übersetzung DE/EN'), 'credits' => '12', 'meta' => __('pro Sprachrichtung')],
],
],
[
'title' => __('Distribution'),
'description' => __('Zusätzliche Formate und externe Reichweite für passende Meldungen.'),
'services' => [
['name' => __('PDF-Export mit Branding'), 'credits' => '2', 'meta' => __('für Weitergabe')],
['name' => __('Social-Snippet-Generierung'), 'credits' => '3', 'meta' => __('Kurztexte')],
['name' => __('Verteiler-Versand klein'), 'credits' => '39', 'meta' => __('branchenspezifisch')],
['name' => __('Verteiler-Versand mittel'), 'credits' => '99', 'meta' => __('mehr Empfänger')],
['name' => __('Verteiler-Versand groß'), 'credits' => '199', 'meta' => __('branchenübergreifend')],
],
],
[
'title' => __('Account & Profil'),
'description' => __('Vertrauen, Wiedererkennung und zusätzliche Profilfunktionen.'),
'services' => [
['name' => __('Verifiziertes Firmenprofil'), 'credits' => '79', 'meta' => __('einmalig')],
['name' => __('Custom Subdomain'), 'credits' => '49', 'meta' => __('pro Jahr')],
['name' => __('Erweiterte Statistiken'), 'credits' => '15', 'meta' => __('pro Monat')],
],
],
],
'placements' => [
['name' => __('Highlight Kategorie'), 'credits' => '15', 'duration' => __('3 Tage'), 'tier' => __('Standard'), 'score' => '30+'],
['name' => __('Highlight Kategorie'), 'credits' => '30', 'duration' => __('7 Tage'), 'tier' => __('Standard'), 'score' => '30+'],
['name' => __('Startseite-Highlight'), 'credits' => '39', 'duration' => __('24 h'), 'tier' => __('Geprüft'), 'score' => '60+'],
['name' => __('Startseite-Highlight'), 'credits' => '89', 'duration' => __('3 Tage'), 'tier' => __('Geprüft'), 'score' => '60+'],
['name' => __('Top-Slot Startseite'), 'credits' => '119', 'duration' => __('24 h'), 'tier' => __('Hochwertig'), 'score' => '80+'],
['name' => __('Newsletter-Erwähnung'), 'credits' => '59', 'duration' => __('nächster Versand'), 'tier' => __('Geprüft'), 'score' => '60+'],
['name' => __('Social-Share'), 'credits' => '25', 'duration' => __('offizieller Kanal'), 'tier' => __('Geprüft'), 'score' => '60+'],
],
'activeBookings' => [],
'bookingHistory' => [],
];
}
}; ?>
<div class="space-y-8">
{{-- ============== CHECKOUT-RÜCKMELDUNG ============== --}}
@if ($checkoutResult === 'erfolg')
<div class="px-4 py-3 rounded-[5px] border text-[13px] flex items-start gap-3
bg-[color:var(--color-hub-soft)] border-[color:var(--color-hub-soft-2)] text-[color:var(--color-ink-2)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
<div class="flex-1">
<span class="font-semibold text-[color:var(--color-ink)]">{{ __('Vielen Dank für Ihre Buchung!') }}</span>
{{ __('Die Zahlung wird von Stripe bestätigt — die Buchung erscheint hier in wenigen Augenblicken. Die Rechnung finden Sie anschließend unter Rechnungen.') }}
</div>
</div>
@elseif ($checkoutResult === 'abbruch')
<div class="px-4 py-3 rounded-[5px] border text-[13px] flex items-start gap-3
bg-[color:var(--color-bg-subtle)] border-[color:var(--color-bg-rule)] text-[color:var(--color-ink-2)]">
<flux:icon.information-circle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-ink-3)]" />
<div class="flex-1">
{{ __('Der Bezahlvorgang wurde abgebrochen. Es wurde nichts gebucht — Sie können den Checkout jederzeit erneut starten.') }}
</div>
</div>
@endif
@if ($checkoutNotice)
<div class="px-4 py-3 rounded-[5px] border text-[13px] flex items-start gap-3
bg-[color:var(--color-bg-subtle)] border-[color:var(--color-bg-rule)] text-[color:var(--color-ink-2)]">
<flux:icon.information-circle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-ink-3)]" />
<div class="flex-1">{{ $checkoutNotice }}</div>
</div>
@endif
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('User Backend') }}</span>
<span class="eyebrow muted">{{ __('Mein Bereich · Finanzen') }}</span>
<span class="badge hub">{{ __('Konzeptstand Mai 2026') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Buchungen & Add-ons') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Der Marktplatz für Credit-Pakete, KI-Services, Platzierungen und Firmen-Add-ons. Die Preise folgen dem neuen Credit-Modell: 1 Credit entspricht dem Listenwert von 1 €.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button size="sm" variant="filled" icon="document-text" href="{{ route('me.invoices.index') }}" wire:navigate>
{{ __('Rechnungen') }}
</flux:button>
<flux:button size="sm" variant="primary" icon="plus" disabled>
{{ __('Credits kaufen') }}
</flux:button>
</div>
</header>
{{-- ============== CREDIT-ÜBERSICHT ============== --}}
<section class="grid gap-4 lg:grid-cols-[1.2fr_0.8fr]">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Credit-Stand') }}</span>
<span class="badge ok dot">{{ __('Auto-Refill vorbereitet') }}</span>
</div>
<div class="p-5 grid gap-5 md:grid-cols-[0.8fr_1.2fr]">
<div>
<div class="text-[42px] font-bold tracking-[-1.2px] leading-none text-[color:var(--color-ink)]">
{{ $creditSummary['total'] }}
</div>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] mt-2 mb-0">
{{ __('verfügbare Credits') }}
</p>
</div>
<div class="grid gap-3 sm:grid-cols-2">
<div class="rounded-[6px] border border-[color:var(--color-bg-rule)] p-4 bg-[color:var(--color-bg-subtle)]">
<div class="text-[12px] text-[color:var(--color-ink-3)]">{{ __('Bonus-Credits') }}</div>
<div class="text-[22px] font-semibold text-[color:var(--color-ink)]">{{ $creditSummary['bonus'] }}</div>
<div class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ __('monatlich verfallend') }}</div>
</div>
<div class="rounded-[6px] border border-[color:var(--color-bg-rule)] p-4 bg-[color:var(--color-bg-subtle)]">
<div class="text-[12px] text-[color:var(--color-ink-3)]">{{ __('Gekaufte Credits') }}</div>
<div class="text-[22px] font-semibold text-[color:var(--color-ink)]">{{ $creditSummary['paid'] }}</div>
<div class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ __('24 Monate gültig') }}</div>
</div>
</div>
<div class="md:col-span-2 px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-hub-soft)] border-[color:var(--color-hub-soft-2)] text-[color:var(--color-ink-2)]">
<flux:icon.information-circle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
<div class="flex-1">
{{ $creditSummary['validity'] }}
{{ __('Für spätere Checkouts ist Auto-Refill :threshold vorgesehen.', ['threshold' => $creditSummary['auto_refill']]) }}
</div>
</div>
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Aktueller Tarif') }}</span>
<span class="badge hub">{{ $currentPlan['name'] }}</span>
</div>
<div class="p-5 space-y-4">
<div>
<div class="text-[28px] font-bold tracking-[-0.7px] text-[color:var(--color-ink)]">
{{ $currentPlan['price'] }}
</div>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] mt-1 mb-0">
{{ __('inkl. :credits Bonus-Credits und :pms', [
'credits' => $currentPlan['bonus_credits'],
'pms' => $currentPlan['press_releases'],
]) }}
</p>
</div>
<div class="rounded-[6px] border border-[color:var(--color-bg-rule)] p-4">
<div class="text-[12px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Nächster sinnvoller Schritt') }}
</div>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Bei mehreren PMs mit KI-Optimierung oder Platzierungen ergänzt das Standard-Paket die monatlichen Bonus-Credits am saubersten.') }}
</p>
</div>
</div>
</article>
</section>
{{-- ============== CREDIT-PAKETE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Credit-Pakete') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ __('Volumenrabatt nach Paketgröße') }}</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Paket') }}</flux:table.column>
<flux:table.column>{{ __('Credits') }}</flux:table.column>
<flux:table.column>{{ __('Preis') }}</flux:table.column>
<flux:table.column>{{ __('Effektiv/Credit') }}</flux:table.column>
<flux:table.column>{{ __('Ersparnis') }}</flux:table.column>
<flux:table.column>{{ __('Aktion') }}</flux:table.column>
</flux:table.columns>
@foreach ($creditPackages as $package)
<flux:table.row wire:key="credit-package-{{ $package['name'] }}">
<flux:table.cell>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $package['name'] }}</span>
</flux:table.cell>
<flux:table.cell>{{ number_format($package['credits'], 0, ',', '.') }}</flux:table.cell>
<flux:table.cell>
<span class="font-semibold text-[color:var(--color-ink)]">{{ $package['price'] }}</span>
</flux:table.cell>
<flux:table.cell>{{ $package['rate'] }}</flux:table.cell>
<flux:table.cell>
@if ($package['saving'])
<span class="badge ok">{{ $package['saving'] }}</span>
@else
<span class="text-[12px] text-[color:var(--color-ink-3)]"></span>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:button size="sm" variant="filled" disabled>
{{ __('Kaufen') }}
</flux:button>
</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table>
</article>
{{-- ============== PLATZIERUNGEN ============== --}}
<section class="space-y-4">
<div>
<span class="section-eyebrow">{{ __('Boost & Platzierungen') }}</span>
<h2 class="text-[22px] font-bold tracking-[-0.4px] mt-1 mb-1 text-[color:var(--color-ink)]">
{{ __('Sichtbarkeit buchen, wenn die Score-Stufe passt') }}
</h2>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] max-w-[760px] m-0">
{{ __('Platzierungen bleiben an Qualitätsstufen gekoppelt: Standard reicht für Kategorie-Highlights, Geprüft für Startseite/Newsletter/Social und Hochwertig für den Top-Slot.') }}
</p>
</div>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
@foreach ($placements as $placement)
<article class="panel">
<div class="p-5 space-y-4">
<div class="flex items-start justify-between gap-4">
<div class="flex items-start gap-3">
<div class="w-10 h-10 rounded-[6px] flex items-center justify-center bg-[color:var(--color-hub-soft)] text-[color:var(--color-hub)]">
<flux:icon.megaphone class="size-5" />
</div>
<div>
<h3 class="text-[14px] font-semibold text-[color:var(--color-ink)] m-0">
{{ $placement['name'] }}
</h3>
<p class="text-[12px] text-[color:var(--color-ink-3)] mt-1 mb-0">
{{ $placement['duration'] }}
</p>
</div>
</div>
<div class="text-right">
<div class="text-[22px] font-bold text-[color:var(--color-ink)]">{{ $placement['credits'] }}</div>
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ __('Credits') }}</div>
</div>
</div>
<div class="flex items-center justify-between gap-3 rounded-[6px] border border-[color:var(--color-bg-rule)] p-3">
<div>
<div class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ __('Mindeststufe') }}</div>
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $placement['tier'] }}</div>
</div>
<flux:tooltip content="{{ __('Interner Score-Schwellenwert: :score', ['score' => $placement['score']]) }}">
<span class="badge hub">{{ __('Score :score', ['score' => $placement['score']]) }}</span>
</flux:tooltip>
</div>
<flux:button size="sm" variant="primary" class="w-full" disabled>
{{ __('Buchung vorbereiten') }}
</flux:button>
</div>
</article>
@endforeach
</div>
</section>
{{-- ============== SERVICE-MARKTPLATZ ============== --}}
<section class="space-y-4">
<div>
<span class="section-eyebrow">{{ __('Add-on-Marktplatz') }}</span>
<h2 class="text-[22px] font-bold tracking-[-0.4px] mt-1 mb-1 text-[color:var(--color-ink)]">
{{ __('Buchbare Services nach Kategorie') }}
</h2>
</div>
<div class="grid gap-4 xl:grid-cols-2">
@foreach ($serviceGroups as $group)
<article class="panel">
<div class="panel-head">
<div class="flex items-center gap-2">
<flux:icon.sparkles class="size-4 text-[color:var(--color-hub)]" />
<span class="section-eyebrow">{{ $group['title'] }}</span>
</div>
</div>
<div class="p-5">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] mt-0 mb-4">
{{ $group['description'] }}
</p>
<div class="divide-y divide-[color:var(--color-bg-rule)]">
@foreach ($group['services'] as $service)
<div class="py-3 first:pt-0 last:pb-0 flex items-center justify-between gap-4">
<div class="min-w-0">
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $service['name'] }}</div>
<div class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ $service['meta'] }}</div>
</div>
<div class="text-right flex-shrink-0">
<div class="text-[15px] font-bold text-[color:var(--color-ink)]">{{ $service['credits'] }}</div>
<div class="text-[10.5px] uppercase tracking-[0.08em] text-[color:var(--color-ink-3)]">{{ __('Credits') }}</div>
</div>
</div>
@endforeach
</div>
</div>
</article>
@endforeach
</div>
</section>
{{-- ============== AKTIVE BUCHUNGEN / VERLAUF ============== --}}
<section class="grid gap-4 lg:grid-cols-2">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Aktive Buchungen') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ __('läuft aktuell') }}</span>
</div>
<div class="p-5">
@forelse ($activeBookings as $booking)
<div>{{ $booking }}</div>
@empty
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.calendar-days class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Noch keine aktiven Buchungen') }}
</div>
<p class="text-[12px] text-[color:var(--color-ink-3)] max-w-md m-0">
{{ __('Gebuchte Highlights, Newsletter-Platzierungen oder Add-ons erscheinen hier mit Laufzeit und zugehöriger Firma.') }}
</p>
</div>
@endforelse
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Verlauf') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ __('verbrauchte Credits') }}</span>
</div>
<div class="p-5">
@forelse ($bookingHistory as $booking)
<div>{{ $booking }}</div>
@empty
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-bg-subtle)] border border-[color:var(--color-bg-rule)] text-[color:var(--color-ink-3)]">
<flux:icon.clock class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Noch kein Buchungsverlauf') }}
</div>
<p class="text-[12px] text-[color:var(--color-ink-3)] max-w-md m-0">
{{ __('Nach dem ersten Checkout werden Verbrauch, Rechnungsbezug und betroffene Pressemitteilung hier nachvollziehbar.') }}
</p>
</div>
@endforelse
</div>
</article>
</section>
</div>