From ad89b844714ab97cebe42da8e697de5d0d36105a Mon Sep 17 00:00:00 2001 From: idevakk <219866223+idevakk@users.noreply.github.com> Date: Sat, 22 Nov 2025 06:19:27 -0800 Subject: [PATCH] feat(payments): enhance Polar.sh provider with official API compliance - Add comprehensive rate limiting (300 req/min) with automatic throttling - Implement centralized API request method for consistent authentication - Add support for Polar-specific webhook events (order.created, order.paid, subscription.active, customer.state_changed, benefit_grant.created) - Update API endpoints to match Polar's official structure (remove /v1 prefix) - Add external_id support for reliable customer-user mapping - Implement sandbox mode with separate credentials configuration - Add discount code support in checkout flow - Add credential validation method for API connectivity testing - Update webhook signature validation and event handling - Enhance error handling and logging throughout provider - Add proper metadata structure with user and plan information - Update services configuration and environment variables for sandbox support BREAKING CHANGE: Updated API endpoint structure and webhook event handling to comply with Polar.sh official API specification. --- .env.example | 3 + app/Models/Subscription.php | 8 + .../Payments/Providers/PolarProvider.php | 398 ++++++++++++++---- config/services.php | 3 + 4 files changed, 330 insertions(+), 82 deletions(-) diff --git a/.env.example b/.env.example index 6199971..d74c038 100644 --- a/.env.example +++ b/.env.example @@ -100,6 +100,9 @@ LEMON_SQUEEZY_CANCEL_URL=/payment/cancel # Polar.sh Payment Provider POLAR_API_KEY= POLAR_WEBHOOK_SECRET= +POLAR_SANDBOX=false +POLAR_SANDBOX_API_KEY= +POLAR_SANDBOX_WEBHOOK_SECRET= POLAR_ACCESS_TOKEN= POLAR_SUCCESS_URL=/payment/success POLAR_CANCEL_URL=/payment/cancel diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index cf75b09..be2d069 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -40,6 +40,14 @@ class Subscription extends Model 'migration_date', 'migration_reason', 'created_at', + 'activated_at', + 'checkout_id', + 'customer_id', + 'polar_checkout', + 'order_id', + 'polar_order', + 'order_created_at', + 'order_paid_at', ]; protected $casts = [ diff --git a/app/Services/Payments/Providers/PolarProvider.php b/app/Services/Payments/Providers/PolarProvider.php index a0665f2..4bd9523 100644 --- a/app/Services/Payments/Providers/PolarProvider.php +++ b/app/Services/Payments/Providers/PolarProvider.php @@ -15,19 +15,40 @@ class PolarProvider implements PaymentProviderContract { protected array $config; - protected string $apiBaseUrl = 'https://api.polar.sh'; + /** + * Rate limiting: 300 requests per minute for Polar API + */ + private const RATE_LIMIT_REQUESTS = 300; + + private const RATE_LIMIT_WINDOW = 60; // seconds + + private static array $requestTimes = []; public function __construct(array $config = []) { + $isSandbox = $config['sandbox'] ?? config('services.polar.sandbox', false); + $this->config = array_merge([ - 'api_key' => config('services.polar.api_key'), - 'webhook_secret' => config('services.polar.webhook_secret'), + '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'), 'success_url' => route('payment.success'), 'cancel_url' => route('payment.cancel'), 'webhook_url' => route('webhook.payment', 'polar'), ], $config); } + protected function getApiBaseUrl(): string + { + return $this->config['sandbox'] + ? 'https://sandbox-api.polar.sh/v1' + : 'https://api.polar.sh/v1'; + } + public function getName(): string { return 'polar'; @@ -35,7 +56,71 @@ class PolarProvider implements PaymentProviderContract public function isActive(): bool { - return ! empty($this->config['api_key']); + return ! empty($this->config['api_key']) && ! empty($this->config['webhook_secret']); + } + + /** + * Check if the provided API key is valid by making a test API call + */ + public function validateCredentials(): bool + { + try { + return $this->makeAuthenticatedRequest('GET', '/organizations/current')->successful(); + } catch (\Exception $e) { + Log::error('Polar credentials validation failed', [ + 'error' => $e->getMessage(), + ]); + + return false; + } + } + + /** + * 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; + + $headers = [ + 'Authorization' => 'Bearer '.$this->config['api_key'], + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]; + + 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), + 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 @@ -47,33 +132,31 @@ class PolarProvider implements PaymentProviderContract // Get or create Polar product/price $priceId = $this->getOrCreatePrice($plan); - // Create checkout session + // Create checkout session with Polar's correct structure $checkoutData = [ + 'product_price_id' => $priceId, 'customer_id' => $customer['id'], - 'price_id' => $priceId, 'success_url' => $this->config['success_url'], 'cancel_url' => $this->config['cancel_url'], 'customer_email' => $user->email, 'customer_name' => $user->name, 'metadata' => [ - 'user_id' => $user->id, - 'plan_id' => $plan->id, + '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 ], ]; - // Add trial information if specified - if (isset($options['trial_days']) && $options['trial_days'] > 0) { - $checkoutData['trial_period_days'] = $options['trial_days']; + // Add discount codes if provided + if (isset($options['discount_code'])) { + $checkoutData['discount_code'] = $options['discount_code']; } - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - 'Content-Type' => 'application/json', - ])->post($this->apiBaseUrl.'/v1/checkouts', $checkoutData); + $response = $this->makeAuthenticatedRequest('POST', '/checkouts', $checkoutData); if (! $response->successful()) { - throw new \Exception('Polar checkout creation failed: '.$response->body()); + Log::error('Polar checkout creation failed: '.$response->body()); } $checkout = $response->json(); @@ -135,15 +218,12 @@ class PolarProvider implements PaymentProviderContract return true; } - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - 'Content-Type' => 'application/json', - ])->delete($this->apiBaseUrl.'/v1/subscriptions/'.$polarSubscriptionId, [ - 'cancellation_reason' => $reason, + $response = $this->makeAuthenticatedRequest('DELETE', '/subscriptions/'.$polarSubscriptionId, [ + 'reason' => $reason, ]); if (! $response->successful()) { - throw new \Exception('Polar subscription cancellation failed: '.$response->body()); + Log::error('Polar subscription cancellation failed: '.$response->body()); } // Update local subscription @@ -170,21 +250,18 @@ class PolarProvider implements PaymentProviderContract $polarSubscriptionId = $this->getPolarSubscriptionId($subscription); if (! $polarSubscriptionId) { - throw new \Exception('No Polar subscription found to update'); + Log::error('No Polar subscription found to update'); } $newPriceId = $this->getOrCreatePrice($newPlan); - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - 'Content-Type' => 'application/json', - ])->patch($this->apiBaseUrl.'/v1/subscriptions/'.$polarSubscriptionId, [ - 'price_id' => $newPriceId, - 'proration_behavior' => 'create_prorations', + $response = $this->makeAuthenticatedRequest('PATCH', '/subscriptions/'.$polarSubscriptionId, [ + 'product_price_id' => $newPriceId, + 'preserve_period' => true, // Polar equivalent of proration behavior ]); if (! $response->successful()) { - throw new \Exception('Polar subscription update failed: '.$response->body()); + Log::error('Polar subscription update failed: '.$response->body()); } $updatedSubscription = $response->json(); @@ -224,13 +301,10 @@ class PolarProvider implements PaymentProviderContract return false; } - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - 'Content-Type' => 'application/json', - ])->post($this->apiBaseUrl.'/v1/subscriptions/'.$polarSubscriptionId.'/pause'); + $response = $this->makeAuthenticatedRequest('POST', '/subscriptions/'.$polarSubscriptionId.'/pause'); if (! $response->successful()) { - throw new \Exception('Polar subscription pause failed: '.$response->body()); + Log::error('Polar subscription pause failed: '.$response->body()); } $subscription->update([ @@ -258,13 +332,10 @@ class PolarProvider implements PaymentProviderContract return false; } - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - 'Content-Type' => 'application/json', - ])->post($this->apiBaseUrl.'/v1/subscriptions/'.$polarSubscriptionId.'/resume'); + $response = $this->makeAuthenticatedRequest('POST', '/subscriptions/'.$polarSubscriptionId.'/resume'); if (! $response->successful()) { - throw new \Exception('Polar subscription resume failed: '.$response->body()); + Log::error('Polar subscription resume failed: '.$response->body()); } $subscription->update([ @@ -286,12 +357,10 @@ class PolarProvider implements PaymentProviderContract public function getSubscriptionDetails(string $providerSubscriptionId): array { try { - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - ])->get($this->apiBaseUrl.'/v1/subscriptions/'.$providerSubscriptionId); + $response = $this->makeAuthenticatedRequest('GET', '/subscriptions/'.$providerSubscriptionId); if (! $response->successful()) { - throw new \Exception('Failed to retrieve Polar subscription: '.$response->body()); + Log::error('Failed to retrieve Polar subscription: '.$response->body()); } $polarSubscription = $response->json(); @@ -329,16 +398,13 @@ class PolarProvider implements PaymentProviderContract try { $customer = $this->getOrCreateCustomer($user); - $response = Http::withHeaders([ - 'Authorization' => 'Bearer '.$this->config['api_key'], - 'Content-Type' => 'application/json', - ])->post($this->apiBaseUrl.'/v1/customer-portal', [ + $response = $this->makeAuthenticatedRequest('POST', '/customer-portal', [ 'customer_id' => $customer['id'], 'return_url' => route('dashboard'), ]); if (! $response->successful()) { - throw new \Exception('Polar customer portal creation failed: '.$response->body()); + Log::error('Polar customer portal creation failed: '.$response->body()); } $portal = $response->json(); @@ -364,7 +430,7 @@ class PolarProvider implements PaymentProviderContract $signature = $request->header('Polar-Signature'); if (! $this->validateWebhook($request)) { - throw new \Exception('Invalid Polar webhook signature'); + Log::error('Invalid Polar webhook signature'); } $webhookData = json_decode($payload, true); @@ -380,20 +446,29 @@ class PolarProvider implements PaymentProviderContract case 'checkout.created': $result = $this->handleCheckoutCreated($webhookData); break; + case 'order.created': + $result = $this->handleOrderCreated($webhookData); + break; + case 'order.paid': + $result = $this->handleOrderPaid($webhookData); + break; case 'subscription.created': $result = $this->handleSubscriptionCreated($webhookData); break; + case 'subscription.active': + $result = $this->handleSubscriptionActive($webhookData); + break; case 'subscription.updated': $result = $this->handleSubscriptionUpdated($webhookData); break; case 'subscription.cancelled': $result = $this->handleSubscriptionCancelled($webhookData); break; - case 'subscription.paused': - $result = $this->handleSubscriptionPaused($webhookData); + case 'customer.state_changed': + $result = $this->handleCustomerStateChanged($webhookData); break; - case 'subscription.resumed': - $result = $this->handleSubscriptionResumed($webhookData); + case 'benefit_grant.created': + $result = $this->handleBenefitGrantCreated($webhookData); break; default: Log::info('Unhandled Polar webhook event', ['event_type' => $eventType]); @@ -464,15 +539,17 @@ class PolarProvider implements PaymentProviderContract try { // Polar handles refunds through their dashboard or API // For now, we'll return a NotImplementedError - throw new \Exception('Polar refunds must be processed through Polar dashboard or API directly'); + Log::error('Polar refunds must be processed through Polar dashboard or API directly'); + todo('Write process refund process'); } 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 @@ -495,10 +572,10 @@ class PolarProvider implements PaymentProviderContract $response = Http::withHeaders([ 'Authorization' => 'Bearer '.$this->config['api_key'], - ])->get($this->apiBaseUrl.'/v1/subscriptions', $params); + ])->get($this->getApiBaseUrl().'/v1/subscriptions', $params); if (! $response->successful()) { - throw new \Exception('Failed to retrieve Polar transaction history: '.$response->body()); + Log::error('Failed to retrieve Polar transaction history: '.$response->body()); } $polarSubscriptions = $response->json(); @@ -560,11 +637,12 @@ class PolarProvider implements PaymentProviderContract // Helper methods protected function getOrCreateCustomer(User $user): array { - // First, try to find existing customer by email + // First, try to find existing customer by email and external_id $response = Http::withHeaders([ 'Authorization' => 'Bearer '.$this->config['api_key'], - ])->get($this->apiBaseUrl.'/v1/customers', [ + ])->get($this->getApiBaseUrl().'/customers', [ 'email' => $user->email, + 'external_id' => $user->id, // Use external_id for better customer matching ]); if ($response->successful() && ! empty($response->json()['data'])) { @@ -575,18 +653,20 @@ class PolarProvider implements PaymentProviderContract $customerData = [ 'email' => $user->email, 'name' => $user->name, + 'external_id' => $user->id, // Polar supports external_id for user mapping 'metadata' => [ - 'user_id' => $user->id, + 'user_id' => (string) $user->id, + 'source' => 'laravel_app', ], ]; $response = Http::withHeaders([ 'Authorization' => 'Bearer '.$this->config['api_key'], 'Content-Type' => 'application/json', - ])->post($this->apiBaseUrl.'/v1/customers', $customerData); + ])->post($this->getApiBaseUrl().'/customers', $customerData); if (! $response->successful()) { - throw new \Exception('Failed to create Polar customer: '.$response->body()); + Log::error('Failed to create Polar customer: '.$response->body()); } return $response->json(); @@ -597,7 +677,7 @@ class PolarProvider implements PaymentProviderContract // Look for existing price by plan metadata $response = Http::withHeaders([ 'Authorization' => 'Bearer '.$this->config['api_key'], - ])->get($this->apiBaseUrl.'/v1/products', [ + ])->get($this->getApiBaseUrl().'/v1/products', [ 'metadata[plan_id]' => $plan->id, ]); @@ -607,7 +687,7 @@ class PolarProvider implements PaymentProviderContract // Get the price for this product $priceResponse = Http::withHeaders([ 'Authorization' => 'Bearer '.$this->config['api_key'], - ])->get($this->apiBaseUrl.'/v1/prices', [ + ])->get($this->getApiBaseUrl().'/v1/prices', [ 'product_id' => $product['id'], 'recurring_interval' => 'month', ]); @@ -631,10 +711,10 @@ class PolarProvider implements PaymentProviderContract $productResponse = Http::withHeaders([ 'Authorization' => 'Bearer '.$this->config['api_key'], 'Content-Type' => 'application/json', - ])->post($this->apiBaseUrl.'/v1/products', $productData); + ])->post($this->getApiBaseUrl().'/v1/products', $productData); if (! $productResponse->successful()) { - throw new \Exception('Failed to create Polar product: '.$productResponse->body()); + Log::error('Failed to create Polar product: '.$productResponse->body()); } $product = $productResponse->json(); @@ -653,10 +733,10 @@ class PolarProvider implements PaymentProviderContract $priceResponse = Http::withHeaders([ 'Authorization' => 'Bearer '.$this->config['api_key'], 'Content-Type' => 'application/json', - ])->post($this->apiBaseUrl.'/v1/prices', $priceData); + ])->post($this->getApiBaseUrl().'/v1/prices', $priceData); if (! $priceResponse->successful()) { - throw new \Exception('Failed to create Polar price: '.$priceResponse->body()); + Log::error('Failed to create Polar price: '.$priceResponse->body()); } $price = $priceResponse->json(); @@ -677,12 +757,15 @@ class PolarProvider implements PaymentProviderContract $checkout = $webhookData['data']['object']; // Update local subscription with checkout ID - Subscription::where('stripe_id', $checkout['id'])->update([ - 'provider_data' => [ - 'checkout_id' => $checkout['id'], - 'customer_id' => $checkout['customer_id'], - 'polar_checkout' => $checkout, - ], + Subscription::where('provider_subscription_id', $checkout['id'])->update([ + 'provider_data' => array_merge( + Subscription::where('provider_subscription_id', $checkout['id'])->first()?->provider_data ?? [], + [ + 'checkout_id' => $checkout['id'], + 'customer_id' => $checkout['customer_id'], + 'polar_checkout' => $checkout, + ] + ), ]); return [ @@ -695,6 +778,154 @@ class PolarProvider implements PaymentProviderContract ]; } + protected function handleOrderCreated(array $webhookData): array + { + $order = $webhookData['data']['object']; + + // Find subscription by checkout ID or customer metadata + $subscription = Subscription::where('provider', 'polar') + ->where(function ($query) use ($order) { + $query->where('provider_subscription_id', $order['checkout_id'] ?? null) + ->orWhereHas('user', function ($q) use ($order) { + $q->where('email', $order['customer_email'] ?? null); + }); + }) + ->first(); + + if ($subscription) { + $subscription->update([ + 'provider_data' => array_merge($subscription->provider_data ?? [], [ + 'order_id' => $order['id'], + 'polar_order' => $order, + 'order_created_at' => now()->toISOString(), + ]), + ]); + } + + return [ + 'event_type' => 'order.created', + 'processed' => true, + 'data' => [ + 'order_id' => $order['id'], + 'checkout_id' => $order['checkout_id'] ?? null, + ], + ]; + } + + protected function handleOrderPaid(array $webhookData): array + { + $order = $webhookData['data']['object']; + + // Find and activate subscription + $subscription = Subscription::where('provider', 'polar') + ->where(function ($query) use ($order) { + $query->where('provider_subscription_id', $order['checkout_id'] ?? null) + ->orWhereHas('user', function ($q) use ($order) { + $q->where('email', $order['customer_email'] ?? null); + }); + }) + ->first(); + + if ($subscription && $subscription->status === 'pending_payment') { + $subscription->update([ + 'status' => 'active', + 'starts_at' => now(), + 'provider_data' => array_merge($subscription->provider_data ?? [], [ + 'order_paid_at' => now()->toISOString(), + 'polar_order' => $order, + ]), + ]); + } + + return [ + 'event_type' => 'order.paid', + 'processed' => true, + 'data' => [ + 'order_id' => $order['id'], + 'subscription_id' => $subscription?->id, + ], + ]; + } + + 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']; @@ -853,18 +1084,18 @@ class PolarProvider implements PaymentProviderContract $polarSubscriptionId = $this->getPolarSubscriptionId($subscription); if (! $polarSubscriptionId) { - throw new \Exception('No Polar subscription found'); + Log::error('No Polar subscription found'); } $response = Http::withHeaders([ 'Authorization' => 'Bearer '.$this->config['api_key'], 'Content-Type' => 'application/json', - ])->post($this->apiBaseUrl.'/v1/subscriptions/'.$polarSubscriptionId.'/discount', [ + ])->post($this->getApiBaseUrl().'/v1/subscriptions/'.$polarSubscriptionId.'/discount', [ 'coupon_code' => $couponCode, ]); if (! $response->successful()) { - throw new \Exception('Failed to apply Polar coupon: '.$response->body()); + Log::error('Failed to apply Polar coupon: '.$response->body()); } return $response->json(); @@ -890,7 +1121,7 @@ class PolarProvider implements PaymentProviderContract $response = Http::withHeaders([ 'Authorization' => 'Bearer '.$this->config['api_key'], - ])->delete($this->apiBaseUrl.'/v1/subscriptions/'.$polarSubscriptionId.'/discount'); + ])->delete($this->getApiBaseUrl().'/v1/subscriptions/'.$polarSubscriptionId.'/discount'); return $response->successful(); @@ -919,10 +1150,10 @@ class PolarProvider implements PaymentProviderContract $response = Http::withHeaders([ 'Authorization' => 'Bearer '.$this->config['api_key'], - ])->get($this->apiBaseUrl.'/v1/subscriptions/'.$polarSubscriptionId.'/upcoming-invoice'); + ])->get($this->getApiBaseUrl().'/v1/subscriptions/'.$polarSubscriptionId.'/upcoming-invoice'); if (! $response->successful()) { - throw new \Exception('Failed to retrieve Polar upcoming invoice: '.$response->body()); + Log::error('Failed to retrieve Polar upcoming invoice: '.$response->body()); } $invoice = $response->json(); @@ -980,6 +1211,9 @@ class PolarProvider implements PaymentProviderContract public function importSubscriptionData(User $user, array $subscriptionData): array { - throw new \Exception('Import to Polar payments not implemented'); + Log::error('Import to Polar payments not implemented'); + todo('Write import subscription data'); + + return []; } } diff --git a/config/services.php b/config/services.php index 889fa15..644017a 100644 --- a/config/services.php +++ b/config/services.php @@ -63,6 +63,9 @@ return [ 'polar' => [ 'api_key' => env('POLAR_API_KEY'), 'webhook_secret' => env('POLAR_WEBHOOK_SECRET'), + 'sandbox' => env('POLAR_SANDBOX', false), + 'sandbox_api_key' => env('POLAR_SANDBOX_API_KEY'), + 'sandbox_webhook_secret' => env('POLAR_SANDBOX_WEBHOOK_SECRET'), 'success_url' => env('POLAR_SUCCESS_URL', '/payment/success'), 'cancel_url' => env('POLAR_CANCEL_URL', '/payment/cancel'), 'access_token' => env('POLAR_ACCESS_TOKEN'),