validate([ 'login' => ['required', 'string'], 'password' => ['required', 'string'], 'remember' => ['nullable', 'boolean'], ]); $credentials = ['password' => $data['password']]; if (filter_var($data['login'], FILTER_VALIDATE_EMAIL)) { $credentials['email'] = $data['login']; } else { $credentials['username'] = $data['login']; } if (! Auth::attempt($credentials, (bool) ($data['remember'] ?? false))) { throw ValidationException::withMessages([ 'login' => __('auth.failed'), ]); } $request->session()->regenerate(); $user = $request->user(); return response()->json([ 'status' => 'authenticated', 'user' => $this->transformUser($user), 'next_step' => 'payment', 'needs_verification' => $user?->email_verified_at === null, ]); } public function register(Request $request): JsonResponse { $data = $request->validate([ 'username' => ['required', 'string', 'max:255', 'unique:users,username'], 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:users,email'], 'password' => ['required', 'confirmed', \Illuminate\Validation\Rules\Password::defaults()], 'first_name' => ['required', 'string', 'max:255'], 'last_name' => ['required', 'string', 'max:255'], 'address' => ['required', 'string', 'max:500'], 'phone' => ['required', 'string', 'max:20'], 'privacy_consent' => ['accepted'], 'package_id' => ['nullable', 'exists:packages,id'], ]); $shouldAutoVerify = app()->environment(['local', 'testing']); $package = $data['package_id'] ? Package::find($data['package_id']) : null; DB::beginTransaction(); try { $user = User::create([ 'username' => $data['username'], 'email' => $data['email'], 'first_name' => $data['first_name'], 'last_name' => $data['last_name'], 'address' => $data['address'], 'phone' => $data['phone'], 'password' => Hash::make($data['password']), 'role' => 'user', 'pending_purchase' => $package && (($package->price ?? 0) > 0), ]); $tenant = Tenant::create([ 'user_id' => $user->id, 'name' => trim($data['first_name'].' '.$data['last_name']), 'slug' => Str::slug($data['first_name'].' '.$data['last_name'].'-'.now()->timestamp), 'email' => $data['email'], 'is_active' => true, 'is_suspended' => false, 'event_credits_balance' => 0, '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' => $data['email'], 'event_default_type' => 'general', ]), ]); if ($shouldAutoVerify) { $user->forceFill(['email_verified_at' => now()])->save(); } $assignedPackage = null; if ($package && (float) $package->price <= 0.0) { $assignedPackage = $package; TenantPackage::updateOrCreate( [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, ], [ 'price' => 0, 'active' => true, 'purchased_at' => now(), 'expires_at' => now()->addYear(), ] ); PackagePurchase::create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'provider_id' => 'free', 'price' => 0, 'type' => $package->type === 'endcustomer' ? 'endcustomer_event' : 'reseller_subscription', 'purchased_at' => now(), 'refunded' => false, ]); $tenant->update(['subscription_status' => 'active']); $user->forceFill(['pending_purchase' => false, 'role' => 'tenant_admin'])->save(); } DB::commit(); } catch (\Throwable $e) { DB::rollBack(); throw $e; } event(new Registered($user)); Auth::login($user); $request->session()->regenerate(); Mail::to($user)->queue(new \App\Mail\Welcome($user)); $nextStep = 'payment'; if ($assignedPackage) { $nextStep = 'success'; } return response()->json([ 'status' => 'registered', 'user' => $this->transformUser($user), 'next_step' => $nextStep, 'needs_verification' => $user->email_verified_at === null, 'package' => $package ? [ 'id' => $package->id, 'name' => $package->name, 'price' => $package->price, 'type' => $package->type, ] : null, ]); } public function createStripeIntent(Request $request): JsonResponse { $data = $request->validate([ 'package_id' => ['required', 'exists:packages,id'], ]); $user = $request->user(); if (! $user) { throw ValidationException::withMessages(['auth' => __('auth.login')]); } $tenant = $user->tenant; if (! $tenant) { throw ValidationException::withMessages(['tenant' => 'Tenant not found']); } $package = Package::findOrFail($data['package_id']); if ($package->price <= 0) { throw ValidationException::withMessages(['package_id' => 'Stripe payment is not required for this package.']); } Stripe::setApiKey(config('services.stripe.secret')); $intent = PaymentIntent::create([ 'amount' => (int) round($package->price * 100), 'currency' => 'eur', 'metadata' => [ 'user_id' => $user->id, 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'package_type' => $package->type, ], 'automatic_payment_methods' => ['enabled' => true], ]); return response()->json([ 'client_secret' => $intent->client_secret, 'payment_intent_id' => $intent->id, ]); } public function completeStripe(Request $request): JsonResponse { $data = $request->validate([ 'package_id' => ['required', 'exists:packages,id'], 'payment_intent_id' => ['required', 'string'], ]); $user = $request->user(); if (! $user) { throw ValidationException::withMessages(['auth' => __('auth.login')]); } $package = Package::findOrFail($data['package_id']); $tenant = $this->resolveTenant($user->id); Stripe::setApiKey(config('services.stripe.secret')); $intent = PaymentIntent::retrieve($data['payment_intent_id']); if ($intent->status !== 'succeeded') { throw ValidationException::withMessages(['payment' => 'The payment is not completed.']); } $this->finalizePurchase($tenant, $package, 'stripe', [ 'payment_intent' => $intent->id, ]); return response()->json(['status' => 'completed']); } public function createPaypalOrder(Request $request): JsonResponse { $data = $request->validate([ 'package_id' => ['required', 'exists:packages,id'], ]); $user = $request->user(); if (! $user) { throw ValidationException::withMessages(['auth' => __('auth.login')]); } $tenant = $this->resolveTenant($user->id); $package = Package::findOrFail($data['package_id']); if ($package->price <= 0) { throw ValidationException::withMessages(['package_id' => 'PayPal payment is not required for this package.']); } $client = $this->makePaypalClient(); $orders = $client->orders(); $createRequest = new OrdersCreateRequest(); $createRequest->prefer('return=representation'); $createRequest->body = [ 'intent' => 'CAPTURE', 'purchase_units' => [[ 'amount' => [ 'currency_code' => 'EUR', 'value' => number_format($package->price, 2, '.', ''), ], 'description' => 'Package: '.$package->name, 'custom_id' => json_encode([ 'user_id' => $user->id, 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'package_type' => $package->type, ]), ]], ]; try { $response = $orders->createOrder($createRequest); $order = $response->result; return response()->json([ 'order_id' => $order->id, 'status' => $order->status ?? 'CREATED', ]); } catch (HttpException $exception) { Log::error('PayPal order creation failed', [ 'message' => $exception->getMessage(), 'status_code' => $exception->statusCode ?? null, ]); return response()->json(['error' => 'Unable to create PayPal order.'], 422); } } public function capturePaypalOrder(Request $request): JsonResponse { $data = $request->validate([ 'order_id' => ['required', 'string'], 'package_id' => ['required', 'exists:packages,id'], ]); $user = $request->user(); if (! $user) { throw ValidationException::withMessages(['auth' => __('auth.login')]); } $package = Package::findOrFail($data['package_id']); $tenant = $this->resolveTenant($user->id); $client = $this->makePaypalClient(); $orders = $client->orders(); $captureRequest = new OrdersCaptureRequest($data['order_id']); $captureRequest->prefer('return=representation'); try { $response = $orders->captureOrder($captureRequest); $capture = $response->result; if (($capture->status ?? null) !== 'COMPLETED') { return response()->json(['error' => 'Capture incomplete.'], 422); } $customId = $capture->purchaseUnits[0]->customId ?? null; if ($customId) { $metadata = json_decode($customId, true); if (($metadata['package_id'] ?? null) !== $package->id || ($metadata['tenant_id'] ?? null) !== $tenant->id) { return response()->json(['error' => 'Order metadata mismatch.'], 422); } } $this->finalizePurchase($tenant, $package, 'paypal', [ 'order_id' => $data['order_id'], 'capture_status' => $capture->status ?? null, ]); return response()->json([ 'status' => 'captured', ]); } catch (HttpException $exception) { Log::error('PayPal capture failed', [ 'message' => $exception->getMessage(), 'status_code' => $exception->statusCode ?? null, ]); return response()->json(['error' => 'Unable to capture PayPal order.'], 422); } } public function assignFreePackage(Request $request): JsonResponse { $data = $request->validate([ 'package_id' => ['required', 'exists:packages,id'], ]); $user = $request->user(); if (! $user) { throw ValidationException::withMessages(['auth' => __('auth.login')]); } $package = Package::findOrFail($data['package_id']); if ($package->price > 0) { throw ValidationException::withMessages(['package_id' => 'Package is not free.']); } $tenant = $this->resolveTenant($user->id); $this->finalizePurchase($tenant, $package, 'free_wizard'); return response()->json(['status' => 'assigned']); } private function resolveTenant(int $userId): Tenant { $tenant = Tenant::where('user_id', $userId)->first(); if (! $tenant) { throw ValidationException::withMessages(['tenant' => 'Tenant not found']); } return $tenant; } private function finalizePurchase(Tenant $tenant, Package $package, string $providerId, array $metadata = []): void { TenantPackage::updateOrCreate( [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, ], [ 'price' => $package->price, 'active' => true, 'purchased_at' => now(), 'expires_at' => now()->addYear(), ] ); PackagePurchase::create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'provider_id' => $providerId, 'price' => $package->price, 'type' => $package->type === 'endcustomer' ? 'endcustomer_event' : 'reseller_subscription', 'purchased_at' => now(), 'metadata' => $metadata ? json_encode($metadata) : null, 'refunded' => false, ]); } private function makePaypalClient(): Client { return Client::create([ 'clientId' => config('services.paypal.client_id'), 'clientSecret' => config('services.paypal.secret'), 'environment' => config('services.paypal.sandbox', true) ? 'sandbox' : 'live', ]); } private function transformUser(?User $user): array { if (! $user) { return []; } return [ 'id' => $user->id, 'email' => $user->email, 'name' => trim(($user->first_name ?? '').' '.($user->last_name ?? '')) ?: $user->username, 'pending_purchase' => (bool) $user->pending_purchase, 'email_verified' => (bool) $user->email_verified_at, ]; } }