Checkout: Stripe-Tax-Adressanforderung erfüllen
Stripe Tax verlangt eine gültige Kundenadresse. Beide Checkout-Sessions erfassen jetzt die Rechnungsadresse verpflichtend und speichern sie am Stripe-Customer (customer_update address/name = auto; Name ist Pflicht bei aktivierter USt-ID-Abfrage). Zusätzlich liefert User::stripeAddress() die lokale Rechnungsadresse bei der Customer-Anlage mit (Cashier-Hook). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6a82e2a2a8
commit
8f3261d0b4
3 changed files with 191 additions and 133 deletions
|
|
@ -80,6 +80,31 @@ class User extends Authenticatable
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adresse für die Stripe-Customer-Anlage (Cashier-Hook). Stripe Tax
|
||||||
|
* braucht eine gültige Kundenadresse — falls lokal eine
|
||||||
|
* Rechnungsadresse gepflegt ist, wird sie direkt mitgegeben; sonst
|
||||||
|
* speichert der Checkout die dort erfasste Adresse (customer_update).
|
||||||
|
*
|
||||||
|
* @return array<string, string|null>|null
|
||||||
|
*/
|
||||||
|
public function stripeAddress(): ?array
|
||||||
|
{
|
||||||
|
$address = $this->billingAddress;
|
||||||
|
|
||||||
|
if (! $address) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'line1' => $address->address1,
|
||||||
|
'line2' => $address->address2,
|
||||||
|
'postal_code' => $address->postal_code,
|
||||||
|
'city' => $address->city,
|
||||||
|
'country' => $address->country_code,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Der Tarif des aktiven Stripe-Abos, aufgelöst über die in `plans`
|
* Der Tarif des aktiven Stripe-Abos, aufgelöst über die in `plans`
|
||||||
* gepflegten Stripe-Preis-IDs. Null ohne (gültiges) Abo.
|
* gepflegten Stripe-Preis-IDs. Null ohne (gültiges) Abo.
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,28 @@ class StripeCheckoutService
|
||||||
|
|
||||||
return $user
|
return $user
|
||||||
->newSubscription('default', $priceId)
|
->newSubscription('default', $priceId)
|
||||||
->checkout([
|
->checkout($this->sessionOptions());
|
||||||
'success_url' => route('me.bookings.index', ['checkout' => 'erfolg']),
|
}
|
||||||
'cancel_url' => route('me.bookings.index', ['checkout' => 'abbruch']),
|
|
||||||
]);
|
/**
|
||||||
|
* Gemeinsame Session-Optionen: Stripe Tax braucht eine gültige
|
||||||
|
* Kundenadresse — die im Checkout erfasste Rechnungsadresse (und der
|
||||||
|
* Name, Pflicht bei USt-ID-Abfrage) wird darum am Stripe-Customer
|
||||||
|
* gespeichert (`customer_update: auto`).
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function sessionOptions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'success_url' => route('me.bookings.index', ['checkout' => 'erfolg']),
|
||||||
|
'cancel_url' => route('me.bookings.index', ['checkout' => 'abbruch']),
|
||||||
|
'billing_address_collection' => 'required',
|
||||||
|
'customer_update' => [
|
||||||
|
'address' => 'auto',
|
||||||
|
'name' => 'auto',
|
||||||
|
],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -52,8 +70,7 @@ class StripeCheckoutService
|
||||||
public function forSinglePurchase(User $user, SinglePurchase $purchase): Checkout
|
public function forSinglePurchase(User $user, SinglePurchase $purchase): Checkout
|
||||||
{
|
{
|
||||||
return $user->checkout([config('billing.single_pm_stripe_price_id') => 1], [
|
return $user->checkout([config('billing.single_pm_stripe_price_id') => 1], [
|
||||||
'success_url' => route('me.bookings.index', ['checkout' => 'erfolg']),
|
...$this->sessionOptions(),
|
||||||
'cancel_url' => route('me.bookings.index', ['checkout' => 'abbruch']),
|
|
||||||
'metadata' => ['single_purchase_id' => (string) $purchase->id],
|
'metadata' => ['single_purchase_id' => (string) $purchase->id],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,10 @@ use Livewire\Volt\Component;
|
||||||
* echte Buchungsdaten. Launch-Credits (Extra-PM, Boost, Nachweis-PDF)
|
* echte Buchungsdaten. Launch-Credits (Extra-PM, Boost, Nachweis-PDF)
|
||||||
* folgen mit Phase 9I, das Credit-Wallet mit Phase 2.
|
* folgen mit Phase 9I, das Credit-Wallet mit Phase 2.
|
||||||
*/
|
*/
|
||||||
new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class extends Component
|
new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class extends Component {
|
||||||
{
|
|
||||||
public function formatEuro(int $cents): string
|
public function formatEuro(int $cents): string
|
||||||
{
|
{
|
||||||
return number_format($cents / 100, $cents % 100 === 0 ? 0 : 2, ',', '.').' €';
|
return number_format($cents / 100, $cents % 100 === 0 ? 0 : 2, ',', '.') . ' €';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function planIcon(Plan $plan): string
|
public function planIcon(Plan $plan): string
|
||||||
|
|
@ -39,9 +38,7 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
|
|
||||||
$currentInterval = null;
|
$currentInterval = null;
|
||||||
if ($currentPlan && $subscription) {
|
if ($currentPlan && $subscription) {
|
||||||
$currentInterval = $subscription->stripe_price === $currentPlan->stripe_price_id_yearly
|
$currentInterval = $subscription->stripe_price === $currentPlan->stripe_price_id_yearly ? 'yearly' : 'monthly';
|
||||||
? 'yearly'
|
|
||||||
: 'monthly';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
@ -57,24 +54,14 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
|
|
||||||
// Bestandstarife: laufende Legacy-Vereinbarungen (MAN-Kreis,
|
// Bestandstarife: laufende Legacy-Vereinbarungen (MAN-Kreis,
|
||||||
// unbegrenzte PMs — Entscheidung 12.06.2026).
|
// unbegrenzte PMs — Entscheidung 12.06.2026).
|
||||||
'legacyOptions' => $user->userPaymentOptions()
|
'legacyOptions' => $user
|
||||||
->whereIn('status', [
|
->userPaymentOptions()
|
||||||
UserPaymentOptionStatus::Active->value,
|
->whereIn('status', [UserPaymentOptionStatus::Active->value, UserPaymentOptionStatus::Grandfathered->value])
|
||||||
UserPaymentOptionStatus::Grandfathered->value,
|
|
||||||
])
|
|
||||||
->orderBy('current_period_end')
|
->orderBy('current_period_end')
|
||||||
->get(),
|
->get(),
|
||||||
|
|
||||||
'openPurchases' => $user->singlePurchases()
|
'openPurchases' => $user->singlePurchases()->grantingSubmission()->orderBy('paid_at')->get(),
|
||||||
->grantingSubmission()
|
'consumedPurchases' => $user->singlePurchases()->where('status', SinglePurchaseStatus::Consumed->value)->with('pressRelease')->latest('consumed_at')->limit(10)->get(),
|
||||||
->orderBy('paid_at')
|
|
||||||
->get(),
|
|
||||||
'consumedPurchases' => $user->singlePurchases()
|
|
||||||
->where('status', SinglePurchaseStatus::Consumed->value)
|
|
||||||
->with('pressRelease')
|
|
||||||
->latest('consumed_at')
|
|
||||||
->limit(10)
|
|
||||||
->get(),
|
|
||||||
|
|
||||||
'quotaRemaining' => $user->pressReleaseQuotaRemaining(),
|
'quotaRemaining' => $user->pressReleaseQuotaRemaining(),
|
||||||
'quotaTotal' => $user->pressReleaseQuotaTotal(),
|
'quotaTotal' => $user->pressReleaseQuotaTotal(),
|
||||||
|
|
@ -87,16 +74,19 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
{{-- ============== CHECKOUT-RÜCKMELDUNG ============== --}}
|
{{-- ============== CHECKOUT-RÜCKMELDUNG ============== --}}
|
||||||
@if ($checkoutResult === 'erfolg')
|
@if ($checkoutResult === 'erfolg')
|
||||||
<div class="px-4 py-3 rounded-[5px] border text-[13px] flex items-start gap-3
|
<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)]">
|
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)]" />
|
<flux:icon.check-circle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<span class="font-semibold text-[color:var(--color-ink)]">{{ __('Vielen Dank für Ihre Buchung!') }}</span>
|
<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.') }}
|
{{ __('Die Zahlung wird von Stripe bestätigt — die Buchung erscheint hier in wenigen Augenblicken. Die Rechnung finden Sie anschließend unter Rechnungen.') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@elseif ($checkoutResult === 'abbruch')
|
@elseif ($checkoutResult === 'abbruch')
|
||||||
<div class="px-4 py-3 rounded-[5px] border text-[13px] flex items-start gap-3
|
<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)]">
|
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)]" />
|
<flux:icon.information-circle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-ink-3)]" />
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
|
|
@ -106,7 +96,8 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if ($checkoutNotice)
|
@if ($checkoutNotice)
|
||||||
<div class="px-4 py-3 rounded-[5px] border text-[13px] flex items-start gap-3
|
<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)]">
|
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)]" />
|
<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 class="flex-1">{{ $checkoutNotice }}</div>
|
||||||
|
|
@ -129,7 +120,8 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2 flex-shrink-0">
|
<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>
|
<flux:button size="sm" variant="filled" icon="document-text" href="{{ route('me.invoices.index') }}"
|
||||||
|
wire:navigate>
|
||||||
{{ __('Rechnungen') }}
|
{{ __('Rechnungen') }}
|
||||||
</flux:button>
|
</flux:button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -139,90 +131,94 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
{{-- Erscheint erst, wenn eine Buchung existiert — vorher würde hier nur
|
{{-- Erscheint erst, wenn eine Buchung existiert — vorher würde hier nur
|
||||||
„kein Tarif" stehen und das Kontingent wäre irreführend. --}}
|
„kein Tarif" stehen und das Kontingent wäre irreführend. --}}
|
||||||
@if ($subscription || $legacyOptions->isNotEmpty() || $openPurchases->isNotEmpty())
|
@if ($subscription || $legacyOptions->isNotEmpty() || $openPurchases->isNotEmpty())
|
||||||
<article class="panel">
|
<article class="panel">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<span class="section-eyebrow">{{ __('Aktueller Tarif') }}</span>
|
<span class="section-eyebrow">{{ __('Aktueller Tarif') }}</span>
|
||||||
@if ($currentPlan)
|
|
||||||
<span class="badge hub">{{ $currentPlan->name }}</span>
|
|
||||||
@elseif ($legacyOptions->isNotEmpty())
|
|
||||||
<span class="badge ok dot">{{ __('Bestandstarif') }}</span>
|
|
||||||
@else
|
|
||||||
<span class="badge hub">{{ __('Einzel-PM') }}</span>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
<div class="p-5 grid gap-5 md:grid-cols-[1.2fr_0.8fr]">
|
|
||||||
<div class="space-y-2">
|
|
||||||
@if ($currentPlan)
|
@if ($currentPlan)
|
||||||
<div class="text-[28px] font-bold tracking-[-0.7px] text-[color:var(--color-ink)]">
|
<span class="badge hub">{{ $currentPlan->name }}</span>
|
||||||
{{ $currentInterval === 'yearly'
|
@elseif ($legacyOptions->isNotEmpty())
|
||||||
? $this->formatEuro($currentPlan->yearly_price_cents).' / '.__('Jahr')
|
<span class="badge ok dot">{{ __('Bestandstarif') }}</span>
|
||||||
: $this->formatEuro($currentPlan->monthly_price_cents).' / '.__('Monat') }}
|
@else
|
||||||
<span class="text-[13px] font-normal text-[color:var(--color-ink-3)]">{{ __('netto') }}</span>
|
<span class="badge hub">{{ __('Einzel-PM') }}</span>
|
||||||
</div>
|
@endif
|
||||||
<p class="text-[12.5px] text-[color:var(--color-ink-2)] m-0">
|
</div>
|
||||||
{{ __(':quota Pressemitteilungen pro Monat', ['quota' => $currentPlan->press_release_quota]) }}
|
<div class="p-5 grid gap-5 md:grid-cols-[1.2fr_0.8fr]">
|
||||||
@if ($currentPlan->daily_limit)
|
<div class="space-y-2">
|
||||||
· {{ __('max. :limit Veröffentlichungen pro Tag', ['limit' => $currentPlan->daily_limit]) }}
|
@if ($currentPlan)
|
||||||
|
<div class="text-[28px] font-bold tracking-[-0.7px] text-[color:var(--color-ink)]">
|
||||||
|
{{ $currentInterval === 'yearly'
|
||||||
|
? $this->formatEuro($currentPlan->yearly_price_cents) . ' / ' . __('Jahr')
|
||||||
|
: $this->formatEuro($currentPlan->monthly_price_cents) . ' / ' . __('Monat') }}
|
||||||
|
<span
|
||||||
|
class="text-[13px] font-normal text-[color:var(--color-ink-3)]">{{ __('netto') }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-[12.5px] text-[color:var(--color-ink-2)] m-0">
|
||||||
|
{{ __(':quota Pressemitteilungen pro Monat', ['quota' => $currentPlan->press_release_quota]) }}
|
||||||
|
@if ($currentPlan->daily_limit)
|
||||||
|
·
|
||||||
|
{{ __('max. :limit Veröffentlichungen pro Tag', ['limit' => $currentPlan->daily_limit]) }}
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
@if ($subscription?->onGracePeriod())
|
||||||
|
<p class="text-[12px] text-[color:var(--color-warn,#b45309)] m-0">
|
||||||
|
{{ __('Gekündigt — läuft am :date aus.', ['date' => $subscription->ends_at?->format('d.m.Y')]) }}
|
||||||
|
</p>
|
||||||
@endif
|
@endif
|
||||||
</p>
|
@elseif ($legacyOptions->isNotEmpty())
|
||||||
@if ($subscription?->onGracePeriod())
|
@foreach ($legacyOptions as $option)
|
||||||
<p class="text-[12px] text-[color:var(--color-warn,#b45309)] m-0">
|
<div>
|
||||||
{{ __('Gekündigt — läuft am :date aus.', ['date' => $subscription->ends_at?->format('d.m.Y')]) }}
|
<div class="text-[15px] font-semibold text-[color:var(--color-ink)]">
|
||||||
|
{{ data_get($option->legacy_conditions, 'name') ?? ($option->paymentOption?->article_number ?? __('Bestehende Vereinbarung')) }}
|
||||||
|
</div>
|
||||||
|
<p class="text-[12.5px] text-[color:var(--color-ink-2)] mt-1 mb-0">
|
||||||
|
{{ __('Unbegrenzte Pressemitteilungen (Bestandsschutz).') }}
|
||||||
|
@if ($option->current_period_end)
|
||||||
|
{{ __('Nächste Rechnung am :date.', ['date' => $option->current_period_end->format('d.m.Y')]) }}
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">
|
||||||
|
{{ __('Ihre bisherigen Konditionen gelten unverändert weiter; die Abrechnung erfolgt wie gewohnt per Rechnung.') }}
|
||||||
|
</p>
|
||||||
|
@elseif ($openPurchases->isNotEmpty())
|
||||||
|
<div class="text-[15px] font-semibold text-[color:var(--color-ink)]">
|
||||||
|
{{ trans_choice(':count Einzel-Pressemitteilung verfügbar|:count Einzel-Pressemitteilungen verfügbar', $openPurchases->count(), ['count' => $openPurchases->count()]) }}
|
||||||
|
</div>
|
||||||
|
<p class="text-[12.5px] text-[color:var(--color-ink-2)] m-0">
|
||||||
|
{{ __('Jeder Kauf berechtigt zu genau einer Veröffentlichung — eingelöst wird er erst, wenn die Pressemitteilung live geht.') }}
|
||||||
</p>
|
</p>
|
||||||
@endif
|
@endif
|
||||||
@elseif ($legacyOptions->isNotEmpty())
|
</div>
|
||||||
@foreach ($legacyOptions as $option)
|
|
||||||
<div>
|
|
||||||
<div class="text-[15px] font-semibold text-[color:var(--color-ink)]">
|
|
||||||
{{ data_get($option->legacy_conditions, 'name') ?? $option->paymentOption?->article_number ?? __('Bestehende Vereinbarung') }}
|
|
||||||
</div>
|
|
||||||
<p class="text-[12.5px] text-[color:var(--color-ink-2)] mt-1 mb-0">
|
|
||||||
{{ __('Unbegrenzte Pressemitteilungen (Bestandsschutz).') }}
|
|
||||||
@if ($option->current_period_end)
|
|
||||||
{{ __('Nächste Rechnung am :date.', ['date' => $option->current_period_end->format('d.m.Y')]) }}
|
|
||||||
@endif
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">
|
|
||||||
{{ __('Ihre bisherigen Konditionen gelten unverändert weiter; die Abrechnung erfolgt wie gewohnt per Rechnung.') }}
|
|
||||||
</p>
|
|
||||||
@elseif ($openPurchases->isNotEmpty())
|
|
||||||
<div class="text-[15px] font-semibold text-[color:var(--color-ink)]">
|
|
||||||
{{ trans_choice(':count Einzel-Pressemitteilung verfügbar|:count Einzel-Pressemitteilungen verfügbar', $openPurchases->count(), ['count' => $openPurchases->count()]) }}
|
|
||||||
</div>
|
|
||||||
<p class="text-[12.5px] text-[color:var(--color-ink-2)] m-0">
|
|
||||||
{{ __('Jeder Kauf berechtigt zu genau einer Veröffentlichung — eingelöst wird er erst, wenn die Pressemitteilung live geht.') }}
|
|
||||||
</p>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
{{-- Kontingent nur als echte Zahl — „unbegrenzt" wäre vor dem
|
{{-- Kontingent nur als echte Zahl — „unbegrenzt" wäre vor dem
|
||||||
Launch-Schalter inhaltlich falsch. --}}
|
Launch-Schalter inhaltlich falsch. --}}
|
||||||
@if (! is_null($quotaRemaining))
|
@if (!is_null($quotaRemaining))
|
||||||
<div class="rounded-[6px] border border-[color:var(--color-bg-rule)] p-4 bg-[color:var(--color-bg-subtle)]">
|
<div
|
||||||
<div class="text-[12px] text-[color:var(--color-ink-3)]">{{ __('PM-Kontingent diesen Monat') }}</div>
|
class="rounded-[6px] border border-[color:var(--color-bg-rule)] p-4 bg-[color:var(--color-bg-subtle)]">
|
||||||
<div class="text-[22px] font-semibold text-[color:var(--color-ink)]">
|
<div class="text-[12px] text-[color:var(--color-ink-3)]">
|
||||||
{{ $quotaRemaining }} / {{ $quotaTotal }}
|
{{ __('PM-Kontingent diesen Monat') }}</div>
|
||||||
|
<div class="text-[22px] font-semibold text-[color:var(--color-ink)]">
|
||||||
|
{{ $quotaRemaining }} / {{ $quotaTotal }}
|
||||||
|
</div>
|
||||||
|
<div class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||||
|
{{ __('Wird erst bei Veröffentlichung verbraucht.') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
@endif
|
||||||
{{ __('Wird erst bei Veröffentlichung verbraucht.') }}
|
@if ($subscription)
|
||||||
</div>
|
<flux:button size="sm" variant="filled" icon="cog-6-tooth" class="w-full"
|
||||||
</div>
|
href="{{ route('me.checkout.billing-portal') }}">
|
||||||
@endif
|
{{ __('Abo verwalten') }}
|
||||||
@if ($subscription)
|
</flux:button>
|
||||||
<flux:button size="sm" variant="filled" icon="cog-6-tooth" class="w-full"
|
<p class="text-[11px] text-[color:var(--color-ink-3)] m-0">
|
||||||
href="{{ route('me.checkout.billing-portal') }}">
|
{{ __('Zahlungsmethode, Rechnungen und Kündigung — sicher über das Stripe-Kundenportal.') }}
|
||||||
{{ __('Abo verwalten') }}
|
</p>
|
||||||
</flux:button>
|
@endif
|
||||||
<p class="text-[11px] text-[color:var(--color-ink-3)] m-0">
|
</div>
|
||||||
{{ __('Zahlungsmethode, Rechnungen und Kündigung — sicher über das Stripe-Kundenportal.') }}
|
|
||||||
</p>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</article>
|
||||||
</article>
|
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- ============== TARIF-RASTER ============== --}}
|
{{-- ============== TARIF-RASTER ============== --}}
|
||||||
|
|
@ -238,15 +234,20 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-1 rounded-[6px] border border-[color:var(--color-bg-rule)] p-1 bg-[color:var(--color-bg-subtle)]">
|
<div
|
||||||
|
class="flex items-center gap-1 rounded-[6px] border border-[color:var(--color-bg-rule)] p-1 bg-[color:var(--color-bg-subtle)]">
|
||||||
<button type="button" @click="interval = 'monthly'"
|
<button type="button" @click="interval = 'monthly'"
|
||||||
class="px-3 py-1.5 rounded-[4px] text-[12.5px] font-semibold transition-colors"
|
class="px-3 py-1.5 rounded-[4px] text-[12.5px] font-semibold transition-colors"
|
||||||
:class="interval === 'monthly' ? 'bg-[color:var(--color-bg-elev)] text-[color:var(--color-ink)] shadow-sm' : 'text-[color:var(--color-ink-3)]'">
|
:class="interval === 'monthly' ?
|
||||||
|
'bg-[color:var(--color-bg-elev)] text-[color:var(--color-ink)] shadow-sm' :
|
||||||
|
'text-[color:var(--color-ink-3)]'">
|
||||||
{{ __('Monatlich') }}
|
{{ __('Monatlich') }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" @click="interval = 'yearly'"
|
<button type="button" @click="interval = 'yearly'"
|
||||||
class="px-3 py-1.5 rounded-[4px] text-[12.5px] font-semibold transition-colors"
|
class="px-3 py-1.5 rounded-[4px] text-[12.5px] font-semibold transition-colors"
|
||||||
:class="interval === 'yearly' ? 'bg-[color:var(--color-bg-elev)] text-[color:var(--color-ink)] shadow-sm' : 'text-[color:var(--color-ink-3)]'">
|
:class="interval === 'yearly' ?
|
||||||
|
'bg-[color:var(--color-bg-elev)] text-[color:var(--color-ink)] shadow-sm' :
|
||||||
|
'text-[color:var(--color-ink-3)]'">
|
||||||
{{ __('Jährlich') }} <span class="badge ok ms-1">{{ __('2 Monate gratis') }}</span>
|
{{ __('Jährlich') }} <span class="badge ok ms-1">{{ __('2 Monate gratis') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -255,15 +256,20 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
@foreach ($plans as $plan)
|
@foreach ($plans as $plan)
|
||||||
@php($isCurrent = $currentPlan && $plan->is($currentPlan))
|
@php($isCurrent = $currentPlan && $plan->is($currentPlan))
|
||||||
<article @class(['panel', 'ring-2 ring-[color:var(--color-hub)]' => $isCurrent]) wire:key="plan-{{ $plan->slug }}">
|
<article @class([
|
||||||
|
'panel',
|
||||||
|
'ring-2 ring-[color:var(--color-hub)]' => $isCurrent,
|
||||||
|
]) wire:key="plan-{{ $plan->slug }}">
|
||||||
<div class="p-6 space-y-5 flex flex-col h-full">
|
<div class="p-6 space-y-5 flex flex-col h-full">
|
||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-11 h-11 rounded-[6px] flex items-center justify-center flex-shrink-0
|
<div
|
||||||
|
class="w-11 h-11 rounded-[6px] flex items-center justify-center flex-shrink-0
|
||||||
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
|
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
|
||||||
<flux:icon :name="$this->planIcon($plan)" class="size-5" />
|
<flux:icon :name="$this->planIcon($plan)" class="size-5" />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-[17px] font-bold tracking-[-0.3px] text-[color:var(--color-ink)] m-0">{{ $plan->name }}</h3>
|
<h3 class="text-[17px] font-bold tracking-[-0.3px] text-[color:var(--color-ink)] m-0">
|
||||||
|
{{ $plan->name }}</h3>
|
||||||
</div>
|
</div>
|
||||||
@if ($isCurrent)
|
@if ($isCurrent)
|
||||||
<span class="badge hub dot">{{ __('Aktuell') }}</span>
|
<span class="badge hub dot">{{ __('Aktuell') }}</span>
|
||||||
|
|
@ -272,20 +278,28 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div x-show="interval === 'monthly'">
|
<div x-show="interval === 'monthly'">
|
||||||
<span class="text-[32px] font-bold tracking-[-0.8px] leading-none text-[color:var(--color-ink)]">{{ $this->formatEuro($plan->monthly_price_cents) }}</span>
|
<span
|
||||||
<span class="text-[12.5px] text-[color:var(--color-ink-3)]">/ {{ __('Monat') }}</span>
|
class="text-[32px] font-bold tracking-[-0.8px] leading-none text-[color:var(--color-ink)]">{{ $this->formatEuro($plan->monthly_price_cents) }}</span>
|
||||||
|
<span class="text-[12.5px] text-[color:var(--color-ink-3)]">/
|
||||||
|
{{ __('Monat') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div x-show="interval === 'yearly'" x-cloak>
|
<div x-show="interval === 'yearly'" x-cloak>
|
||||||
<span class="text-[32px] font-bold tracking-[-0.8px] leading-none text-[color:var(--color-ink)]">{{ $this->formatEuro($plan->yearly_price_cents) }}</span>
|
<span
|
||||||
<span class="text-[12.5px] text-[color:var(--color-ink-3)]">/ {{ __('Jahr') }}</span>
|
class="text-[32px] font-bold tracking-[-0.8px] leading-none text-[color:var(--color-ink)]">{{ $this->formatEuro($plan->yearly_price_cents) }}</span>
|
||||||
|
<span class="text-[12.5px] text-[color:var(--color-ink-3)]">/
|
||||||
|
{{ __('Jahr') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[11px] text-[color:var(--color-ink-3)] mt-1.5">{{ __('netto zzgl. USt.') }}</div>
|
<div class="text-[11px] text-[color:var(--color-ink-3)] mt-1.5">
|
||||||
|
{{ __('netto zzgl. USt.') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="m-0 p-0 list-none space-y-2.5 text-[12.5px] text-[color:var(--color-ink-2)] flex-1 border-t border-[color:var(--color-bg-rule)] pt-4">
|
<ul
|
||||||
|
class="m-0 p-0 list-none space-y-2.5 text-[12.5px] text-[color:var(--color-ink-2)] flex-1 border-t border-[color:var(--color-bg-rule)] pt-4">
|
||||||
<li class="flex items-start gap-2">
|
<li class="flex items-start gap-2">
|
||||||
<flux:icon.check class="size-4 flex-shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
|
<flux:icon.check class="size-4 flex-shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
|
||||||
<span><strong class="text-[color:var(--color-ink)]">{{ $plan->press_release_quota }}</strong> {{ __('Pressemitteilungen pro Monat') }}</span>
|
<span><strong
|
||||||
|
class="text-[color:var(--color-ink)]">{{ $plan->press_release_quota }}</strong>
|
||||||
|
{{ __('Pressemitteilungen pro Monat') }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex items-start gap-2">
|
<li class="flex items-start gap-2">
|
||||||
<flux:icon.check class="size-4 flex-shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
|
<flux:icon.check class="size-4 flex-shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
|
||||||
|
|
@ -309,13 +323,13 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
@else
|
@else
|
||||||
<div x-show="interval === 'monthly'">
|
<div x-show="interval === 'monthly'">
|
||||||
<flux:button variant="primary" class="w-full"
|
<flux:button variant="primary" class="w-full"
|
||||||
href="{{ route('me.checkout.subscription', ['planSlug' => $plan->slug, 'interval' => 'monthly']) }}">
|
href="{{ route('me.checkout.subscription', ['planSlug' => $plan->slug, 'interval' => 'monthly']) }}">
|
||||||
{{ __('Monatlich buchen') }}
|
{{ __('Monatlich buchen') }}
|
||||||
</flux:button>
|
</flux:button>
|
||||||
</div>
|
</div>
|
||||||
<div x-show="interval === 'yearly'" x-cloak>
|
<div x-show="interval === 'yearly'" x-cloak>
|
||||||
<flux:button variant="primary" class="w-full"
|
<flux:button variant="primary" class="w-full"
|
||||||
href="{{ route('me.checkout.subscription', ['planSlug' => $plan->slug, 'interval' => 'yearly']) }}">
|
href="{{ route('me.checkout.subscription', ['planSlug' => $plan->slug, 'interval' => 'yearly']) }}">
|
||||||
{{ __('Jährlich buchen') }}
|
{{ __('Jährlich buchen') }}
|
||||||
</flux:button>
|
</flux:button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -327,7 +341,7 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-[12px] text-[color:var(--color-ink-3)] m-0">
|
<p class="text-[12px] text-[color:var(--color-ink-3)] m-0">
|
||||||
{{ __('Mehr als 60 Pressemitteilungen pro Monat, mehrere Teams oder Sonderkonditionen? Enterprise-Konditionen erhalten Sie auf Anfrage über den Support.') }}
|
{{ __('Mehr als 60 Pressemitteilungen pro Monat, mehrere Teams oder Sonderkonditionen? Enterprise-Konditionen erhalten Sie auf Anfrage über den Support. Inklusive KI-Prüfung und Veröffentlichung.info@pressekonto.com') }}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -335,7 +349,8 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
<article class="panel">
|
<article class="panel">
|
||||||
<div class="p-5 grid gap-5 md:grid-cols-[1fr_auto] md:items-center">
|
<div class="p-5 grid gap-5 md:grid-cols-[1fr_auto] md:items-center">
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<div class="w-10 h-10 rounded-[6px] flex items-center justify-center flex-shrink-0 bg-[color:var(--color-hub-soft)] text-[color:var(--color-hub)]">
|
<div
|
||||||
|
class="w-10 h-10 rounded-[6px] flex items-center justify-center flex-shrink-0 bg-[color:var(--color-hub-soft)] text-[color:var(--color-hub)]">
|
||||||
<flux:icon.document-plus class="size-5" />
|
<flux:icon.document-plus class="size-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -343,7 +358,7 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
{{ __('Einzel-Pressemitteilung — ohne Abo') }}
|
{{ __('Einzel-Pressemitteilung — ohne Abo') }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-[12.5px] text-[color:var(--color-ink-2)] mt-1 mb-0 max-w-[560px]">
|
<p class="text-[12.5px] text-[color:var(--color-ink-2)] mt-1 mb-0 max-w-[560px]">
|
||||||
{{ __('Genau eine Veröffentlichung inklusive KI-Prüfung. Eingelöst wird der Kauf erst, wenn die Pressemitteilung live geht — Ablehnungen kosten nichts.') }}
|
{{ __('Genau eine Veröffentlichung inklusive Prüfung und Veröffentlichung. Eingelöst wird der Kauf erst, wenn die Pressemitteilung live geht — Ablehnungen kosten nichts.') }}
|
||||||
@if ($openPurchases->isNotEmpty())
|
@if ($openPurchases->isNotEmpty())
|
||||||
<span class="font-semibold text-[color:var(--color-ink)]">
|
<span class="font-semibold text-[color:var(--color-ink)]">
|
||||||
{{ trans_choice('Aktuell :count offener Kauf.|Aktuell :count offene Käufe.', $openPurchases->count(), ['count' => $openPurchases->count()]) }}
|
{{ trans_choice('Aktuell :count offener Kauf.|Aktuell :count offene Käufe.', $openPurchases->count(), ['count' => $openPurchases->count()]) }}
|
||||||
|
|
@ -357,9 +372,8 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
<div class="text-[22px] font-bold text-[color:var(--color-ink)]">{{ $singlePmPrice }}</div>
|
<div class="text-[22px] font-bold text-[color:var(--color-ink)]">{{ $singlePmPrice }}</div>
|
||||||
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ __('netto zzgl. USt.') }}</div>
|
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ __('netto zzgl. USt.') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<flux:button size="sm" variant="primary"
|
<flux:button size="sm" variant="primary" href="{{ route('me.checkout.single-pm') }}"
|
||||||
href="{{ route('me.checkout.single-pm') }}"
|
:disabled="!$singlePmAvailable">
|
||||||
:disabled="! $singlePmAvailable">
|
|
||||||
{{ __('Jetzt buchen') }}
|
{{ __('Jetzt buchen') }}
|
||||||
</flux:button>
|
</flux:button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -385,7 +399,8 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
@if ($consumedPurchases->isNotEmpty())
|
@if ($consumedPurchases->isNotEmpty())
|
||||||
<div class="divide-y divide-[color:var(--color-bg-rule)]">
|
<div class="divide-y divide-[color:var(--color-bg-rule)]">
|
||||||
@foreach ($consumedPurchases as $purchase)
|
@foreach ($consumedPurchases as $purchase)
|
||||||
<div class="py-3 first:pt-0 last:pb-0 flex items-center justify-between gap-4" wire:key="consumed-purchase-{{ $purchase->id }}">
|
<div class="py-3 first:pt-0 last:pb-0 flex items-center justify-between gap-4"
|
||||||
|
wire:key="consumed-purchase-{{ $purchase->id }}">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] truncate">
|
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] truncate">
|
||||||
{{ $purchase->pressRelease?->title ?? $purchase->type->label() }}
|
{{ $purchase->pressRelease?->title ?? $purchase->type->label() }}
|
||||||
|
|
@ -400,7 +415,8 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
|
<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
|
<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)]">
|
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" />
|
<flux:icon.clock class="size-6" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue