Tarifwechsel im Buchungs-Modal: Abo-Swap mit anteiliger Verrechnung

Tarif-Karten bieten bei bestehendem Abo jetzt 'Zu diesem Tarif
wechseln' (pro Intervall, aktueller Tarif deaktiviert). Der Wechsel
laeuft ueber das bekannte Bestaetigungs-Modal (T1neu-Checkbox) als
POST auf die neue checkout.swap-Route: Cashier swapAndInvoice stellt
sofort um, verrechnet anteilig und hebt eine vorgemerkte Kuendigung
auf. SCA-Faelle leiten auf die Cashier-Bezahlseite. QA-Befund 07.07.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-07 12:58:32 +00:00
parent af156608b1
commit b3780e0fea
6 changed files with 289 additions and 8 deletions

View file

@ -102,7 +102,14 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
$treatment = $resolver->resolve($user->billingAddress?->country_code, $user->billingAddress?->vat_id);
$vatCents = $resolver->taxCentsFor($netCents, $treatment);
// Tarif-Auswahl mit bestehendem Abo = Tarifwechsel (Swap statt
// neuem Checkout) — das Modal erklärt die anteilige Verrechnung.
$subscription = $user->subscription();
$isSwap = $type === 'subscription' && (bool) $subscription?->valid();
$this->booking = [
'is_swap' => $isSwap,
'resumes_cancellation' => $isSwap && $subscription->onGracePeriod(),
'type' => $type,
'ref' => $ref,
'title' => $title,
@ -136,12 +143,15 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
}
$isYearly = $interval === 'yearly';
$routeName = auth()->user()->subscription()?->valid()
? 'me.checkout.swap'
: 'me.checkout.subscription';
return [
$plan->name . ' — ' . ($isYearly ? __('Jährlich') : __('Monatlich')),
__(':quota Pressemitteilungen pro Monat, Prüfung und Veröffentlichung inklusive.', ['quota' => $plan->press_release_quota]),
$isYearly ? (int) $plan->yearly_price_cents : (int) $plan->monthly_price_cents,
route('me.checkout.subscription', ['planSlug' => $plan->slug, 'interval' => $isYearly ? 'yearly' : 'monthly']),
route($routeName, ['planSlug' => $plan->slug, 'interval' => $isYearly ? 'yearly' : 'monthly']),
];
}
@ -297,6 +307,17 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
{{ __('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 === 'wechsel')
<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)]">{{ __('Ihr Tarifwechsel wurde durchgeführt!') }}</span>
{{ __('Der neue Tarif gilt ab sofort. Bereits gezahlte Beträge wurden anteilig verrechnet — die Rechnung finden Sie unter Rechnungen.') }}
</div>
</div>
@elseif ($checkoutResult === 'abbruch')
<div
class="px-4 py-3 rounded-[5px] border text-[13px] flex items-start gap-3
@ -523,7 +544,7 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
{{ __('Den passenden Tarif wählen') }}
</h2>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] max-w-[640px] m-0">
{{ __('Monatlich kündbar. Im Jahrestarif sind 2 Monate gratis — Sie zahlen 10 von 12 Monaten.') }}
{{ __('Monatstarife sind monatlich kündbar. Im Jahrestarif sind 2 Monate gratis — Sie zahlen 10 von 12 Monaten, das Abo verlängert sich um jeweils zwölf Monate und ist zum Ende der Laufzeit kündbar.') }}
</p>
</div>
@ -617,9 +638,33 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
<div class="pt-2">
@if ($subscription)
<flux:button variant="filled" class="w-full" disabled>
{{ $isCurrent ? __('Ihr aktueller Tarif') : __('Wechsel über „Abo verwalten"') }}
</flux:button>
{{-- Tarifwechsel: Swap des bestehenden Abos statt
neuem Checkout — pro Intervall, der aktuell
gebuchte Tarif ist deaktiviert. --}}
<div x-show="interval === 'monthly'">
@if ($isCurrent && $currentInterval === 'monthly')
<flux:button variant="filled" class="w-full" disabled>
{{ __('Ihr aktueller Tarif') }}
</flux:button>
@else
<flux:button variant="primary" class="w-full"
wire:click="selectBooking('subscription', '{{ $plan->slug }}', 'monthly')">
{{ __('Zu diesem Tarif wechseln') }}
</flux:button>
@endif
</div>
<div x-show="interval === 'yearly'" x-cloak>
@if ($isCurrent && $currentInterval === 'yearly')
<flux:button variant="filled" class="w-full" disabled>
{{ __('Ihr aktueller Tarif') }}
</flux:button>
@else
<flux:button variant="primary" class="w-full"
wire:click="selectBooking('subscription', '{{ $plan->slug }}', 'yearly')">
{{ __('Zu diesem Tarif wechseln') }}
</flux:button>
@endif
</div>
@else
<div x-show="interval === 'monthly'">
<flux:button variant="primary" class="w-full"
@ -930,7 +975,9 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
@if ($booking)
<div class="space-y-5">
<div>
<flux:heading size="lg">{{ __('Buchung bestätigen') }}</flux:heading>
<flux:heading size="lg">
{{ ($booking['is_swap'] ?? false) ? __('Tarifwechsel bestätigen') : __('Buchung bestätigen') }}
</flux:heading>
<flux:subheading>{{ $booking['title'] }}</flux:subheading>
@if ($booking['subtitle'])
<p class="text-[12.5px] text-[color:var(--color-ink-3)] mt-2 m-0">{{ $booking['subtitle'] }}</p>
@ -971,6 +1018,15 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Die endgültige Steuer berechnet Stripe anhand Ihrer Rechnungsadresse. Die Abrechnung erfolgt sicher über Stripe.') }}
</p>
@if ($booking['is_swap'] ?? false)
<p class="text-[11.5px] text-[color:var(--color-ink-2)] m-0">
{{ __('Der Wechsel gilt sofort: Bereits gezahlte Beträge werden anteilig gutgeschrieben, die Differenz wird umgehend in Rechnung gestellt.') }}
@if ($booking['resumes_cancellation'] ?? false)
<span class="font-semibold text-[color:var(--color-ink)]">{{ __('Ihre vorgemerkte Kündigung wird durch den Wechsel aufgehoben.') }}</span>
@endif
</p>
@endif
</div>
@if ($booking['needs_address'])
@ -1008,7 +1064,22 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
@if ($entrepreneurConfirmed)
@if ($booking['is_swap'] ?? false)
{{-- Tarifwechsel mutiert das Abo direkt (kein
Stripe-Redirect) — darum POST statt Link. --}}
@if ($entrepreneurConfirmed)
<form method="POST" action="{{ $booking['url'] }}" class="m-0">
@csrf
<flux:button type="submit" variant="primary" icon="arrow-path">
{{ __('Kostenpflichtig wechseln') }}
</flux:button>
</form>
@else
<flux:button variant="primary" icon="arrow-path" disabled>
{{ __('Kostenpflichtig wechseln') }}
</flux:button>
@endif
@elseif ($entrepreneurConfirmed)
<flux:button variant="primary" icon="lock-closed" href="{{ $booking['url'] }}">
{{ __('Kostenpflichtig buchen') }}
</flux:button>