find($package); if (! $resolvedPackage) { return redirect() ->route('packages', ['locale' => $locale]) ->with('error', __('marketing.packages.package_not_found')); } $googleStatus = session()->pull('checkout_google_status'); $googleError = session()->pull('checkout_google_error'); $googleProfile = session()->pull('checkout_google_profile'); $packageOptions = Package::orderBy('price')->get() ->map(fn (Package $pkg) => $this->presentPackage($pkg)) ->values() ->all(); return Inertia::render('marketing/CheckoutWizardPage', [ 'package' => $this->presentPackage($resolvedPackage), 'packageOptions' => $packageOptions, 'privacyHtml' => view('legal.datenschutz-partial')->render(), 'auth' => [ 'user' => Auth::user(), ], 'googleAuth' => [ 'status' => $googleStatus, 'error' => $googleError, 'profile' => $googleProfile, ], 'paddle' => [ 'environment' => config('paddle.environment'), 'client_token' => config('paddle.client_token'), ], ]); } public function register(CheckoutRegisterRequest $request): JsonResponse { $validated = $request->validated(); $package = Package::findOrFail($validated['package_id']); $user = DB::transaction(function () use ($package, $validated) { // User erstellen $user = User::create([ 'email' => $validated['email'], 'username' => $validated['username'], 'first_name' => $validated['first_name'], 'last_name' => $validated['last_name'], 'name' => trim($validated['first_name'].' '.$validated['last_name']), 'address' => $validated['address'], 'phone' => $validated['phone'], 'preferred_locale' => $validated['locale'] ?? null, 'password' => Hash::make($validated['password']), 'pending_purchase' => true, ]); // Tenant erstellen $tenant = Tenant::create([ 'user_id' => $user->id, 'name' => $validated['first_name'].' '.$validated['last_name'], 'slug' => Str::slug($validated['first_name'].' '.$validated['last_name'].'-'.now()->timestamp), 'email' => $validated['email'], 'is_active' => true, 'is_suspended' => false, 'subscription_tier' => 'free', 'subscription_expires_at' => null, 'settings' => json_encode([ 'branding' => [ 'logo_url' => null, 'primary_color' => '#3B82F6', 'secondary_color' => '#1F2937', 'font_family' => 'Inter, sans-serif', ], 'features' => [ 'photo_likes_enabled' => false, 'event_checklist' => false, 'custom_domain' => false, 'advanced_analytics' => false, ], 'custom_domain' => null, 'contact_email' => $validated['email'], 'event_default_type' => 'general', ]), ]); $user->forceFill(['tenant_id' => $tenant->id])->save(); // Package zuweisen $tenant->packages()->attach($package->id, [ 'price' => $package->price, 'purchased_at' => now(), 'expires_at' => $this->packageIsFree($package) ? now()->addYear() : now()->addYear(), 'active' => $this->packageIsFree($package), // Kostenlose Pakete sofort aktivieren ]); // E-Mail-Verifizierung senden $user->sendEmailVerificationNotification(); // Willkommens-E-Mail senden Mail::to($user) ->locale($user->preferred_locale ?? app()->getLocale()) ->queue(new Welcome($user)); return $user; }); Auth::login($user); $request->session()->put('checkout.pending_package_id', $package->id); $redirectUrl = CheckoutRoutes::wizardUrl($package, $validated['locale'] ?? null); $request->session()->put('checkout.verify_redirect', $redirectUrl); $request->session()->put('url.intended', $redirectUrl); return response()->json([ 'success' => true, 'message' => 'Registrierung erfolgreich. Bitte überprüfen Sie Ihre E-Mail zur Verifizierung.', 'redirect' => $redirectUrl, 'user' => [ 'id' => $user->id, 'email' => $user->email, 'name' => $user->name ?? trim($user->first_name.' '.$user->last_name), 'pending_purchase' => $user->pending_purchase ?? true, 'email_verified_at' => $user->email_verified_at, ], 'pending_purchase' => $user->pending_purchase ?? true, ]); } public function login(CheckoutLoginRequest $request): JsonResponse { $validated = $request->validated(); $packageId = $validated['package_id'] ?? $request->session()->get('selected_package_id'); if ($packageId) { $request->session()->put('selected_package_id', $packageId); } // Custom Auth für Identifier (E-Mail oder Username) $identifier = $validated['identifier']; $user = User::where('email', $identifier) ->orWhere('username', $identifier) ->first(); if (! $user || ! Hash::check($validated['password'], $user->password)) { return response()->json([ 'errors' => ['identifier' => ['Ungültige Anmeldedaten.']], ], 422); } Auth::login($user, $request->boolean('remember')); $request->session()->regenerate(); // Checkout-spezifische Logik DB::transaction(function () use ($request, $user, $packageId) { if ($packageId && ! $user->pending_purchase) { $user->update(['pending_purchase' => true]); $request->session()->put('pending_package_id', $packageId); } }); return response()->json([ 'user' => [ 'id' => $user->id, 'email' => $user->email, 'name' => $user->name ?? null, 'pending_purchase' => $user->pending_purchase ?? false, ], 'message' => 'Login erfolgreich', ]); } public function activateFree( CheckoutFreeActivationRequest $request, CheckoutSessionService $sessions, CheckoutAssignmentService $assignment, ): JsonResponse { $validated = $request->validated(); $user = $request->user(); if (! $user) { return response()->json(['message' => 'Unauthenticated.'], 401); } $package = Package::findOrFail($validated['package_id']); if ($package->price > 0) { return response()->json([ 'message' => 'Package is not eligible for free activation.', ], 422); } $session = $sessions->createOrResume($user, $package, [ 'tenant' => $user->tenant, 'locale' => $validated['locale'] ?? null, ]); $sessions->selectProvider($session, CheckoutSession::PROVIDER_FREE); $now = now(); $session->forceFill([ 'accepted_terms_at' => $now, 'accepted_privacy_at' => $now, 'accepted_withdrawal_notice_at' => $now, 'digital_content_waiver_at' => null, 'legal_version' => config('app.legal_version', $now->toDateString()), ])->save(); $assignment->finalise($session, [ 'provider' => CheckoutSession::PROVIDER_FREE, ]); $sessions->markCompleted($session, $now); return response()->json([ 'status' => 'completed', 'checkout_session_id' => $session->id, ]); } public function sessionStatus( CheckoutSessionStatusRequest $request, CheckoutSession $session, CheckoutSessionService $sessions, CheckoutAssignmentService $assignment, PaddleTransactionService $transactions, ): JsonResponse { $this->attemptPaddleRecovery($session, $sessions, $assignment, $transactions); $session->refresh(); return response()->json([ 'status' => $session->status, 'completed_at' => optional($session->completed_at)->toIso8601String(), ]); } public function confirmSession( CheckoutSessionConfirmRequest $request, CheckoutSession $session, CheckoutSessionService $sessions, CheckoutAssignmentService $assignment, PaddleTransactionService $transactions, ): JsonResponse { $validated = $request->validated(); $transactionId = $validated['transaction_id'] ?? null; $checkoutId = $validated['checkout_id'] ?? null; $metadata = $session->provider_metadata ?? []; $metadataUpdated = false; if ($transactionId) { $session->paddle_transaction_id = $transactionId; $metadata['paddle_transaction_id'] = $transactionId; $metadataUpdated = true; } if ($checkoutId) { $metadata['paddle_checkout_id'] = $checkoutId; $metadataUpdated = true; } if ($metadataUpdated) { $metadata['paddle_client_event_at'] = now()->toIso8601String(); $session->provider_metadata = $metadata; $session->save(); } $this->attemptPaddleRecovery($session, $sessions, $assignment, $transactions); $session->refresh(); return response()->json([ 'status' => $session->status, 'completed_at' => optional($session->completed_at)->toIso8601String(), ]); } 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']); } private function packageIsFree(Package $package): bool { if (isset($package->is_free)) { return (bool) $package->is_free; } $price = (float) $package->price; return $price <= 0; } private function attemptPaddleRecovery( CheckoutSession $session, CheckoutSessionService $sessions, CheckoutAssignmentService $assignment, PaddleTransactionService $transactions ): void { if ($session->provider !== CheckoutSession::PROVIDER_PADDLE) { return; } if (in_array($session->status, [ CheckoutSession::STATUS_COMPLETED, CheckoutSession::STATUS_FAILED, CheckoutSession::STATUS_CANCELLED, ], true)) { return; } $metadata = $session->provider_metadata ?? []; $lastPollAt = $metadata['paddle_poll_at'] ?? null; $now = now(); if ($lastPollAt) { try { $lastPoll = Carbon::parse($lastPollAt); if ($lastPoll->diffInSeconds($now) < 15) { return; } } catch (\Throwable) { // Ignore invalid timestamps. } } $checkoutId = $metadata['paddle_checkout_id'] ?? $session->paddle_checkout_id ?? null; $transactionId = $metadata['paddle_transaction_id'] ?? $session->paddle_transaction_id ?? null; if (! $checkoutId && ! $transactionId) { Log::info('[Checkout] Paddle recovery missing checkout reference, falling back to custom data scan', [ 'session_id' => $session->id, ]); } $metadata['paddle_poll_at'] = $now->toIso8601String(); $session->forceFill([ 'provider_metadata' => $metadata, ])->save(); try { $transaction = $transactionId ? $transactions->retrieve($transactionId) : null; if (! $transaction && $checkoutId) { $transaction = $transactions->findByCheckoutId($checkoutId); } if (! $transaction) { $transaction = $transactions->findByCustomData([ 'checkout_session_id' => $session->id, 'package_id' => (string) $session->package_id, 'tenant_id' => (string) $session->tenant_id, ]); } } catch (PaddleException $exception) { Log::warning('[Checkout] Paddle recovery failed', [ 'session_id' => $session->id, 'checkout_id' => $checkoutId, 'transaction_id' => $transactionId, 'status' => $exception->status(), 'message' => $exception->getMessage(), ]); return; } catch (\Throwable $exception) { Log::warning('[Checkout] Paddle recovery failed', [ 'session_id' => $session->id, 'checkout_id' => $checkoutId, 'transaction_id' => $transactionId, 'message' => $exception->getMessage(), ]); return; } if (! $transaction) { Log::info('[Checkout] Paddle recovery: transaction not found', [ 'session_id' => $session->id, 'checkout_id' => $checkoutId, 'transaction_id' => $transactionId, ]); return; } $status = strtolower((string) ($transaction['status'] ?? '')); $transactionId = $transactionId ?: ($transaction['id'] ?? null); if ($transactionId && $session->paddle_transaction_id !== $transactionId) { $session->forceFill([ 'paddle_transaction_id' => $transactionId, ])->save(); } if ($status === 'completed') { $sessions->markProcessing($session, [ 'paddle_status' => $status, 'paddle_transaction_id' => $transactionId, 'paddle_recovered_at' => $now->toIso8601String(), ]); $assignment->finalise($session, [ 'source' => 'paddle_poll', 'provider' => CheckoutSession::PROVIDER_PADDLE, 'provider_reference' => $transactionId, 'payload' => $transaction, ]); $sessions->markCompleted($session, $now); Log::info('[Checkout] Paddle session recovered via API', [ 'session_id' => $session->id, 'checkout_id' => $checkoutId, 'transaction_id' => $transactionId, ]); return; } if (in_array($status, ['failed', 'cancelled', 'canceled'], true)) { $sessions->markFailed($session, 'paddle_'.$status); Log::info('[Checkout] Paddle transaction failed', [ 'session_id' => $session->id, 'checkout_id' => $checkoutId, 'transaction_id' => $transactionId, 'status' => $status, ]); return; } Log::info('[Checkout] Paddle transaction pending', [ 'session_id' => $session->id, 'checkout_id' => $checkoutId, 'transaction_id' => $transactionId, 'status' => $status, ]); } }