findOrFail($planId); $this->editingPlanId = $plan->id; $this->name = $plan->name; $this->monthlyPrice = number_format($plan->monthly_price_cents / 100, 2, ',', ''); $this->yearlyPrice = number_format($plan->yearly_price_cents / 100, 2, ',', ''); $this->quota = (string) $plan->press_release_quota; $this->dailyLimit = $plan->daily_limit === null ? '' : (string) $plan->daily_limit; $this->isActive = $plan->is_active; $this->sortOrder = (string) $plan->sort_order; $this->resetValidation(); Flux::modal('plan-edit')->show(); } public function save(StripePlanSyncService $stripeSync): void { // Deutsche Dezimal-Eingaben (49,00) für die numeric-Regel normalisieren. $this->monthlyPrice = str_replace(',', '.', trim($this->monthlyPrice)); $this->yearlyPrice = str_replace(',', '.', trim($this->yearlyPrice)); $validated = $this->validate( [ 'name' => ['required', 'string', 'max:120'], 'monthlyPrice' => ['required', 'numeric', 'min:0'], 'yearlyPrice' => ['required', 'numeric', 'min:0'], 'quota' => ['required', 'integer', 'min:0'], 'dailyLimit' => ['nullable', 'integer', 'min:1'], 'sortOrder' => ['required', 'integer', 'min:0'], ], attributes: [ 'name' => __('Name'), 'monthlyPrice' => __('Monatspreis'), 'yearlyPrice' => __('Jahrespreis'), 'quota' => __('PM-Kontingent'), 'dailyLimit' => __('Tageslimit'), 'sortOrder' => __('Sortierung'), ], ); $plan = Plan::query()->findOrFail($this->editingPlanId); $plan->fill([ 'name' => trim($validated['name']), 'monthly_price_cents' => $this->toCents($validated['monthlyPrice']), 'yearly_price_cents' => $this->toCents($validated['yearlyPrice']), 'press_release_quota' => (int) $validated['quota'], 'daily_limit' => $validated['dailyLimit'] === null || $validated['dailyLimit'] === '' ? null : (int) $validated['dailyLimit'], 'is_active' => $this->isActive, 'sort_order' => (int) $validated['sortOrder'], ]); $priceChanged = $plan->isDirty(['monthly_price_cents', 'yearly_price_cents']); $plan->save(); $stripeSync->syncAfterUpdate($plan, $plan->getChanges()); $this->savedMessage = $priceChanged ? __('Tarif „:name" gespeichert. Der neue Preis gilt sofort für neue Buchungen — Bestandsabos behalten ihren bisherigen Preis.', ['name' => $plan->name]) : __('Tarif „:name" gespeichert.', ['name' => $plan->name]); Flux::modal('plan-edit')->close(); } /** * Wandelt eine Preiseingabe (deutsches oder englisches Dezimalformat) * verlustfrei in Cent um. */ private function toCents(string $price): int { return (int) round(((float) str_replace(',', '.', $price)) * 100); } public function with(): array { return [ 'plans' => Plan::query()->orderBy('sort_order')->orderBy('id')->get(), 'singlePmPriceCents' => (int) config('billing.single_pm_price_cents'), 'singlePmPriceId' => config('billing.single_pm_stripe_price_id'), ]; } }; ?>
{{-- ============== PAGE HEADER ============== --}}
{{ __('Admin Backend') }} {{ __('Administration · Finanzen') }}

{{ __('Tarife & Pakete') }}

{{ __('Preise, Kontingente und Limits der Tarife pflegen. Änderungen erscheinen sofort auf der Buchungs-Seite und werden direkt nach Stripe synchronisiert.') }}

{{ __('Zahlungen') }}
@if ($savedMessage)
{{ $savedMessage }}
@endif {{-- ============== HINWEIS STRIPE-PREISLOGIK ============== --}}
{{ __('Stripe-Preise sind unveränderlich: Eine Preisänderung legt automatisch ein neues Preis-Objekt in Stripe an und deaktiviert das alte für neue Buchungen. Laufende Abos behalten ihren bisherigen Preis. Alle Preise sind Netto-Preise — die Umsatzsteuer ergänzt Stripe Tax im Checkout.') }}
{{-- ============== TARIF-TABELLE ============== --}}
{{ __('Tarife') }} {{ __(':count Tarife', ['count' => $plans->count()]) }}
{{ __('Tarif') }} {{ __('Monatlich (netto)') }} {{ __('Jährlich (netto)') }} {{ __('PM-Kontingent') }} {{ __('Tageslimit') }} {{ __('Stripe') }} {{ __('Status') }} @forelse ($plans as $plan)
{{ $plan->name }}
{{ $plan->slug }}
{{ number_format($plan->monthly_price_cents / 100, 2, ',', '.') }} € {{ number_format($plan->yearly_price_cents / 100, 2, ',', '.') }} € {{ __(':count PM / Monat', ['count' => $plan->press_release_quota]) }} {{ $plan->daily_limit ? __('max. :count / Tag', ['count' => $plan->daily_limit]) : __('ohne') }} @if ($plan->stripe_product_id && $plan->stripe_price_id_monthly && $plan->stripe_price_id_yearly) {{ __('verknüpft') }} @else {{ __('nicht synchronisiert') }} @endif @if ($plan->is_active) {{ __('Aktiv') }} @else {{ __('Inaktiv') }} @endif {{ __('Bearbeiten') }}
@empty
{{ __('Noch keine Tarife angelegt. Der Tarif-Katalog wird über den Seeder bzw. die Migrationen befüllt.') }}
@endforelse
{{-- ============== EINZEL-PM (KONFIGURATION) ============== --}}
{{ __('Einzel-Pressemitteilung') }}

{{ __('Preis: :price € netto pro Veröffentlichung.', ['price' => number_format($singlePmPriceCents / 100, 2, ',', '.')]) }} @if ($singlePmPriceId) {{ __('Stripe verknüpft') }} @else {{ __('STRIPE_PRICE_SINGLE_PM fehlt') }} @endif

{{ __('Der Einzel-PM-Preis wird in config/billing.php bzw. über die ENV-Variable STRIPE_PRICE_SINGLE_PM gepflegt (ein fester Preis, kein Tarif). Eine Änderung erfordert „billing:sync-stripe-plans" mit geleerter ENV-Variable.') }}

{{-- ============== EDIT-MODAL ============== --}}
{{ __('Tarif bearbeiten') }} {{ __('Preisänderungen erzeugen ein neues Stripe-Preis-Objekt und gelten nur für neue Buchungen.') }}
{{ __('Abbrechen') }} {{ __('Speichern & mit Stripe abgleichen') }}