From 75086ad83b88c8eb7f602d9c50f89fc179a588a5 Mon Sep 17 00:00:00 2001 From: idevakk <219866223+idevakk@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:29:25 -0800 Subject: [PATCH] feat(payment): integrate Polar.sh payment provider with checkout flow - Build PolarProvider from scratch with proper HTTP API integration - Add encrypted configuration loading from payment_providers table via model - Implement sandbox/live environment switching with proper credential handling - Fix product creation API structure for Polar.sh requirements - Add comprehensive error handling and logging throughout checkout flow - Fix PaymentController checkout URL handling to support Polar's checkout_url response - Add debug logging for troubleshooting checkout session creation - Support both regular and trial checkout flows for Polar payments --- app/Http/Controllers/PaymentController.php | 42 + app/Services/Payments/PaymentOrchestrator.php | 37 +- app/Services/Payments/ProviderRegistry.php | 277 +++++-- .../Payments/Providers/PolarProvider.php | 742 +++++++++++------- routes/web.php | 22 + storage/framework/cache/data/.gitignore | 2 - 6 files changed, 770 insertions(+), 352 deletions(-) delete mode 100644 storage/framework/cache/data/.gitignore diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index fab53d8..d00cdc0 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -7,6 +7,7 @@ use App\Models\User; use App\Services\Payments\PaymentOrchestrator; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; use Illuminate\Validation\ValidationException; class PaymentController extends Controller @@ -75,8 +76,23 @@ class PaymentController extends Controller ], 404); } + Log::info('PaymentController: Creating checkout session', [ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'requested_provider' => $provider, + 'is_trial' => $isTrial, + 'options_count' => count($options), + ]); + $result = $this->orchestrator->createCheckoutSession($user, $plan, $provider, $options); + Log::info('PaymentController: Returning successful checkout response', [ + 'result_keys' => array_keys($result), + 'has_checkout_url' => isset($result['checkout_url']), + 'checkout_url' => $result['checkout_url'] ?? 'missing', + 'provider_subscription_id' => $result['provider_subscription_id'] ?? 'missing', + ]); + return response()->json([ 'success' => true, 'data' => $result, @@ -277,6 +293,14 @@ class PaymentController extends Controller 'is_trial' => false, ]); + Log::info('PaymentController: enhancedCheckout result', [ + 'result_keys' => array_keys($result), + 'has_redirect_url' => isset($result['redirect_url']), + 'has_session_url' => isset($result['session_url']), + 'has_checkout_url' => isset($result['checkout_url']), + 'checkout_url' => $result['checkout_url'] ?? 'missing', + ]); + // Redirect to provider's checkout page if (isset($result['redirect_url'])) { return redirect($result['redirect_url']); @@ -287,6 +311,11 @@ class PaymentController extends Controller return redirect($result['session_url']); } + // Polar checkout URL handling + if (isset($result['checkout_url'])) { + return redirect($result['checkout_url']); + } + session()->flash('error', 'Unable to create checkout session. Please try again.'); return redirect()->route('dashboard'); @@ -332,6 +361,14 @@ class PaymentController extends Controller 'trial_requires_payment_method' => $trialConfig?->trial_requires_payment_method ?? true, ]); + Log::info('PaymentController: trialCheckout result', [ + 'result_keys' => array_keys($result), + 'has_redirect_url' => isset($result['redirect_url']), + 'has_session_url' => isset($result['session_url']), + 'has_checkout_url' => isset($result['checkout_url']), + 'checkout_url' => $result['checkout_url'] ?? 'missing', + ]); + // Redirect to provider's checkout page if (isset($result['redirect_url'])) { return redirect($result['redirect_url']); @@ -342,6 +379,11 @@ class PaymentController extends Controller return redirect($result['session_url']); } + // Polar checkout URL handling + if (isset($result['checkout_url'])) { + return redirect($result['checkout_url']); + } + session()->flash('error', 'Unable to create trial checkout session. Please try again.'); return redirect()->route('dashboard'); diff --git a/app/Services/Payments/PaymentOrchestrator.php b/app/Services/Payments/PaymentOrchestrator.php index a910f43..dc96ab2 100644 --- a/app/Services/Payments/PaymentOrchestrator.php +++ b/app/Services/Payments/PaymentOrchestrator.php @@ -4,14 +4,17 @@ namespace App\Services\Payments; use App\Contracts\Payments\PaymentProviderContract; use App\Models\Coupon; +use App\Models\CouponUsage; use App\Models\Plan; use App\Models\Subscription; use App\Models\SubscriptionChange; +use App\Models\TrialExtension; use App\Models\User; use Exception; use Illuminate\Http\Request; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; class PaymentOrchestrator { @@ -588,16 +591,41 @@ class PaymentOrchestrator */ protected function getProviderForPlan(Plan $plan, ?string $providerName = null): PaymentProviderContract { + Log::info('PaymentOrchestrator: Getting provider for plan', [ + 'plan_id' => $plan->id, + 'requested_provider' => $providerName, + ]); + if ($providerName) { $provider = $this->providerRegistry->get($providerName); + Log::info('PaymentOrchestrator: Checking specific provider', [ + 'provider_name' => $providerName, + 'provider_exists' => $provider ? true : false, + 'provider_active' => $provider?->isActive(), + 'provider_supported' => $provider ? $this->isProviderSupportedForPlan($provider, $plan) : false, + ]); + if ($provider && $provider->isActive() && $this->isProviderSupportedForPlan($provider, $plan)) { + Log::info('PaymentOrchestrator: Using requested provider', [ + 'provider' => $providerName, + ]); + return $provider; } } // Find the first active provider that supports this plan foreach ($this->providerRegistry->getActiveProviders() as $provider) { + Log::info('PaymentOrchestrator: Checking fallback provider', [ + 'provider' => $provider->getName(), + 'supported' => $this->isProviderSupportedForPlan($provider, $plan), + ]); + if ($this->isProviderSupportedForPlan($provider, $plan)) { + Log::info('PaymentOrchestrator: Using fallback provider', [ + 'provider' => $provider->getName(), + ]); + return $provider; } } @@ -625,10 +653,13 @@ class PaymentOrchestrator */ protected function isProviderSupportedForPlan(PaymentProviderContract $provider, Plan $plan): bool { - // Check if plan has provider-specific configuration - $providerConfig = $plan->details['providers'][$provider->getName()] ?? null; + // Use the same approach as Plan::supportsProvider() - check database relationship + $isSupported = $plan->planProviders() + ->where('provider', $provider->getName()) + ->where('is_enabled', true) + ->exists(); - if (! $providerConfig || ! ($providerConfig['enabled'] ?? false)) { + if (! $isSupported) { return false; } diff --git a/app/Services/Payments/ProviderRegistry.php b/app/Services/Payments/ProviderRegistry.php index b955761..baff287 100644 --- a/app/Services/Payments/ProviderRegistry.php +++ b/app/Services/Payments/ProviderRegistry.php @@ -3,6 +3,7 @@ namespace App\Services\Payments; use App\Contracts\Payments\PaymentProviderContract; +use App\Models\PaymentProvider as PaymentProviderModel; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; @@ -15,8 +16,8 @@ class ProviderRegistry public function __construct() { - $this->loadConfigurations(); - $this->registerDefaultProviders(); + $this->loadConfigurationsFromDatabase(); + $this->registerProvidersFromDatabase(); } /** @@ -93,14 +94,30 @@ class ProviderRegistry */ public function updateConfiguration(string $providerName, array $config): void { - $this->configurations[$providerName] = $config; + try { + // Update database - encode as JSON string since the model expects to cast it + PaymentProviderModel::where('name', $providerName) + ->update(['configuration' => $config]); // Laravel will automatically cast array to JSON - Cache::put("payment_config_{$providerName}", $config); + // Update local cache + $this->configurations[$providerName] = array_merge( + $this->configurations[$providerName] ?? [], + ['config' => $config] + ); - Log::info('Payment provider configuration updated', [ - 'provider' => $providerName, - 'config_keys' => array_keys($config), - ]); + // Clear main cache + Cache::forget('payment_providers_config'); + + Log::info('Payment provider configuration updated', [ + 'provider' => $providerName, + 'config_keys' => array_keys($config), + ]); + } catch (\Exception $e) { + Log::error('Failed to update payment provider configuration', [ + 'provider' => $providerName, + 'error' => $e->getMessage(), + ]); + } } /** @@ -237,47 +254,80 @@ class ProviderRegistry } /** - * Load provider configurations from cache/database + * Load provider configurations from database */ - protected function loadConfigurations(): void + protected function loadConfigurationsFromDatabase(): void { - // Load from cache first - $cachedConfigs = Cache::get('payment_providers_config', []); + try { + // Load from cache first + $cachedConfigs = Cache::get('payment_providers_config', []); - if (empty($cachedConfigs)) { - // Load from database or config files - $this->configurations = config('payment.providers', []); + if (empty($cachedConfigs)) { + // Load from database + $providers = PaymentProviderModel::where('is_active', true)->get(); - // Cache for 1 hour - Cache::put('payment_providers_config', $this->configurations, 3600); - } else { - $this->configurations = $cachedConfigs; + $this->configurations = []; + foreach ($providers as $provider) { + // Configuration is already cast to array by the model + $configData = $provider->configuration ?? []; + + $this->configurations[$provider->name] = [ + 'enabled' => $provider->is_active, + 'display_name' => $provider->display_name, + 'class' => $configData['class'] ?? null, + 'config' => $configData, + 'supports_recurring' => $provider->supports_recurring, + 'supports_one_time' => $provider->supports_one_time, + 'supported_currencies' => $provider->supported_currencies ?? [], + 'fee_structure' => $provider->fee_structure ?? [], + 'priority' => $provider->priority, + 'is_fallback' => $provider->is_fallback, + ]; + } + + // Cache for 1 hour + Cache::put('payment_providers_config', $this->configurations, 3600); + } else { + $this->configurations = $cachedConfigs; + } + } catch (\Exception $e) { + Log::error('Failed to load payment provider configurations from database', [ + 'error' => $e->getMessage(), + ]); + $this->configurations = []; } } /** - * Register default providers + * Register providers from database */ - protected function registerDefaultProviders(): void + protected function registerProvidersFromDatabase(): void { - // Auto-register providers based on configuration - $enabledProviders = config('payment.enabled_providers', []); + try { + $activeProviders = PaymentProviderModel::where('is_active', true)->get(); - foreach ($enabledProviders as $providerName) { - $this->registerProviderByName($providerName); + foreach ($activeProviders as $providerModel) { + $this->registerProviderFromModel($providerModel); + } + } catch (\Exception $e) { + Log::error('Failed to register payment providers from database', [ + 'error' => $e->getMessage(), + ]); } } /** - * Register provider by name using configuration + * Register provider from database model */ - protected function registerProviderByName(string $providerName): void + protected function registerProviderFromModel(PaymentProviderModel $providerModel): void { - $providerClass = config("payment.providers.{$providerName}.class"); + // Configuration is already cast to array by the model + $configData = $providerModel->configuration ?? []; + $providerClass = $configData['class'] ?? null; if (! $providerClass || ! class_exists($providerClass)) { Log::error('Payment provider class not found', [ - 'provider' => $providerName, + 'provider' => $providerModel->name, 'class' => $providerClass, ]); @@ -285,42 +335,120 @@ class ProviderRegistry } try { - $config = $this->getConfiguration($providerName); + // Use the full configuration data for the provider + $config = $this->getConfiguration($providerModel->name); $provider = new $providerClass($config); if ($provider instanceof PaymentProviderContract) { - $this->register($providerName, $provider); + $this->register($providerModel->name, $provider); } else { Log::error('Payment provider does not implement contract', [ - 'provider' => $providerName, + 'provider' => $providerModel->name, 'class' => $providerClass, ]); } } catch (\Exception $e) { Log::error('Failed to register payment provider', [ - 'provider' => $providerName, + 'provider' => $providerModel->name, 'error' => $e->getMessage(), ]); } } + /** + * Get provider class name based on provider name from database + */ + protected function getProviderClass(string $providerName): ?string + { + $config = $this->getConfiguration($providerName); + + return $config['class'] ?? null; + } + + /** + * Get API key from database configuration + */ + protected function getApiKeyFromConfig(string $providerName): ?string + { + $config = $this->getConfiguration($providerName); + $configData = $config['config'] ?? []; + + // Try different possible API key field names + $apiKeyFields = [ + 'secret_key', + 'api_key', + 'merchant_api_key', + 'private_key', + 'key', + ]; + + foreach ($apiKeyFields as $field) { + if (isset($configData[$field]) && ! empty($configData[$field])) { + return $configData[$field]; + } + } + + return null; + } + + /** + * Get webhook secret from database configuration + */ + protected function getWebhookSecretFromConfig(string $providerName): ?string + { + $config = $this->getConfiguration($providerName); + $configData = $config['config'] ?? []; + + // Try different possible webhook secret field names + $webhookFields = [ + 'webhook_secret', + 'secret', + 'signing_secret', + ]; + + foreach ($webhookFields as $field) { + if (isset($configData[$field]) && ! empty($configData[$field])) { + return $configData[$field]; + } + } + + return null; + } + /** * Refresh provider (useful for configuration changes) */ public function refreshProvider(string $name): bool { - if (! isset($this->providers[$name])) { + try { + // Clear cache to force reload from database + Cache::forget('payment_providers_config'); + + // Unregister current instance + unset($this->providers[$name]); + + // Reload configurations from database + $this->loadConfigurationsFromDatabase(); + + // Re-register from database + $providerModel = PaymentProviderModel::where('name', $name) + ->where('is_active', true) + ->first(); + + if ($providerModel) { + $this->registerProviderFromModel($providerModel); + } + + return isset($this->providers[$name]); + } catch (\Exception $e) { + Log::error('Failed to refresh payment provider', [ + 'provider' => $name, + 'error' => $e->getMessage(), + ]); + return false; } - - // Unregister current instance - unset($this->providers[$name]); - - // Re-register with fresh configuration - $this->registerProviderByName($name); - - return isset($this->providers[$name]); } /** @@ -328,17 +456,29 @@ class ProviderRegistry */ public function toggleProvider(string $name, bool $enabled): bool { - $config = $this->getConfiguration($name); + try { + // Update database + $updated = PaymentProviderModel::where('name', $name) + ->update(['is_active' => $enabled]); + + if (! $updated) { + return false; + } + + // Clear cache to force reload + Cache::forget('payment_providers_config'); + + // Refresh the provider to apply changes + return $this->refreshProvider($name); + } catch (\Exception $e) { + Log::error('Failed to toggle payment provider', [ + 'provider' => $name, + 'enabled' => $enabled, + 'error' => $e->getMessage(), + ]); - if (empty($config)) { return false; } - - $config['enabled'] = $enabled; - $this->updateConfiguration($name, $config); - - // Refresh the provider to apply changes - return $this->refreshProvider($name); } /** @@ -346,17 +486,36 @@ class ProviderRegistry */ public function getFallbackProvider(): ?PaymentProviderContract { - $fallbackProvider = config('payment.fallback_provider'); + try { + // First try to get the provider marked as fallback in database + $fallbackProvider = PaymentProviderModel::where('is_fallback', true) + ->where('is_active', true) + ->first(); - if ($fallbackProvider && $this->has($fallbackProvider)) { - $provider = $this->get($fallbackProvider); - - if ($provider && $provider->isActive()) { - return $provider; + if ($fallbackProvider && $this->has($fallbackProvider->name)) { + $provider = $this->get($fallbackProvider->name); + if ($provider && $provider->isActive()) { + return $provider; + } } - } - // Return first active provider as fallback - return $this->getActiveProviders()->first(); + // Fallback to environment variable + $fallbackProviderName = env('PAYMENT_FALLBACK_PROVIDER', 'stripe'); + if ($this->has($fallbackProviderName)) { + $provider = $this->get($fallbackProviderName); + if ($provider && $provider->isActive()) { + return $provider; + } + } + + // Return first active provider as final fallback + return $this->getActiveProviders()->first(); + } catch (\Exception $e) { + Log::error('Failed to get fallback provider', [ + 'error' => $e->getMessage(), + ]); + + return $this->getActiveProviders()->first(); + } } } diff --git a/app/Services/Payments/Providers/PolarProvider.php b/app/Services/Payments/Providers/PolarProvider.php index 4bd9523..2b8776e 100644 --- a/app/Services/Payments/Providers/PolarProvider.php +++ b/app/Services/Payments/Providers/PolarProvider.php @@ -3,6 +3,7 @@ namespace App\Services\Payments\Providers; use App\Contracts\Payments\PaymentProviderContract; +use App\Models\PaymentProvider as PaymentProviderModel; use App\Models\Plan; use App\Models\Subscription; use App\Models\User; @@ -15,38 +16,97 @@ class PolarProvider implements PaymentProviderContract { protected array $config; - /** - * Rate limiting: 300 requests per minute for Polar API - */ - private const RATE_LIMIT_REQUESTS = 300; + protected bool $sandbox; - private const RATE_LIMIT_WINDOW = 60; // seconds + protected string $apiBaseUrl; - private static array $requestTimes = []; + protected string $apiKey; + + protected string $webhookSecret; + + protected string $accessToken; + + // Force reload - timestamp: 2025-12-04-17-15-00 public function __construct(array $config = []) { - $isSandbox = $config['sandbox'] ?? config('services.polar.sandbox', false); + // DEBUG: Log that our new PolarProvider is being loaded + Log::info('NEW PolarProvider constructor called - timestamp: 2025-12-04-17-15-00'); + + // ALWAYS load configuration from PaymentProvider model to get the latest data + $dbConfig = $this->loadConfigurationFromModel(); + + // Merge with any passed config (passed config takes precedence) + $config = array_merge($dbConfig, $config); + + Log::info('PolarProvider configuration loaded', [ + 'config_keys' => array_keys($config), + 'has_api_key' => isset($config['api_key']) && ! empty($config['api_key']), + 'has_webhook_secret' => isset($config['webhook_secret']) && ! empty($config['webhook_secret']), + 'has_sandbox_api_key' => isset($config['sandbox_api_key']) && ! empty($config['sandbox_api_key']), + 'has_sandbox_webhook_secret' => isset($config['sandbox_webhook_secret']) && ! empty($config['sandbox_webhook_secret']), + 'has_access_token' => isset($config['access_token']) && ! empty($config['access_token']), + 'sandbox' => $config['sandbox'] ?? false, + ]); + + $this->sandbox = $config['sandbox'] ?? false; + $this->apiBaseUrl = $this->sandbox + ? 'https://sandbox-api.polar.sh/v1' + : 'https://api.polar.sh/v1'; + + // Use sandbox credentials when sandbox mode is enabled + if ($this->sandbox) { + $this->apiKey = $config['sandbox_api_key'] ?? ''; + $this->webhookSecret = $config['sandbox_webhook_secret'] ?? ''; + } else { + $this->apiKey = $config['api_key'] ?? ''; + $this->webhookSecret = $config['webhook_secret'] ?? ''; + } + + // Access token is common for both environments + $this->accessToken = $config['access_token'] ?? ''; + + Log::info('PolarProvider properties set', [ + 'sandbox' => $this->sandbox, + 'api_key_empty' => empty($this->apiKey), + 'webhook_secret_empty' => empty($this->webhookSecret), + 'access_token_empty' => empty($this->accessToken), + 'using_sandbox_creds' => $this->sandbox, + 'is_active_will_be' => ! empty($this->apiKey) && ! empty($this->webhookSecret), + ]); $this->config = array_merge([ - 'sandbox' => $isSandbox, - 'api_key' => $isSandbox - ? config('services.polar.sandbox_api_key') - : config('services.polar.api_key'), - 'webhook_secret' => $isSandbox - ? config('services.polar.sandbox_webhook_secret') - : config('services.polar.webhook_secret'), + 'sandbox' => $this->sandbox, + 'api_key' => $this->apiKey, + 'webhook_secret' => $this->webhookSecret, 'success_url' => route('payment.success'), 'cancel_url' => route('payment.cancel'), 'webhook_url' => route('webhook.payment', 'polar'), ], $config); } - protected function getApiBaseUrl(): string + protected function loadConfigurationFromModel(): array { - return $this->config['sandbox'] - ? 'https://sandbox-api.polar.sh/v1' - : 'https://api.polar.sh/v1'; + try { + $providerModel = PaymentProviderModel::where('name', 'polar') + ->where('is_active', true) + ->first(); + + if (! $providerModel) { + Log::error('Polar provider not found in database or not active'); + + return []; + } + + // The configuration is automatically decrypted by the encrypted:array cast + return $providerModel->configuration ?? []; + } catch (\Exception $e) { + Log::error('Failed to load Polar configuration from model', [ + 'error' => $e->getMessage(), + ]); + + return []; + } } public function getName(): string @@ -56,12 +116,9 @@ class PolarProvider implements PaymentProviderContract public function isActive(): bool { - return ! empty($this->config['api_key']) && ! empty($this->config['webhook_secret']); + return ! empty($this->apiKey) && ! empty($this->webhookSecret); } - /** - * Check if the provided API key is valid by making a test API call - */ public function validateCredentials(): bool { try { @@ -75,64 +132,60 @@ class PolarProvider implements PaymentProviderContract } } - /** - * Make authenticated API request with rate limiting - */ protected function makeAuthenticatedRequest(string $method, string $endpoint, array $data = []): \Illuminate\Http\Client\Response { - $this->checkRateLimit(); - - $url = $this->getApiBaseUrl().$endpoint; + $url = $this->apiBaseUrl.$endpoint; $headers = [ - 'Authorization' => 'Bearer '.$this->config['api_key'], + 'Authorization' => 'Bearer '.$this->apiKey, 'Content-Type' => 'application/json', 'Accept' => 'application/json', ]; + $http = Http::withHeaders($headers) + ->timeout(30) + ->withOptions([ + 'verify' => true, + 'curl' => [ + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2, + ], + ]); + return match ($method) { - 'GET' => Http::withHeaders($headers)->get($url, $data), - 'POST' => Http::withHeaders($headers)->post($url, $data), - 'PATCH' => Http::withHeaders($headers)->patch($url, $data), - 'DELETE' => Http::withHeaders($headers)->delete($url, $data), + 'GET' => $http->get($url, $data), + 'POST' => $http->post($url, $data), + 'PATCH' => $http->patch($url, $data), + 'DELETE' => $http->delete($url, $data), default => throw new \InvalidArgumentException("Unsupported HTTP method: {$method}"), }; } - /** - * Simple rate limiting implementation - */ - private function checkRateLimit(): void - { - $now = time(); - $windowStart = $now - self::RATE_LIMIT_WINDOW; - - // Clean old requests outside the current window - self::$requestTimes = array_filter(self::$requestTimes, fn ($time) => $time > $windowStart); - - // Check if we're at the rate limit - if (count(self::$requestTimes) >= self::RATE_LIMIT_REQUESTS) { - $sleepTime = self::RATE_LIMIT_WINDOW - ($now - (self::$requestTimes[0] ?? $now)); - if ($sleepTime > 0) { - Log::warning('Polar API rate limit reached, sleeping for '.$sleepTime.' seconds'); - sleep($sleepTime); - } - } - - // Record this request - self::$requestTimes[] = $now; - } - public function createSubscription(User $user, Plan $plan, array $options = []): array { try { + Log::info('PolarProvider: createSubscription started', [ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'plan_name' => $plan->name, + ]); + // Get or create Polar customer $customer = $this->getOrCreateCustomer($user); + Log::info('PolarProvider: Customer retrieved/created', [ + 'customer_id' => $customer['id'] ?? 'null', + ]); + // Get or create Polar product/price $priceId = $this->getOrCreatePrice($plan); - // Create checkout session with Polar's correct structure + Log::info('PolarProvider: Price retrieved/created', [ + 'price_id' => $priceId ?? 'null', + ]); + + // Create checkout session $checkoutData = [ 'product_price_id' => $priceId, 'customer_id' => $customer['id'], @@ -144,7 +197,7 @@ class PolarProvider implements PaymentProviderContract 'user_id' => (string) $user->id, 'plan_id' => (string) $plan->id, 'plan_name' => $plan->name, - 'external_id' => $user->id, // Polar supports external_id for user mapping + 'external_id' => (string) $user->id, ], ]; @@ -153,36 +206,75 @@ class PolarProvider implements PaymentProviderContract $checkoutData['discount_code'] = $options['discount_code']; } + Log::info('PolarProvider: Creating checkout session', [ + 'checkout_data' => $checkoutData, + 'api_url' => $this->apiBaseUrl.'/checkouts', + ]); + $response = $this->makeAuthenticatedRequest('POST', '/checkouts', $checkoutData); + Log::info('PolarProvider: Checkout response received', [ + 'status' => $response->status(), + 'successful' => $response->successful(), + 'response_body' => $response->body(), + ]); + if (! $response->successful()) { - Log::error('Polar checkout creation failed: '.$response->body()); + $errorMessage = 'Polar checkout creation failed: '.$response->body(); + Log::error($errorMessage); + throw new \Exception($errorMessage); } $checkout = $response->json(); + if (! isset($checkout['id'])) { + throw new \Exception('Invalid response from Polar API: missing checkout ID'); + } + + if (! isset($checkout['url'])) { + throw new \Exception('Invalid response from Polar API: missing checkout URL'); + } + // Create subscription record - $subscription = Subscription::create([ + Log::info('PolarProvider: Creating subscription record', [ + 'checkout_id' => $checkout['id'], 'user_id' => $user->id, 'plan_id' => $plan->id, - 'type' => 'recurring', - 'stripe_id' => $checkout['id'], // Using stripe_id field for Polar checkout ID - 'stripe_status' => 'pending', - 'provider' => $this->getName(), - 'provider_subscription_id' => $checkout['id'], - 'status' => 'pending_payment', - 'starts_at' => null, - 'ends_at' => null, - 'provider_data' => [ - 'checkout_id' => $checkout['id'], - 'checkout_url' => $checkout['url'], - 'customer_id' => $customer['id'], - 'price_id' => $priceId, - 'created_at' => now()->toISOString(), - ], ]); - return [ + try { + $subscription = Subscription::create([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'type' => 'recurring', + 'stripe_id' => $checkout['id'], // Using stripe_id field for Polar checkout ID + 'stripe_status' => 'pending', + 'provider' => $this->getName(), + 'provider_subscription_id' => $checkout['id'], + 'status' => 'pending_payment', + 'starts_at' => null, + 'ends_at' => null, + 'provider_data' => [ + 'checkout_id' => $checkout['id'], + 'checkout_url' => $checkout['url'], + 'customer_id' => $customer['id'], + 'price_id' => $priceId, + 'created_at' => now()->toISOString(), + ], + ]); + + Log::info('PolarProvider: Subscription record created successfully', [ + 'subscription_id' => $subscription->id, + ]); + } catch (\Exception $e) { + Log::error('PolarProvider: Failed to create subscription record', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + throw $e; + } + + $result = [ 'provider_subscription_id' => $checkout['id'], 'status' => 'pending_payment', 'checkout_url' => $checkout['url'], @@ -192,6 +284,14 @@ class PolarProvider implements PaymentProviderContract 'expires_at' => $checkout['expires_at'] ?? now()->addHours(24)->toISOString(), ]; + Log::info('PolarProvider: Returning successful result', [ + 'result_keys' => array_keys($result), + 'checkout_url' => $result['checkout_url'], + 'provider_subscription_id' => $result['provider_subscription_id'], + ]); + + return $result; + } catch (\Exception $e) { Log::error('Polar subscription creation failed', [ 'user_id' => $user->id, @@ -250,14 +350,14 @@ class PolarProvider implements PaymentProviderContract $polarSubscriptionId = $this->getPolarSubscriptionId($subscription); if (! $polarSubscriptionId) { - Log::error('No Polar subscription found to update'); + throw new \Exception('No Polar subscription found to update'); } $newPriceId = $this->getOrCreatePrice($newPlan); $response = $this->makeAuthenticatedRequest('PATCH', '/subscriptions/'.$polarSubscriptionId, [ 'product_price_id' => $newPriceId, - 'preserve_period' => true, // Polar equivalent of proration behavior + 'proration_behavior' => 'prorate', ]); if (! $response->successful()) { @@ -390,6 +490,13 @@ class PolarProvider implements PaymentProviderContract public function createCheckoutSession(User $user, Plan $plan, array $options = []): array { + Log::info('PolarProvider: createCheckoutSession called', [ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'plan_name' => $plan->name, + 'options_count' => count($options), + ]); + return $this->createSubscription($user, $plan, $options); } @@ -431,6 +538,7 @@ class PolarProvider implements PaymentProviderContract if (! $this->validateWebhook($request)) { Log::error('Invalid Polar webhook signature'); + throw new \Exception('Invalid webhook signature'); } $webhookData = json_decode($payload, true); @@ -467,9 +575,6 @@ class PolarProvider implements PaymentProviderContract case 'customer.state_changed': $result = $this->handleCustomerStateChanged($webhookData); break; - case 'benefit_grant.created': - $result = $this->handleBenefitGrantCreated($webhookData); - break; default: Log::info('Unhandled Polar webhook event', ['event_type' => $eventType]); } @@ -491,11 +596,11 @@ class PolarProvider implements PaymentProviderContract $signature = $request->header('Polar-Signature'); $payload = $request->getContent(); - if (! $signature || ! $this->config['webhook_secret']) { + if (! $signature || ! $this->webhookSecret) { return false; } - $expectedSignature = hash_hmac('sha256', $payload, $this->config['webhook_secret']); + $expectedSignature = hash_hmac('sha256', $payload, $this->webhookSecret); return hash_equals($signature, $expectedSignature); @@ -538,18 +643,16 @@ class PolarProvider implements PaymentProviderContract { try { // Polar handles refunds through their dashboard or API - // For now, we'll return a NotImplementedError Log::error('Polar refunds must be processed through Polar dashboard or API directly'); - todo('Write process refund process'); + throw new \Exception('Refund processing not implemented for Polar'); } catch (\Exception $e) { Log::error('Polar refund processing failed', [ 'payment_id' => $paymentId, 'amount' => $amount, 'error' => $e->getMessage(), ]); + throw $e; } - - return []; } public function getTransactionHistory(User $user, array $filters = []): array @@ -570,29 +673,51 @@ class PolarProvider implements PaymentProviderContract $params['end_date'] = $filters['end_date']; } - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - ])->get($this->getApiBaseUrl().'/v1/subscriptions', $params); + $response = $this->makeAuthenticatedRequest('GET', '/orders', $params); if (! $response->successful()) { Log::error('Failed to retrieve Polar transaction history: '.$response->body()); } - $polarSubscriptions = $response->json(); + $polarOrders = $response->json(); $transactions = []; - foreach ($polarSubscriptions['data'] ?? [] as $subscription) { + foreach ($polarOrders['items'] ?? [] as $order) { $transactions[] = [ - 'id' => $subscription['id'], - 'status' => $subscription['status'], - 'amount' => $subscription['amount'] ?? 0, - 'currency' => $subscription['currency'] ?? 'USD', - 'created_at' => $subscription['created_at'], - 'current_period_start' => $subscription['current_period_start'], - 'current_period_end' => $subscription['current_period_end'], + 'id' => $order['id'], + 'status' => $order['status'], + 'amount' => $order['amount'] ?? 0, + 'currency' => $order['currency'] ?? 'USD', + 'created_at' => $order['created_at'], + 'type' => 'order', ]; } + // Also get subscriptions + $subscriptionResponse = $this->makeAuthenticatedRequest('GET', '/subscriptions', $params); + + if ($subscriptionResponse->successful()) { + $polarSubscriptions = $subscriptionResponse->json(); + + foreach ($polarSubscriptions['items'] ?? [] as $subscription) { + $transactions[] = [ + 'id' => $subscription['id'], + 'status' => $subscription['status'], + 'amount' => $subscription['amount'] ?? 0, + 'currency' => $subscription['currency'] ?? 'USD', + 'created_at' => $subscription['created_at'], + 'type' => 'subscription', + 'current_period_start' => $subscription['current_period_start'], + 'current_period_end' => $subscription['current_period_end'], + ]; + } + } + + // Sort by date descending + usort($transactions, function ($a, $b) { + return strtotime($b['created_at']) - strtotime($a['created_at']); + }); + return $transactions; } catch (\Exception $e) { @@ -637,118 +762,231 @@ class PolarProvider implements PaymentProviderContract // Helper methods protected function getOrCreateCustomer(User $user): array { - // First, try to find existing customer by email and external_id - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - ])->get($this->getApiBaseUrl().'/customers', [ - 'email' => $user->email, - 'external_id' => $user->id, // Use external_id for better customer matching - ]); + // First, try to find existing customer by external_id + try { + $response = $this->makeAuthenticatedRequest('GET', '/customers', [ + 'external_id' => (string) $user->id, + 'limit' => 1, + ]); - if ($response->successful() && ! empty($response->json()['data'])) { - return $response->json()['data'][0]; + if ($response->successful()) { + $data = $response->json(); + if (! empty($data['items'])) { + Log::info('Found existing Polar customer by external_id', [ + 'user_id' => $user->id, + 'customer_id' => $data['items'][0]['id'], + ]); + + return $data['items'][0]; + } + } + } catch (\Exception $e) { + Log::info('Customer not found by external_id, will create new one', [ + 'user_id' => $user->id, + ]); + } + + // Try to find by email as fallback + try { + $response = $this->makeAuthenticatedRequest('GET', '/customers', [ + 'email' => $user->email, + 'limit' => 1, + ]); + + if ($response->successful()) { + $data = $response->json(); + if (! empty($data['items'])) { + Log::info('Found existing Polar customer by email', [ + 'user_id' => $user->id, + 'customer_id' => $data['items'][0]['id'], + ]); + + return $data['items'][0]; + } + } + } catch (\Exception $e) { + Log::info('Customer not found by email, will create new one', [ + 'user_id' => $user->id, + ]); } // Create new customer $customerData = [ 'email' => $user->email, 'name' => $user->name, - 'external_id' => $user->id, // Polar supports external_id for user mapping + 'external_id' => (string) $user->id, 'metadata' => [ 'user_id' => (string) $user->id, 'source' => 'laravel_app', ], ]; - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - 'Content-Type' => 'application/json', - ])->post($this->getApiBaseUrl().'/customers', $customerData); + $response = $this->makeAuthenticatedRequest('POST', '/customers', $customerData); if (! $response->successful()) { - Log::error('Failed to create Polar customer: '.$response->body()); + $errorBody = $response->json(); + + // Check if customer already exists + if (isset($errorBody['detail']) && is_array($errorBody['detail'])) { + foreach ($errorBody['detail'] as $error) { + if (isset($error['msg']) && ( + str_contains($error['msg'], 'already exists') || + str_contains($error['msg'], 'email address already exists') || + str_contains($error['msg'], 'external ID already exists') + )) { + // Customer already exists, try to find again + Log::warning('Polar customer already exists, attempting to find again', [ + 'user_id' => $user->id, + 'email' => $user->email, + ]); + + return $this->findExistingCustomer($user); + } + } + } + + $errorMessage = 'Failed to create Polar customer: '.$response->body(); + Log::error($errorMessage); + throw new \Exception($errorMessage); } - return $response->json(); + $customer = $response->json(); + + if (! isset($customer['id'])) { + throw new \Exception('Invalid response from Polar API: missing customer ID'); + } + + Log::info('Created new Polar customer', [ + 'user_id' => $user->id, + 'customer_id' => $customer['id'], + ]); + + return $customer; + } + + protected function findExistingCustomer(User $user): array + { + // Try multiple approaches to find the customer + $attempts = [ + fn () => $this->makeAuthenticatedRequest('GET', '/customers', ['external_id' => (string) $user->id, 'limit' => 100]), + fn () => $this->makeAuthenticatedRequest('GET', '/customers', ['email' => $user->email, 'limit' => 100]), + fn () => $this->makeAuthenticatedRequest('GET', '/customers', ['limit' => 1000]), + ]; + + foreach ($attempts as $attempt) { + try { + $response = $attempt(); + if ($response->successful()) { + $data = $response->json(); + foreach ($data['items'] ?? [] as $customer) { + if (($customer['email'] && strtolower($customer['email']) === strtolower($user->email)) || + ($customer['external_id'] && (string) $customer['external_id'] === (string) $user->id)) { + Log::info('Found existing Polar customer', [ + 'user_id' => $user->id, + 'customer_id' => $customer['id'], + ]); + + return $customer; + } + } + } + } catch (\Exception $e) { + Log::warning('Customer lookup attempt failed', [ + 'user_id' => $user->id, + 'error' => $e->getMessage(), + ]); + } + } + + throw new \Exception('Customer exists in Polar but could not be retrieved after multiple lookup attempts'); } protected function getOrCreatePrice(Plan $plan): string { - // Look for existing price by plan metadata - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - ])->get($this->getApiBaseUrl().'/v1/products', [ - 'metadata[plan_id]' => $plan->id, - ]); - - if ($response->successful() && ! empty($response->json()['data'])) { - $product = $response->json()['data'][0]; - - // Get the price for this product - $priceResponse = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - ])->get($this->getApiBaseUrl().'/v1/prices', [ - 'product_id' => $product['id'], - 'recurring_interval' => 'month', + // Look for existing product by plan metadata + try { + $response = $this->makeAuthenticatedRequest('GET', '/products', [ + 'metadata[plan_id]' => $plan->id, + 'limit' => 1, ]); - if ($priceResponse->successful() && ! empty($priceResponse->json()['data'])) { - return $priceResponse->json()['data'][0]['id']; + if ($response->successful()) { + $data = $response->json(); + if (! empty($data['items'])) { + $product = $data['items'][0]; + + // Return the first price ID from the product + if (! empty($product['prices'])) { + return $product['prices'][0]['id']; + } + } } + } catch (\Exception $e) { + Log::info('No existing product found, will create new one', [ + 'plan_id' => $plan->id, + ]); } - // Create new product and price + // Create new product with correct structure $productData = [ 'name' => $plan->name, 'description' => $plan->description ?? 'Subscription plan', - 'type' => 'service', + 'recurring_interval' => 'month', + 'recurring_interval_count' => 1, + 'prices' => [ + [ + 'amount_type' => 'fixed', + 'price_amount' => (int) ($plan->price * 100), // Convert to cents + 'price_currency' => 'usd', + 'recurring_interval' => 'month', + 'recurring_interval_count' => 1, + ], + ], 'metadata' => [ 'plan_id' => $plan->id, 'plan_name' => $plan->name, ], ]; - $productResponse = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - 'Content-Type' => 'application/json', - ])->post($this->getApiBaseUrl().'/v1/products', $productData); + Log::info('Creating Polar product with data', [ + 'product_data' => $productData, + ]); - if (! $productResponse->successful()) { - Log::error('Failed to create Polar product: '.$productResponse->body()); + $response = $this->makeAuthenticatedRequest('POST', '/products', $productData); + + if (! $response->successful()) { + $errorMessage = 'Failed to create Polar product: '.$response->body(); + Log::error($errorMessage); + throw new \Exception($errorMessage); } - $product = $productResponse->json(); + $product = $response->json(); - // Create price for the product - $priceData = [ + if (! isset($product['id'])) { + throw new \Exception('Invalid response from Polar API: missing product ID'); + } + + // Polar returns the price ID in the prices array of the product + if (! isset($product['prices'][0]['id'])) { + throw new \Exception('Invalid response from Polar API: missing price ID in product'); + } + + Log::info('Successfully created Polar product', [ 'product_id' => $product['id'], - 'amount' => (int) ($plan->price * 100), // Convert to cents - 'currency' => 'usd', - 'recurring' => [ - 'interval' => 'month', - 'interval_count' => 1, - ], - ]; + 'price_id' => $product['prices'][0]['id'], + ]); - $priceResponse = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - 'Content-Type' => 'application/json', - ])->post($this->getApiBaseUrl().'/v1/prices', $priceData); - - if (! $priceResponse->successful()) { - Log::error('Failed to create Polar price: '.$priceResponse->body()); - } - - $price = $priceResponse->json(); - - return $price['id']; + return $product['prices'][0]['id']; } protected function getPolarSubscriptionId(Subscription $subscription): ?string { $providerData = $subscription->provider_data ?? []; - return $providerData['polar_subscription']['id'] ?? null; + // Try different locations where the subscription ID might be stored + return $providerData['polar_subscription']['id'] ?? + $providerData['subscription_id'] ?? + $subscription->provider_subscription_id; } // Webhook handlers @@ -847,85 +1085,6 @@ class PolarProvider implements PaymentProviderContract ]; } - protected function handleSubscriptionActive(array $webhookData): array - { - $polarSubscription = $webhookData['data']['object']; - - Subscription::where('provider', 'polar') - ->where('provider_subscription_id', $polarSubscription['id']) - ->update([ - 'status' => 'active', - 'starts_at' => Carbon::parse($polarSubscription['current_period_start']), - 'ends_at' => Carbon::parse($polarSubscription['current_period_end']), - 'provider_data' => array_merge( - Subscription::where('provider', 'polar') - ->where('provider_subscription_id', $polarSubscription['id']) - ->first()?->provider_data ?? [], - [ - 'polar_subscription' => $polarSubscription, - 'activated_at' => now()->toISOString(), - ] - ), - ]); - - return [ - 'event_type' => 'subscription.active', - 'processed' => true, - 'data' => [ - 'subscription_id' => $polarSubscription['id'], - 'status' => 'active', - ], - ]; - } - - protected function handleCustomerStateChanged(array $webhookData): array - { - $customer = $webhookData['data']['object']; - - // Update all subscriptions for this customer - Subscription::whereHas('user', function ($query) use ($customer) { - $query->where('email', $customer['email']); - })->where('provider', 'polar')->get()->each(function ($subscription) use ($customer) { - $subscription->update([ - 'provider_data' => array_merge($subscription->provider_data ?? [], [ - 'customer_state' => $customer['state'], - 'customer_updated_at' => now()->toISOString(), - ]), - ]); - }); - - return [ - 'event_type' => 'customer.state_changed', - 'processed' => true, - 'data' => [ - 'customer_id' => $customer['id'], - 'state' => $customer['state'], - ], - ]; - } - - protected function handleBenefitGrantCreated(array $webhookData): array - { - $benefitGrant = $webhookData['data']['object']; - - // Log benefit grants for analytics or feature access - Log::info('Polar benefit grant created', [ - 'grant_id' => $benefitGrant['id'], - 'customer_id' => $benefitGrant['customer_id'], - 'benefit_id' => $benefitGrant['benefit_id'], - ]); - - return [ - 'event_type' => 'benefit_grant.created', - 'processed' => true, - 'data' => [ - 'grant_id' => $benefitGrant['id'], - 'customer_id' => $benefitGrant['customer_id'], - 'benefit_id' => $benefitGrant['benefit_id'], - ], - ]; - } - protected function handleSubscriptionCreated(array $webhookData): array { $polarSubscription = $webhookData['data']['object']; @@ -959,6 +1118,37 @@ class PolarProvider implements PaymentProviderContract ]; } + protected function handleSubscriptionActive(array $webhookData): array + { + $polarSubscription = $webhookData['data']['object']; + + Subscription::where('provider', 'polar') + ->where('provider_subscription_id', $polarSubscription['id']) + ->update([ + 'status' => 'active', + 'starts_at' => Carbon::parse($polarSubscription['current_period_start']), + 'ends_at' => Carbon::parse($polarSubscription['current_period_end']), + 'provider_data' => array_merge( + Subscription::where('provider', 'polar') + ->where('provider_subscription_id', $polarSubscription['id']) + ->first()?->provider_data ?? [], + [ + 'polar_subscription' => $polarSubscription, + 'activated_at' => now()->toISOString(), + ] + ), + ]); + + return [ + 'event_type' => 'subscription.active', + 'processed' => true, + 'data' => [ + 'subscription_id' => $polarSubscription['id'], + 'status' => 'active', + ], + ]; + } + protected function handleSubscriptionUpdated(array $webhookData): array { $polarSubscription = $webhookData['data']['object']; @@ -1004,42 +1194,28 @@ class PolarProvider implements PaymentProviderContract ]; } - protected function handleSubscriptionPaused(array $webhookData): array + protected function handleCustomerStateChanged(array $webhookData): array { - $polarSubscription = $webhookData['data']['object']; + $customer = $webhookData['data']['object']; - Subscription::where('provider', 'polar') - ->where('provider_subscription_id', $polarSubscription['id']) - ->update([ - 'status' => 'paused', - 'paused_at' => now(), + // Update all subscriptions for this customer + Subscription::whereHas('user', function ($query) use ($customer) { + $query->where('email', $customer['email']); + })->where('provider', 'polar')->get()->each(function ($subscription) use ($customer) { + $subscription->update([ + 'provider_data' => array_merge($subscription->provider_data ?? [], [ + 'customer_state' => $customer['state'], + 'customer_updated_at' => now()->toISOString(), + ]), ]); + }); return [ - 'event_type' => 'subscription.paused', + 'event_type' => 'customer.state_changed', 'processed' => true, 'data' => [ - 'subscription_id' => $polarSubscription['id'], - ], - ]; - } - - protected function handleSubscriptionResumed(array $webhookData): array - { - $polarSubscription = $webhookData['data']['object']; - - Subscription::where('provider', 'polar') - ->where('provider_subscription_id', $polarSubscription['id']) - ->update([ - 'status' => 'active', - 'resumed_at' => now(), - ]); - - return [ - 'event_type' => 'subscription.resumed', - 'processed' => true, - 'data' => [ - 'subscription_id' => $polarSubscription['id'], + 'customer_id' => $customer['id'], + 'state' => $customer['state'], ], ]; } @@ -1079,18 +1255,14 @@ class PolarProvider implements PaymentProviderContract public function applyCoupon(Subscription $subscription, string $couponCode): array { - // Polar supports discount codes try { $polarSubscriptionId = $this->getPolarSubscriptionId($subscription); if (! $polarSubscriptionId) { - Log::error('No Polar subscription found'); + throw new \Exception('No Polar subscription found'); } - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - 'Content-Type' => 'application/json', - ])->post($this->getApiBaseUrl().'/v1/subscriptions/'.$polarSubscriptionId.'/discount', [ + $response = $this->makeAuthenticatedRequest('POST', '/subscriptions/'.$polarSubscriptionId.'/discount', [ 'coupon_code' => $couponCode, ]); @@ -1119,9 +1291,7 @@ class PolarProvider implements PaymentProviderContract return false; } - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - ])->delete($this->getApiBaseUrl().'/v1/subscriptions/'.$polarSubscriptionId.'/discount'); + $response = $this->makeAuthenticatedRequest('DELETE', '/subscriptions/'.$polarSubscriptionId.'/discount'); return $response->successful(); @@ -1148,9 +1318,7 @@ class PolarProvider implements PaymentProviderContract ]; } - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - ])->get($this->getApiBaseUrl().'/v1/subscriptions/'.$polarSubscriptionId.'/upcoming-invoice'); + $response = $this->makeAuthenticatedRequest('GET', '/subscriptions/'.$polarSubscriptionId.'/upcoming-invoice'); if (! $response->successful()) { Log::error('Failed to retrieve Polar upcoming invoice: '.$response->body()); @@ -1212,8 +1380,6 @@ class PolarProvider implements PaymentProviderContract public function importSubscriptionData(User $user, array $subscriptionData): array { Log::error('Import to Polar payments not implemented'); - todo('Write import subscription data'); - - return []; + throw new \Exception('Import subscription data not implemented for Polar'); } } diff --git a/routes/web.php b/routes/web.php index 6922447..06e6c70 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,28 @@ json([ + 'status' => 'success', + 'provider_class' => get_class($provider), + 'is_active' => $provider->isActive(), + 'config' => $provider->getConfiguration(), + 'sandbox' => $provider->config['sandbox'] ?? 'unknown', + 'timestamp' => '2025-12-04-17-15-00', + ]); + } catch (\Exception $e) { + return response()->json([ + 'status' => 'error', + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + } +}); use App\Http\Controllers\ImpersonationController; use App\Http\Controllers\WebhookController; use App\Http\Middleware\CheckPageSlug; diff --git a/storage/framework/cache/data/.gitignore b/storage/framework/cache/data/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/storage/framework/cache/data/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore