- Tenant-Admin-PWA: Neues /event-admin/welcome Onboarding mit WelcomeHero, Packages-, Order-Summary- und Event-Setup-Pages, Zustandsspeicher, Routing-Guard und Dashboard-CTA für Erstnutzer; Filament-/admin-Login via Custom-View behoben.

- Brand/Theming: Marketing-Farb- und Typographievariablen in `resources/css/app.css` eingeführt, AdminLayout, Dashboardkarten und Onboarding-Komponenten entsprechend angepasst; Dokumentation (`docs/todo/tenant-admin-onboarding-fusion.md`, `docs/changes/...`) aktualisiert.
- Checkout & Payments: Checkout-, PayPal-Controller und Tests für integrierte Stripe/PayPal-Flows sowie Paket-Billing-Abläufe überarbeitet; neue PayPal SDK-Factory und Admin-API-Helper (`resources/js/admin/api.ts`) schaffen Grundlage für Billing/Members/Tasks-Seiten.
- DX & Tests: Neue Playwright/E2E-Struktur (docs/testing/e2e.md, `tests/e2e/tenant-onboarding-flow.test.ts`, Utilities), E2E-Tenant-Seeder und zusätzliche Übersetzungen/Factories zur Unterstützung der neuen Flows.
- Marketing-Kommunikation: Automatische Kontakt-Bestätigungsmail (`ContactConfirmation` + Blade-Template) implementiert; Guest-PWA unter `/event` erreichbar.
- Nebensitzung: Blogsystem gefixt und umfassenden BlogPostSeeder für Beispielinhalte angelegt.
This commit is contained in:
Codex Agent
2025-10-10 21:31:55 +02:00
parent 52197f216d
commit d04e234ca0
84 changed files with 8397 additions and 1005 deletions

View File

@@ -2,6 +2,8 @@
namespace App\Http\Controllers;
use App\Mail\Welcome;
use App\Models\AbandonedCheckout;
use App\Models\Package;
use App\Models\Tenant;
use App\Models\User;
@@ -19,6 +21,8 @@ use Illuminate\Support\Str;
use Stripe\PaymentIntent;
use Stripe\Stripe;
use App\Http\Controllers\PayPalController;
class CheckoutController extends Controller
{
public function show(Package $package)
@@ -30,6 +34,7 @@ class CheckoutController extends Controller
'package' => $package,
'packageOptions' => $packages,
'stripePublishableKey' => config('services.stripe.key'),
'paypalClientId' => config('services.paypal.client_id'),
'privacyHtml' => view('legal.datenschutz-partial')->render(),
'auth' => [
'user' => Auth::user(),
@@ -103,7 +108,7 @@ class CheckoutController extends Controller
$user->sendEmailVerificationNotification();
// Willkommens-E-Mail senden
Mail::to($user->email)->send(new \App\Mail\WelcomeMail($user, $package));
Mail::to($user)->queue(new Welcome($user));
});
return response()->json([
@@ -160,6 +165,66 @@ class CheckoutController extends Controller
]);
}
public function trackAbandonedCheckout(Request $request)
{
$validated = $request->validate([
'package_id' => 'required|exists:packages,id',
'email' => 'nullable|email',
'step' => 'nullable|string|in:package,auth,payment,confirmation',
'checkout_state' => 'nullable|array',
]);
$user = Auth::user();
if (! $user && ! empty($validated['email'])) {
$user = User::where('email', $validated['email'])->first();
}
if (! $user) {
return response()->json(['status' => 'skipped'], 202);
}
$package = Package::find($validated['package_id']);
if (! $package) {
return response()->json(['status' => 'missing_package'], 404);
}
$stepMap = [
'package' => 1,
'auth' => 2,
'payment' => 3,
'confirmation' => 4,
];
$lastStep = $stepMap[$validated['step'] ?? 'package'] ?? 1;
$checkout = AbandonedCheckout::firstOrNew([
'user_id' => $user->id,
'package_id' => $package->id,
]);
$checkout->email = $user->email;
$checkout->checkout_state = $validated['checkout_state'] ?? [
'step' => $validated['step'] ?? 'package',
];
$checkout->last_step = $lastStep;
$checkout->abandoned_at = now();
if (! $checkout->exists || $checkout->converted) {
$checkout->reminder_stage = 'none';
$checkout->reminded_at = null;
}
if (! $checkout->expires_at || $checkout->expires_at->isPast()) {
$checkout->expires_at = now()->addDays(30);
}
$checkout->converted = false;
$checkout->save();
return response()->json(['status' => 'tracked']);
}
public function createPaymentIntent(Request $request)
{
$request->validate([
@@ -252,4 +317,66 @@ class CheckoutController extends Controller
'message' => 'Zahlung erfolgreich bestätigt.',
]);
}
public function handlePayPalReturn(Request $request)
{
$orderId = $request->query('orderID');
if (!$orderId) {
return redirect('/checkout')->with('error', 'Ungültige PayPal-Rückkehr.');
}
$user = Auth::user();
if (!$user) {
return redirect('/login')->with('error', 'Bitte melden Sie sich an.');
}
try {
// Capture aufrufen
$paypalController = new PayPalController();
$captureRequest = new Request(['order_id' => $orderId]);
$captureResponse = $paypalController->captureOrder($captureRequest);
if ($captureResponse->getStatusCode() !== 200 || !isset($captureResponse->getData(true)['status']) || $captureResponse->getData(true)['status'] !== 'captured') {
Log::error('PayPal capture failed in return handler', ['order_id' => $orderId, 'response' => $captureResponse->getData(true)]);
return redirect('/checkout')->with('error', 'Zahlung konnte nicht abgeschlossen werden.');
}
// PackagePurchase finden (erzeugt durch captureOrder)
$purchase = \App\Models\PackagePurchase::where('provider_id', $orderId)
->where('tenant_id', $user->tenant_id)
->latest()
->first();
if (!$purchase) {
Log::error('No PackagePurchase found after PayPal capture', ['order_id' => $orderId, 'tenant_id' => $user->tenant_id]);
return redirect('/checkout')->with('error', 'Kauf konnte nicht verifiziert werden.');
}
$package = \App\Models\Package::find($purchase->package_id);
if (!$package) {
return redirect('/checkout')->with('error', 'Paket nicht gefunden.');
}
// TenantPackage zuweisen (ähnlich Stripe)
$user->tenant->packages()->attach($package->id, [
'purchased_at' => now(),
'expires_at' => now()->addYear(),
'is_active' => true,
]);
// pending_purchase zurücksetzen
$user->update(['pending_purchase' => false]);
Log::info('PayPal payment completed and package assigned', ['order_id' => $orderId, 'package_id' => $package->id, 'tenant_id' => $user->tenant_id]);
return redirect('/success/' . $package->id)->with('success', 'Zahlung erfolgreich! Ihr Paket wurde aktiviert.');
} catch (\Exception $e) {
Log::error('Error in PayPal return handler', ['order_id' => $orderId, 'error' => $e->getMessage()]);
return redirect('/checkout')->with('error', 'Fehler beim Abschließen der Zahlung: ' . $e->getMessage());
}
}
}