- 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>
69 lines
3.8 KiB
PHP
69 lines
3.8 KiB
PHP
<?php
|
||
|
||
use App\Http\Controllers\CheckoutController;
|
||
use App\Http\Controllers\LegacyInvoicePdfController;
|
||
use App\Http\Middleware\EnsureUserIsCustomer;
|
||
use App\Http\Middleware\LogSlowAdminRequests;
|
||
use Illuminate\Support\Facades\Route;
|
||
use Livewire\Volt\Volt;
|
||
|
||
// ============================================================
|
||
// "Mein Bereich" – Eigentümer-Sicht im gemeinsamen Admin-Panel
|
||
// URL-Prefix: /admin/me/*
|
||
// Routen-Name: me.*
|
||
// Zugriff: Rollen admin, editor, customer (alle eingeloggten Panel-User)
|
||
// ============================================================
|
||
Route::middleware(['auth', 'verified', EnsureUserIsCustomer::class, LogSlowAdminRequests::class])
|
||
->prefix('admin/me')
|
||
->name('me.')
|
||
->group(function () {
|
||
|
||
Volt::route('/', 'customer.dashboard')->name('dashboard');
|
||
|
||
Volt::route('press-releases', 'customer.press-releases.index')->name('press-releases.index');
|
||
Volt::route('press-releases/create', 'customer.press-releases.create')->name('press-releases.create');
|
||
Volt::route('press-releases/{id}', 'customer.press-releases.show')->name('press-releases.show');
|
||
Volt::route('press-releases/{id}/edit', 'customer.press-releases.edit')->name('press-releases.edit');
|
||
|
||
Volt::route('firmen', 'customer.press-kits.index')->name('press-kits.index');
|
||
Volt::route('firmen/anlegen', 'customer.press-kits.create')->name('press-kits.create');
|
||
Volt::route('firmen/{id}', 'customer.press-kits.show')->name('press-kits.show');
|
||
Route::redirect('pressemappen', '/admin/me/firmen', 301);
|
||
Route::get('pressemappen/{id}', fn (string $id) => redirect("/admin/me/firmen/{$id}", 301))
|
||
->where('id', '[0-9]+');
|
||
|
||
Volt::route('buchungen-add-ons', 'customer.bookings')->name('bookings.index');
|
||
Route::get('checkout/abo/{planSlug}/{interval}', [CheckoutController::class, 'subscription'])
|
||
->whereIn('interval', ['monthly', 'yearly'])
|
||
->name('checkout.subscription');
|
||
Route::get('checkout/einzel-pm', [CheckoutController::class, 'singlePm'])
|
||
->name('checkout.single-pm');
|
||
Volt::route('invoices', 'customer.invoices')->name('invoices.index');
|
||
Route::get('legacy-invoices/{legacyInvoice}/pdf', LegacyInvoicePdfController::class)->name('invoices.pdf');
|
||
Volt::route('tokens', 'customer.tokens')->name('tokens.index');
|
||
|
||
Volt::route('profile', 'customer.profile')->name('profile');
|
||
Volt::route('security', 'customer.security')->name('security');
|
||
});
|
||
|
||
// ============================================================
|
||
// Legacy /customer/*-Pfade als 301-Redirect erhalten,
|
||
// damit Bookmarks und alte Mails weiter funktionieren.
|
||
// ============================================================
|
||
Route::prefix('customer')->group(function () {
|
||
Route::redirect('/', '/admin/me', 301);
|
||
Route::redirect('press-releases', '/admin/me/press-releases', 301);
|
||
Route::redirect('press-releases/create', '/admin/me/press-releases/create', 301);
|
||
Route::get('press-releases/{id}', fn (string $id) => redirect("/admin/me/press-releases/{$id}", 301))
|
||
->where('id', '[0-9]+');
|
||
Route::get('press-releases/{id}/edit', fn (string $id) => redirect("/admin/me/press-releases/{$id}/edit", 301))
|
||
->where('id', '[0-9]+');
|
||
Route::redirect('pressemappen', '/admin/me/firmen', 301);
|
||
Route::get('pressemappen/{id}', fn (string $id) => redirect("/admin/me/firmen/{$id}", 301))
|
||
->where('id', '[0-9]+');
|
||
Route::redirect('buchungen-add-ons', '/admin/me/buchungen-add-ons', 301);
|
||
Route::redirect('invoices', '/admin/me/invoices', 301);
|
||
Route::redirect('tokens', '/admin/me/tokens', 301);
|
||
Route::redirect('profile', '/admin/me/profile', 301);
|
||
Route::redirect('security', '/admin/me/security', 301);
|
||
});
|