- 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:
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user