query('type', 'endcustomer'); $packages = Package::where('type', $type) ->orderBy('price') ->get(); $packages->each(function ($package) { if (is_string($package->features)) { $decoded = json_decode($package->features, true); $package->features = is_array($decoded) ? $decoded : []; return; } if (! is_array($package->features)) { $package->features = []; } }); return response()->json([ 'data' => $packages, 'message' => "Packages for type '{$type}' loaded successfully.", ]); } public function purchase(Request $request): JsonResponse { $request->validate([ 'package_id' => 'required|exists:packages,id', 'type' => 'required|in:endcustomer,reseller', 'payment_method' => 'required|in:paypal', 'event_id' => 'nullable|exists:events,id', // For endcustomer 'success_url' => 'nullable|url', 'return_url' => 'nullable|url', ]); $package = Package::findOrFail($request->package_id); $tenant = $request->attributes->get('tenant'); if (! $tenant) { throw ValidationException::withMessages(['tenant' => 'Tenant not found.']); } if ($package->price == 0) { // Free package - direct assignment return $this->handleFreePurchase($request, $package, $tenant); } // Paid purchase return $this->handlePaidPurchase($request, $package, $tenant); } public function completePurchase(Request $request): JsonResponse { $request->validate([ 'package_id' => 'required|exists:packages,id', 'paypal_order_id' => 'required|string', ]); $package = Package::findOrFail($request->package_id); $tenant = $request->attributes->get('tenant'); if (! $tenant) { throw ValidationException::withMessages(['tenant' => 'Tenant not found.']); } $provider = 'paypal'; DB::transaction(function () use ($request, $package, $tenant, $provider) { PackagePurchase::create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'provider' => $provider, 'provider_id' => $request->input('paypal_order_id'), 'price' => $package->price, 'type' => 'endcustomer_event', 'purchased_at' => now(), 'metadata' => json_encode(['note' => 'Wizard purchase', 'provider' => $provider]), ]); TenantPackage::create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'price' => $package->price, 'purchased_at' => now(), 'active' => true, ]); }); return response()->json([ 'message' => 'Purchase completed successfully.', 'provider' => $provider, ], 201); } public function assignFree(Request $request): JsonResponse { $request->validate([ 'package_id' => 'required|exists:packages,id', ]); $package = Package::findOrFail($request->package_id); $tenant = $request->attributes->get('tenant'); if (! $tenant) { throw ValidationException::withMessages(['tenant' => 'Tenant not found.']); } if ($package->price != 0) { throw ValidationException::withMessages(['package' => 'Not a free package.']); } DB::transaction(function () use ($package, $tenant) { PackagePurchase::create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'provider' => 'free', 'provider_id' => 'free_wizard', 'price' => $package->price, 'type' => 'endcustomer_event', 'purchased_at' => now(), 'metadata' => json_encode(['note' => 'Free via wizard']), ]); TenantPackage::create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'price' => $package->price, 'purchased_at' => now(), 'active' => true, ]); }); return response()->json([ 'message' => 'Free package assigned successfully.', ], 201); } public function createPayPalCheckout(Request $request): JsonResponse { $request->validate([ 'package_id' => 'required|exists:packages,id', 'success_url' => 'nullable|url', 'return_url' => 'nullable|url', 'cancel_url' => 'nullable|url', 'locale' => 'nullable|string|max:10', ]); $package = Package::findOrFail($request->integer('package_id')); $tenant = $request->attributes->get('tenant'); $user = $request->user(); if (! $tenant) { throw ValidationException::withMessages(['tenant' => 'Tenant context missing.']); } if (! $user) { throw ValidationException::withMessages(['user' => 'User context missing.']); } $session = $this->sessions->createOrResume($user, $package, [ 'tenant' => $tenant, ]); $this->sessions->selectProvider($session, CheckoutSession::PROVIDER_PAYPAL); $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(); $successUrl = $request->input('success_url') ?? $request->input('return_url'); $cancelUrl = $request->input('cancel_url') ?? $request->input('return_url'); $paypalReturnUrl = route('paypal.return', absolute: true); try { $order = $this->paypalOrders->createOrder($session, $package, [ 'return_url' => $paypalReturnUrl, 'cancel_url' => $paypalReturnUrl, 'locale' => $request->input('locale'), 'request_id' => $session->id, ]); } catch (PayPalException $exception) { Log::warning('PayPal order creation failed (tenant)', [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'session_id' => $session->id, 'message' => $exception->getMessage(), 'status' => $exception->status(), ]); throw ValidationException::withMessages(['paypal' => 'PayPal checkout could not be created.']); } $orderId = $order['id'] ?? null; if (! is_string($orderId) || $orderId === '') { throw ValidationException::withMessages(['paypal' => 'PayPal order ID missing.']); } $approveUrl = $this->paypalOrders->resolveApproveUrl($order); $session->forceFill([ 'paypal_order_id' => $orderId, 'provider_metadata' => array_merge($session->provider_metadata ?? [], array_filter([ 'paypal_order_id' => $orderId, 'paypal_status' => $order['status'] ?? null, 'paypal_approve_url' => $approveUrl, 'paypal_success_url' => $successUrl, 'paypal_cancel_url' => $cancelUrl, 'paypal_created_at' => now()->toIso8601String(), ])), ])->save(); $this->sessions->markRequiresCustomerAction($session, 'paypal_approval'); return response()->json([ 'order_id' => $orderId, 'approve_url' => $approveUrl, 'status' => $order['status'] ?? null, 'checkout_session_id' => $session->id, ]); } public function checkoutSessionStatus(CheckoutSessionStatusRequest $request, CheckoutSession $session): JsonResponse { $history = $session->status_history ?? []; $reason = null; foreach (array_reverse($history) as $entry) { if (($entry['status'] ?? null) === $session->status) { $reason = $entry['reason'] ?? null; break; } } $checkoutUrl = $session->provider === CheckoutSession::PROVIDER_PAYPAL ? data_get($session->provider_metadata ?? [], 'paypal_approve_url') : data_get($session->provider_metadata ?? [], 'lemonsqueezy_checkout_url'); return response()->json([ 'status' => $session->status, 'completed_at' => optional($session->completed_at)->toIso8601String(), 'reason' => $reason, 'checkout_url' => is_string($checkoutUrl) ? $checkoutUrl : null, ]); } private function handleFreePurchase(Request $request, Package $package, $tenant): JsonResponse { DB::transaction(function () use ($request, $package, $tenant) { $purchaseData = [ 'tenant_id' => $tenant->id, 'event_id' => $request->event_id, 'package_id' => $package->id, 'provider' => 'free', 'provider_id' => 'free', 'price' => $package->price, 'type' => $request->type, 'metadata' => json_encode([ 'note' => 'Free package assigned', 'ip' => $request->ip(), ]), ]; PackagePurchase::create($purchaseData); if ($request->event_id) { // Assign to event \App\Models\EventPackage::create([ 'event_id' => $request->event_id, 'package_id' => $package->id, 'price' => $package->price, 'purchased_at' => now(), ]); } else { // Partner / reseller Event-Kontingent package \App\Models\TenantPackage::create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'price' => $package->price, 'purchased_at' => now(), 'expires_at' => null, 'active' => true, ]); } }); return response()->json([ 'message' => 'Free package assigned successfully.', 'purchase' => ['package' => $package->name, 'type' => $request->type], ], 201); } private function handlePaidPurchase(Request $request, Package $package, $tenant): JsonResponse { $successUrl = $request->input('success_url') ?? $request->input('return_url'); $cancelUrl = $request->input('cancel_url') ?? $request->input('return_url'); $paypalReturnUrl = route('paypal.return', absolute: true); try { $session = $this->sessions->createOrResume($request->user(), $package, [ 'tenant' => $tenant, ]); $this->sessions->selectProvider($session, CheckoutSession::PROVIDER_PAYPAL); $order = $this->paypalOrders->createOrder($session, $package, [ 'return_url' => $paypalReturnUrl, 'cancel_url' => $paypalReturnUrl, 'locale' => $request->input('locale'), 'request_id' => $session->id, ]); } catch (PayPalException $exception) { Log::warning('PayPal order creation failed (purchase)', [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'message' => $exception->getMessage(), 'status' => $exception->status(), ]); throw ValidationException::withMessages(['paypal' => 'PayPal checkout could not be created.']); } $orderId = $order['id'] ?? null; if (! is_string($orderId) || $orderId === '') { throw ValidationException::withMessages(['paypal' => 'PayPal order ID missing.']); } return response()->json([ 'order_id' => $orderId, 'approve_url' => $this->paypalOrders->resolveApproveUrl($order), 'status' => $order['status'] ?? null, 'return_url' => $successUrl, 'cancel_url' => $cancelUrl, ]); } }