Kundenwunsch 06.10.2026: Ein Kunde kam über die Stripe-Rechnungsseite nicht an seine Rechnung und fragte nach Belegen und einer Rechnungs-E-Mail. - Profil ▸ Rechnungsadresse ▸ Rechnungsversand: Schalter „Rechnungs-PDF an die Konto-E-Mail" (Standard an), optionale „E-Mail für Rechnungen" mit Schalter „Rechnungen als PDF an diese Adresse senden" (Standard an). Neue Spalten an billing_addresses (klein, ohne FULLTEXT). - InvoiceDelivery entscheidet Empfänger und Anhang; gleiche Adresse wie das Konto ergibt eine Mail mit PDF. - Zahlungsbestätigung (STR) und Bestandskunden-Rechnung/-Erinnerung (MAN) folgen der Einstellung; die Fassung an die Rechnungs-E-Mail nennt das Konto, ohne Blindkopie und ohne Login-Button. - StripeInvoicePdf holt das Stripe-PDF einmal und legt es lokal ab (storage/app/private/invoices/stripe); Mailversand wiederholt bei Stripe-Ausfall (5 Versuche). - Rechnungsseite: „PDF öffnen" aus pressekonto, „Beleg bei Stripe" für den Zahlungsbeleg auf der Stripe-Seite. Suite 2.340 grün, einer übersprungen; Larastan und Pint ohne Meldung. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
7.5 KiB
PHP
130 lines
7.5 KiB
PHP
<?php
|
||
|
||
use App\Http\Controllers\CheckoutController;
|
||
use App\Http\Controllers\EmailUnsubscribeController;
|
||
use App\Http\Controllers\InvoicePdfController;
|
||
use App\Http\Controllers\LegacyInvoicePdfController;
|
||
use App\Http\Controllers\ProofPdfController;
|
||
use App\Http\Controllers\StripeInvoicePdfController;
|
||
use App\Http\Controllers\StripeInvoiceRedirectController;
|
||
use App\Http\Controllers\Web\PressReleaseReportController;
|
||
use App\Http\Middleware\EnsureUserIsCustomer;
|
||
use App\Http\Middleware\LogSlowAdminRequests;
|
||
use Illuminate\Support\Facades\Route;
|
||
|
||
// ============================================================
|
||
// "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 () {
|
||
|
||
Route::livewire('/', 'customer.dashboard')->name('dashboard');
|
||
|
||
Route::livewire('press-releases', 'customer.press-releases.index')->name('press-releases.index');
|
||
Route::livewire('press-releases/create', 'customer.press-releases.create')->name('press-releases.create');
|
||
Route::livewire('press-releases/{id}', 'customer.press-releases.show')->name('press-releases.show');
|
||
Route::livewire('press-releases/{id}/edit', 'customer.press-releases.edit')->name('press-releases.edit');
|
||
Route::get('press-releases/{id}/nachweis', ProofPdfController::class)
|
||
->where('id', '[0-9]+')
|
||
->name('press-releases.proof');
|
||
|
||
// Meldeverfahren (Phase 3): Eigentümer-Einstieg ohne Magic-Link —
|
||
// die Zuordnung der PM ersetzt die Verifikation (logged_in).
|
||
Route::post('press-releases/{pressReleaseId}/melden', [PressReleaseReportController::class, 'startFromCustomer'])
|
||
->where('pressReleaseId', '[0-9]+')
|
||
->name('press-releases.report');
|
||
|
||
Route::livewire('firmen', 'customer.press-kits.index')->name('press-kits.index');
|
||
Route::livewire('firmen/anlegen', 'customer.press-kits.create')->name('press-kits.create');
|
||
Route::livewire('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]+');
|
||
|
||
Route::livewire('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::post('checkout/tarifwechsel/{planSlug}/{interval}', [CheckoutController::class, 'swap'])
|
||
->whereIn('interval', ['monthly', 'yearly'])
|
||
->name('checkout.swap');
|
||
Route::get('checkout/einzel-pm', [CheckoutController::class, 'singlePm'])
|
||
->name('checkout.single-pm');
|
||
Route::get('checkout/credits/{pack}', [CheckoutController::class, 'creditTopup'])
|
||
->name('checkout.credit-topup');
|
||
Route::get('checkout/depublikation/{pressReleaseId}', [CheckoutController::class, 'depublication'])
|
||
->where('pressReleaseId', '[0-9]+')
|
||
->name('checkout.depublication');
|
||
Route::get('checkout/aenderung/{pressReleaseId}', [CheckoutController::class, 'change'])
|
||
->where('pressReleaseId', '[0-9]+')
|
||
->name('checkout.change');
|
||
Route::get('checkout/abo-verwalten', [CheckoutController::class, 'billingPortal'])
|
||
->name('checkout.billing-portal');
|
||
Route::livewire('invoices', 'customer.invoices')->name('invoices.index');
|
||
Route::get('legacy-invoices/{legacyInvoice}/pdf', LegacyInvoicePdfController::class)->name('invoices.pdf');
|
||
Route::get('invoices/{invoice}/stripe', StripeInvoiceRedirectController::class)->name('invoices.stripe');
|
||
Route::get('invoices/{invoice}/stripe-pdf', StripeInvoicePdfController::class)->name('invoices.stripe-pdf');
|
||
Route::get('invoices/{invoice}/pdf', InvoicePdfController::class)->name('invoices.manual-pdf');
|
||
Route::livewire('tokens', 'customer.tokens')->name('tokens.index');
|
||
|
||
Route::livewire('profile', 'customer.profile')->name('profile');
|
||
Route::livewire('security', 'customer.security')->name('security');
|
||
});
|
||
|
||
// ============================================================
|
||
// E-Mail-Benachrichtigungen: One-Click-Abmeldung ohne Login.
|
||
// Signierte Links aus den Benachrichtigungs-Mails (kein Ablauf, damit
|
||
// auch alte Mails funktionieren). Der POST auf die Abmelde-URL bedient
|
||
// den List-Unsubscribe-Header (RFC 8058, ohne CSRF — Ausnahme in
|
||
// bootstrap/app.php); der Wieder-Anmelden-POST kommt von der
|
||
// Bestätigungsseite selbst.
|
||
// ============================================================
|
||
Route::prefix('e-mail-benachrichtigungen')
|
||
->name('email-preferences.')
|
||
->middleware('throttle:30,1')
|
||
->group(function () {
|
||
Route::get('abmelden/{user}/{type}', [EmailUnsubscribeController::class, 'show'])
|
||
->name('unsubscribe');
|
||
Route::post('abmelden/{user}/{type}', [EmailUnsubscribeController::class, 'oneClickUnsubscribe'])
|
||
->name('unsubscribe.one-click');
|
||
Route::post('anmelden/{user}/{type}', [EmailUnsubscribeController::class, 'resubscribe'])
|
||
->name('resubscribe');
|
||
|
||
// Roadmap 1d.3: Abmeldung für Empfänger ohne Konto (Pressekontakt,
|
||
// Firmen-E-Mail einer API-Meldung) — adressbezogen, in der
|
||
// Sperrliste geführt. Signiert, damit nur ein Link aus unserer Mail
|
||
// eine Adresse abmelden kann.
|
||
Route::get('abmelden-adresse', [EmailUnsubscribeController::class, 'showAddress'])
|
||
->name('unsubscribe-address');
|
||
Route::post('abmelden-adresse', [EmailUnsubscribeController::class, 'oneClickUnsubscribeAddress'])
|
||
->name('unsubscribe-address.one-click');
|
||
Route::post('anmelden-adresse', [EmailUnsubscribeController::class, 'resubscribeAddress'])
|
||
->name('resubscribe-address');
|
||
});
|
||
|
||
// ============================================================
|
||
// 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);
|
||
});
|