catalog->find($addonKey)) { throw ValidationException::withMessages([ 'addon_key' => __('Unbekanntes Add-on.'), ]); } $event->loadMissing('eventPackage'); if (! $event->eventPackage) { throw ValidationException::withMessages([ 'event' => __('Kein Paket für dieses Event hinterlegt.'), ]); } $provider = $this->resolveProvider(); if ($provider === CheckoutSession::PROVIDER_PAYPAL) { return $this->createPayPalCheckout($tenant, $event, $addonKey, $quantity, $acceptedTerms, $acceptedWaiver, $payload); } return $this->createLemonSqueezyCheckout($tenant, $event, $addonKey, $quantity, $acceptedTerms, $acceptedWaiver, $payload); } /** * @param array{addon_key: string, quantity?: int, success_url?: string|null, cancel_url?: string|null} $payload * @return array{checkout_url: string|null, id: string|null, expires_at: string|null} */ protected function createLemonSqueezyCheckout( Tenant $tenant, Event $event, string $addonKey, int $quantity, bool $acceptedTerms, bool $acceptedWaiver, array $payload, ): array { $variantId = $this->catalog->resolveVariantId($addonKey); if (! $variantId) { throw ValidationException::withMessages([ 'addon_key' => __('Für dieses Add-on ist kein Lemon Squeezy Variant hinterlegt.'), ]); } $addonIntent = (string) Str::uuid(); $increments = $this->catalog->resolveIncrements($addonKey); $metadata = array_filter([ 'tenant_id' => (string) $tenant->id, 'event_id' => (string) $event->id, 'event_package_id' => (string) $event->eventPackage->id, 'addon_key' => $addonKey, 'addon_intent' => $addonIntent, 'quantity' => $quantity, 'lemonsqueezy_variant_id' => $variantId, 'legal_version' => $this->resolveLegalVersion(), 'accepted_terms' => $acceptedTerms ? '1' : '0', 'accepted_waiver' => $acceptedWaiver ? '1' : '0', 'success_url' => $payload['success_url'] ?? null, 'cancel_url' => $payload['cancel_url'] ?? null, ], static fn ($value) => $value !== null && $value !== ''); $response = $this->checkout->createVariantCheckout($variantId, $metadata, [ 'success_url' => $payload['success_url'] ?? null, 'return_url' => $payload['cancel_url'] ?? null, 'customer_email' => $tenant->contact_email ?? $tenant->user?->email, ]); $checkoutUrl = $response['checkout_url'] ?? null; $checkoutId = $response['id'] ?? null; if (! $checkoutUrl) { Log::warning('Lemon Squeezy addon checkout response missing url', ['response' => $response]); } EventPackageAddon::create([ 'event_package_id' => $event->eventPackage->id, 'event_id' => $event->id, 'tenant_id' => $tenant->id, 'addon_key' => $addonKey, 'quantity' => $quantity, 'variant_id' => $variantId, 'checkout_id' => $checkoutId, 'transaction_id' => null, 'status' => 'pending', 'metadata' => array_merge($metadata, [ 'increments' => $increments, 'provider_payload' => $response, 'consents' => [ 'legal_version' => $metadata['legal_version'], 'accepted_terms_at' => $acceptedTerms ? now()->toIso8601String() : null, 'digital_content_waiver_at' => $acceptedWaiver ? now()->toIso8601String() : null, ], ]), 'extra_photos' => ($increments['extra_photos'] ?? 0) * $quantity, 'extra_guests' => ($increments['extra_guests'] ?? 0) * $quantity, 'extra_gallery_days' => ($increments['extra_gallery_days'] ?? 0) * $quantity, ]); return [ 'checkout_url' => $checkoutUrl, 'expires_at' => $response['expires_at'] ?? null, 'id' => $checkoutId, ]; } /** * @param array{addon_key: string, quantity?: int, success_url?: string|null, cancel_url?: string|null} $payload * @return array{checkout_url: string|null, id: string|null, expires_at: string|null} */ protected function createPayPalCheckout( Tenant $tenant, Event $event, string $addonKey, int $quantity, bool $acceptedTerms, bool $acceptedWaiver, array $payload, ): array { $price = $this->catalog->resolvePrice($addonKey); $addonLabel = $this->catalog->find($addonKey)['label'] ?? $addonKey; if (! $price) { throw ValidationException::withMessages([ 'addon_key' => __('Dieses Add-on ist aktuell nicht verfügbar.'), ]); } $addonIntent = (string) Str::uuid(); $increments = $this->catalog->resolveIncrements($addonKey); $amount = round($price * $quantity, 2); $currency = strtoupper((string) config('checkout.currency', 'EUR')); $metadata = array_filter([ 'tenant_id' => (string) $tenant->id, 'event_id' => (string) $event->id, 'event_package_id' => (string) $event->eventPackage->id, 'addon_key' => $addonKey, 'addon_intent' => $addonIntent, 'quantity' => $quantity, 'price' => $price, 'currency' => $currency, 'legal_version' => $this->resolveLegalVersion(), 'accepted_terms' => $acceptedTerms ? '1' : '0', 'accepted_waiver' => $acceptedWaiver ? '1' : '0', 'success_url' => $payload['success_url'] ?? null, 'cancel_url' => $payload['cancel_url'] ?? null, ], static fn ($value) => $value !== null && $value !== ''); $addon = EventPackageAddon::create([ 'event_package_id' => $event->eventPackage->id, 'event_id' => $event->id, 'tenant_id' => $tenant->id, 'addon_key' => $addonKey, 'quantity' => $quantity, 'variant_id' => null, 'checkout_id' => null, 'transaction_id' => null, 'status' => 'pending', 'amount' => $amount, 'currency' => $currency, 'metadata' => array_merge($metadata, [ 'increments' => $increments, 'consents' => [ 'legal_version' => $metadata['legal_version'], 'accepted_terms_at' => $acceptedTerms ? now()->toIso8601String() : null, 'digital_content_waiver_at' => $acceptedWaiver ? now()->toIso8601String() : null, ], ]), 'extra_photos' => ($increments['extra_photos'] ?? 0) * $quantity, 'extra_guests' => ($increments['extra_guests'] ?? 0) * $quantity, 'extra_gallery_days' => ($increments['extra_gallery_days'] ?? 0) * $quantity, ]); $successUrl = $payload['success_url'] ?? null; $cancelUrl = $payload['cancel_url'] ?? $successUrl; $paypalReturnUrl = route('paypal.addon.return', absolute: true); try { $order = $this->paypalOrders->createSimpleOrder( referenceId: 'addon-'.$addon->id, description: $addonLabel, amount: $amount, currency: $currency, options: [ 'custom_id' => 'addon_'.$addon->id, 'return_url' => $paypalReturnUrl, 'cancel_url' => $paypalReturnUrl, 'locale' => $tenant->user?->preferred_locale ?? app()->getLocale(), 'request_id' => 'addon-'.$addon->id, ], ); } catch (PayPalException $exception) { Log::warning('PayPal addon checkout creation failed', [ 'addon_id' => $addon->id, 'message' => $exception->getMessage(), 'status' => $exception->status(), ]); $addon->forceFill([ 'status' => 'failed', 'error' => $exception->getMessage(), ])->save(); throw ValidationException::withMessages([ 'addon_key' => __('Add-on checkout failed.'), ]); } $orderId = $order['id'] ?? null; if (! is_string($orderId) || $orderId === '') { $addon->forceFill([ 'status' => 'failed', 'error' => 'PayPal order ID missing.', ])->save(); throw ValidationException::withMessages([ 'addon_key' => __('Add-on checkout failed.'), ]); } $approveUrl = $this->paypalOrders->resolveApproveUrl($order); $addon->forceFill([ 'checkout_id' => $orderId, 'metadata' => array_merge($addon->metadata ?? [], array_filter([ 'paypal_order_id' => $orderId, 'paypal_approve_url' => $approveUrl, 'paypal_success_url' => $successUrl, 'paypal_cancel_url' => $cancelUrl, 'paypal_created_at' => now()->toIso8601String(), ])), ])->save(); return [ 'checkout_url' => $approveUrl, 'expires_at' => null, 'id' => $orderId, ]; } protected function resolveProvider(): ?string { $provider = config('package-addons.provider'); if (is_string($provider) && $provider !== '') { return $provider; } $default = config('checkout.default_provider'); return is_string($default) && $default !== '' ? $default : null; } protected function resolveLegalVersion(): string { return config('app.legal_version', now()->toDateString()); } }