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:stripe,paypal', 'event_id' => 'nullable|exists:events,id', // For endcustomer ]); $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 createPaymentIntent(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.']); } \Stripe\Stripe::setApiKey(config('services.stripe.secret')); $paymentIntent = \Stripe\PaymentIntent::create([ 'amount' => $package->price * 100, 'currency' => 'eur', 'metadata' => [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'type' => 'endcustomer_event', ], ]); return response()->json([ 'client_secret' => $paymentIntent->client_secret, ]); } public function completePurchase(Request $request): JsonResponse { $request->validate([ 'package_id' => 'required|exists:packages,id', 'payment_method_id' => 'required_without:paypal_order_id|string', 'paypal_order_id' => 'required_without:payment_method_id|string', ]); $package = Package::findOrFail($request->package_id); $tenant = $request->attributes->get('tenant'); if (! $tenant) { throw ValidationException::withMessages(['tenant' => 'Tenant not found.']); } $provider = $request->has('paypal_order_id') ? 'paypal' : 'stripe'; DB::transaction(function () use ($request, $package, $tenant, $provider) { PackagePurchase::create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'provider_id' => $request->input($provider === 'paypal' ? 'paypal_order_id' : 'payment_method_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_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 createPayPalOrder(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.']); } $environment = config('services.paypal.sandbox', true) ? new SandboxEnvironment( config('services.paypal.client_id'), config('services.paypal.secret') ) : new LiveEnvironment( config('services.paypal.client_id'), config('services.paypal.secret') ); $client = PayPalClient::client($environment); $request = new OrdersCreateRequest; $request->prefer('return=representation'); $request->body = [ 'intent' => 'CAPTURE', 'purchase_units' => [[ 'amount' => [ 'currency_code' => 'EUR', 'value' => number_format($package->price, 2, '.', ''), ], 'description' => 'Fotospiel Package: '.$package->name, 'custom_id' => json_encode([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'user_id' => $tenant->user_id ?? null, ]), ]], 'application_context' => [ 'shipping_preference' => 'NO_SHIPPING', 'user_action' => 'PAY_NOW', ], ]; try { $response = $client->execute($request); $order = $response->result; return response()->json([ 'orderID' => $order->id, ]); } catch (\Exception $e) { Log::error('PayPal order creation error: '.$e->getMessage()); throw ValidationException::withMessages(['payment' => 'PayPal-Bestellung fehlgeschlagen.']); } } public function capturePayPalOrder(Request $request): JsonResponse { $request->validate([ 'order_id' => 'required|string', ]); $orderId = $request->order_id; $environment = config('services.paypal.sandbox', true) ? new SandboxEnvironment( config('services.paypal.client_id'), config('services.paypal.secret') ) : new LiveEnvironment( config('services.paypal.client_id'), config('services.paypal.secret') ); $client = PayPalClient::client($environment); $request = new OrdersCaptureRequest($orderId); $request->prefer('return=representation'); try { $response = $client->execute($request); $capture = $response->result; if ($capture->status !== 'COMPLETED') { throw new \Exception('PayPal capture not completed: '.$capture->status); } $customId = $capture->purchaseUnits[0]->customId ?? null; if (! $customId) { throw new \Exception('No metadata in PayPal order'); } $metadata = json_decode($customId, true); $tenant = Tenant::find($metadata['tenant_id']); $package = Package::find($metadata['package_id']); if (! $tenant || ! $package) { throw new \Exception('Tenant or package not found'); } // Idempotent check $existing = PackagePurchase::where('provider_id', $orderId)->first(); if ($existing) { return response()->json(['success' => true, 'message' => 'Already processed']); } DB::transaction(function () use ($tenant, $package, $orderId) { PackagePurchase::create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'provider_id' => $orderId, 'price' => $package->price, 'type' => $package->type === 'endcustomer' ? 'endcustomer_event' : 'reseller_subscription', 'purchased_at' => now(), 'metadata' => json_encode(['paypal_order' => $orderId]), ]); // Trial logic for first reseller subscription $activePackages = TenantPackage::where('tenant_id', $tenant->id) ->where('active', true) ->where('package_id', '!=', $package->id) // Exclude current if renewing ->count(); $expiresAt = now()->addYear(); if ($activePackages === 0 && $package->type === 'reseller_subscription') { $expiresAt = now()->addDays(14); // 14-day trial Log::info('PayPal trial activated for tenant', ['tenant_id' => $tenant->id]); } TenantPackage::updateOrCreate( ['tenant_id' => $tenant->id, 'package_id' => $package->id], [ 'price' => $package->price, 'purchased_at' => now(), 'active' => true, 'expires_at' => $expiresAt, ] ); $tenant->update(['subscription_status' => 'active']); }); Log::info('PayPal order captured successfully', ['order_id' => $orderId, 'tenant_id' => $tenant->id]); return response()->json(['success' => true, 'message' => 'Payment successful']); } catch (\Exception $e) { Log::error('PayPal capture error: '.$e->getMessage(), ['order_id' => $orderId]); return response()->json(['success' => false, 'message' => 'Capture failed: '.$e->getMessage()], 422); } } 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_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 { // Reseller subscription \App\Models\TenantPackage::create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'price' => $package->price, 'purchased_at' => now(), 'expires_at' => now()->addYear(), '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 { $type = $request->type; if ($type === 'reseller_subscription') { $response = (new StripeController)->createSubscription($request); return $response; } else { $response = (new StripeController)->createPaymentIntent($request); return $response; } } // Helper for PayPal client - add this if not exists, or use global // But since SDK has PayPalClient, assume it's used }