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'), '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'; } public function isActive(): bool { 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 { try { // Get or create Polar customer $customer = $this->getOrCreateCustomer($user); // Get or create Polar product/price $priceId = $this->getOrCreatePrice($plan); // Create checkout session with Polar's correct structure $checkoutData = [ 'product_price_id' => $priceId, 'customer_id' => $customer['id'], 'success_url' => $this->config['success_url'], 'cancel_url' => $this->config['cancel_url'], 'customer_email' => $user->email, 'customer_name' => $user->name, 'metadata' => [ '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 discount codes if provided if (isset($options['discount_code'])) { $checkoutData['discount_code'] = $options['discount_code']; } $response = $this->makeAuthenticatedRequest('POST', '/checkouts', $checkoutData); if (! $response->successful()) { Log::error('Polar checkout creation failed: '.$response->body()); } $checkout = $response->json(); // Create subscription record $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(), ], ]); return [ 'provider_subscription_id' => $checkout['id'], 'status' => 'pending_payment', 'checkout_url' => $checkout['url'], 'customer_id' => $customer['id'], 'price_id' => $priceId, 'type' => 'polar_checkout', 'expires_at' => $checkout['expires_at'] ?? now()->addHours(24)->toISOString(), ]; } catch (\Exception $e) { Log::error('Polar subscription creation failed', [ 'user_id' => $user->id, 'plan_id' => $plan->id, 'error' => $e->getMessage(), ]); throw $e; } } public function cancelSubscription(Subscription $subscription, string $reason = ''): bool { try { $polarSubscriptionId = $this->getPolarSubscriptionId($subscription); if (! $polarSubscriptionId) { // Local cancellation only $subscription->update([ 'status' => 'cancelled', 'cancelled_at' => now(), 'cancellation_reason' => $reason, ]); return true; } $response = $this->makeAuthenticatedRequest('DELETE', '/subscriptions/'.$polarSubscriptionId, [ 'reason' => $reason, ]); if (! $response->successful()) { Log::error('Polar subscription cancellation failed: '.$response->body()); } // Update local subscription $subscription->update([ 'status' => 'cancelled', 'cancelled_at' => now(), 'cancellation_reason' => $reason, ]); return true; } catch (\Exception $e) { Log::error('Polar subscription cancellation failed', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); throw $e; } } public function updateSubscription(Subscription $subscription, Plan $newPlan): array { try { $polarSubscriptionId = $this->getPolarSubscriptionId($subscription); if (! $polarSubscriptionId) { Log::error('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 ]); if (! $response->successful()) { Log::error('Polar subscription update failed: '.$response->body()); } $updatedSubscription = $response->json(); // Update local subscription $subscription->update([ 'plan_id' => $newPlan->id, 'provider_data' => array_merge($subscription->provider_data ?? [], [ 'updated_at' => now()->toISOString(), 'polar_subscription' => $updatedSubscription, ]), ]); return [ 'provider_subscription_id' => $updatedSubscription['id'], 'status' => $updatedSubscription['status'], 'price_id' => $newPriceId, 'updated_at' => $updatedSubscription['updated_at'], ]; } catch (\Exception $e) { Log::error('Polar subscription update failed', [ 'subscription_id' => $subscription->id, 'new_plan_id' => $newPlan->id, 'error' => $e->getMessage(), ]); throw $e; } } public function pauseSubscription(Subscription $subscription): bool { try { $polarSubscriptionId = $this->getPolarSubscriptionId($subscription); if (! $polarSubscriptionId) { return false; } $response = $this->makeAuthenticatedRequest('POST', '/subscriptions/'.$polarSubscriptionId.'/pause'); if (! $response->successful()) { Log::error('Polar subscription pause failed: '.$response->body()); } $subscription->update([ 'status' => 'paused', 'paused_at' => now(), ]); return true; } catch (\Exception $e) { Log::error('Polar subscription pause failed', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); throw $e; } } public function resumeSubscription(Subscription $subscription): bool { try { $polarSubscriptionId = $this->getPolarSubscriptionId($subscription); if (! $polarSubscriptionId) { return false; } $response = $this->makeAuthenticatedRequest('POST', '/subscriptions/'.$polarSubscriptionId.'/resume'); if (! $response->successful()) { Log::error('Polar subscription resume failed: '.$response->body()); } $subscription->update([ 'status' => 'active', 'resumed_at' => now(), ]); return true; } catch (\Exception $e) { Log::error('Polar subscription resume failed', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); throw $e; } } public function getSubscriptionDetails(string $providerSubscriptionId): array { try { $response = $this->makeAuthenticatedRequest('GET', '/subscriptions/'.$providerSubscriptionId); if (! $response->successful()) { Log::error('Failed to retrieve Polar subscription: '.$response->body()); } $polarSubscription = $response->json(); return [ 'id' => $polarSubscription['id'], 'status' => $polarSubscription['status'], 'customer_id' => $polarSubscription['customer_id'], 'price_id' => $polarSubscription['price_id'], 'current_period_start' => $polarSubscription['current_period_start'], 'current_period_end' => $polarSubscription['current_period_end'], 'cancel_at_period_end' => $polarSubscription['cancel_at_period_end'] ?? false, 'trial_start' => $polarSubscription['trial_start'] ?? null, 'trial_end' => $polarSubscription['trial_end'] ?? null, 'created_at' => $polarSubscription['created_at'], 'updated_at' => $polarSubscription['updated_at'], ]; } catch (\Exception $e) { Log::error('Polar subscription details retrieval failed', [ 'subscription_id' => $providerSubscriptionId, 'error' => $e->getMessage(), ]); throw $e; } } public function createCheckoutSession(User $user, Plan $plan, array $options = []): array { return $this->createSubscription($user, $plan, $options); } public function createCustomerPortalSession(User $user): array { try { $customer = $this->getOrCreateCustomer($user); $response = $this->makeAuthenticatedRequest('POST', '/customer-portal', [ 'customer_id' => $customer['id'], 'return_url' => route('dashboard'), ]); if (! $response->successful()) { Log::error('Polar customer portal creation failed: '.$response->body()); } $portal = $response->json(); return [ 'portal_url' => $portal['url'], 'customer_id' => $customer['id'], ]; } catch (\Exception $e) { Log::error('Polar customer portal creation failed', [ 'user_id' => $user->id, 'error' => $e->getMessage(), ]); throw $e; } } public function processWebhook(Request $request): array { try { $payload = $request->getContent(); $signature = $request->header('Polar-Signature'); if (! $this->validateWebhook($request)) { Log::error('Invalid Polar webhook signature'); } $webhookData = json_decode($payload, true); $eventType = $webhookData['type'] ?? 'unknown'; $result = [ 'event_type' => $eventType, 'processed' => false, 'data' => [], ]; switch ($eventType) { 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 '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]); } return $result; } catch (\Exception $e) { Log::error('Polar webhook processing failed', [ 'error' => $e->getMessage(), 'payload' => $request->getContent(), ]); throw $e; } } public function validateWebhook(Request $request): bool { try { $signature = $request->header('Polar-Signature'); $payload = $request->getContent(); if (! $signature || ! $this->config['webhook_secret']) { return false; } $expectedSignature = hash_hmac('sha256', $payload, $this->config['webhook_secret']); return hash_equals($signature, $expectedSignature); } catch (\Exception $e) { Log::warning('Polar webhook validation failed', [ 'error' => $e->getMessage(), ]); return false; } } public function getConfiguration(): array { return $this->config; } public function syncSubscriptionStatus(Subscription $subscription): array { return $this->getSubscriptionDetails($subscription->provider_subscription_id); } public function getPaymentMethodDetails(string $paymentMethodId): array { try { // Polar doesn't have separate payment method IDs like Stripe // Return subscription details instead return $this->getSubscriptionDetails($paymentMethodId); } catch (\Exception $e) { Log::error('Polar payment method details retrieval failed', [ 'payment_method_id' => $paymentMethodId, 'error' => $e->getMessage(), ]); throw $e; } } public function processRefund(string $paymentId, float $amount, string $reason = ''): array { 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'); } catch (\Exception $e) { Log::error('Polar refund processing failed', [ 'payment_id' => $paymentId, 'amount' => $amount, 'error' => $e->getMessage(), ]); } return []; } public function getTransactionHistory(User $user, array $filters = []): array { try { $customer = $this->getOrCreateCustomer($user); $params = [ 'customer_id' => $customer['id'], 'limit' => $filters['limit'] ?? 50, ]; if (isset($filters['start_date'])) { $params['start_date'] = $filters['start_date']; } if (isset($filters['end_date'])) { $params['end_date'] = $filters['end_date']; } $response = Http::withHeaders([ 'Authorization' => 'Bearer '.$this->config['api_key'], ])->get($this->getApiBaseUrl().'/v1/subscriptions', $params); if (! $response->successful()) { Log::error('Failed to retrieve Polar transaction history: '.$response->body()); } $polarSubscriptions = $response->json(); $transactions = []; foreach ($polarSubscriptions['data'] ?? [] as $subscription) { $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'], ]; } return $transactions; } catch (\Exception $e) { Log::error('Polar transaction history retrieval failed', [ 'user_id' => $user->id, 'error' => $e->getMessage(), ]); throw $e; } } public function calculateFees(float $amount): array { // Polar fees vary by plan and region (typically 5-8%) // Using 6% as default for calculation $percentageFee = $amount * 0.06; $totalFee = $percentageFee; // Polar typically doesn't have fixed fees return [ 'fixed_fee' => 0, 'percentage_fee' => $percentageFee, 'total_fee' => $totalFee, 'net_amount' => $amount - $totalFee, ]; } public function getSupportedCurrencies(): array { return ['USD']; // Polar supports USD, EUR, and other currencies, but USD is most common } public function supportsRecurring(): bool { return true; } public function supportsOneTime(): bool { return true; } // 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 ]); if ($response->successful() && ! empty($response->json()['data'])) { return $response->json()['data'][0]; } // Create new customer $customerData = [ 'email' => $user->email, 'name' => $user->name, 'external_id' => $user->id, // Polar supports external_id for user mapping '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); if (! $response->successful()) { Log::error('Failed to create Polar customer: '.$response->body()); } return $response->json(); } 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', ]); if ($priceResponse->successful() && ! empty($priceResponse->json()['data'])) { return $priceResponse->json()['data'][0]['id']; } } // Create new product and price $productData = [ 'name' => $plan->name, 'description' => $plan->description ?? 'Subscription plan', 'type' => 'service', '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); if (! $productResponse->successful()) { Log::error('Failed to create Polar product: '.$productResponse->body()); } $product = $productResponse->json(); // Create price for the product $priceData = [ 'product_id' => $product['id'], 'amount' => (int) ($plan->price * 100), // Convert to cents 'currency' => 'usd', 'recurring' => [ 'interval' => 'month', 'interval_count' => 1, ], ]; $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']; } protected function getPolarSubscriptionId(Subscription $subscription): ?string { $providerData = $subscription->provider_data ?? []; return $providerData['polar_subscription']['id'] ?? null; } // Webhook handlers protected function handleCheckoutCreated(array $webhookData): array { $checkout = $webhookData['data']['object']; // Update local subscription with checkout ID 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 [ 'event_type' => 'checkout.created', 'processed' => true, 'data' => [ 'checkout_id' => $checkout['id'], 'customer_id' => $checkout['customer_id'], ], ]; } 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']; // Find and update local subscription $localSubscription = Subscription::where('provider', 'polar') ->where('provider_subscription_id', $polarSubscription['checkout_id']) ->first(); if ($localSubscription) { $localSubscription->update([ 'stripe_id' => $polarSubscription['id'], 'provider_subscription_id' => $polarSubscription['id'], 'status' => $polarSubscription['status'], 'starts_at' => Carbon::parse($polarSubscription['current_period_start']), 'ends_at' => Carbon::parse($polarSubscription['current_period_end']), 'provider_data' => array_merge($localSubscription->provider_data ?? [], [ 'polar_subscription' => $polarSubscription, 'activated_at' => now()->toISOString(), ]), ]); } return [ 'event_type' => 'subscription.created', 'processed' => true, 'data' => [ 'subscription_id' => $polarSubscription['id'], 'status' => $polarSubscription['status'], ], ]; } protected function handleSubscriptionUpdated(array $webhookData): array { $polarSubscription = $webhookData['data']['object']; Subscription::where('provider', 'polar') ->where('provider_subscription_id', $polarSubscription['id']) ->update([ 'status' => $polarSubscription['status'], 'provider_data' => [ 'polar_subscription' => $polarSubscription, 'updated_at' => now()->toISOString(), ], ]); return [ 'event_type' => 'subscription.updated', 'processed' => true, 'data' => [ 'subscription_id' => $polarSubscription['id'], 'status' => $polarSubscription['status'], ], ]; } protected function handleSubscriptionCancelled(array $webhookData): array { $polarSubscription = $webhookData['data']['object']; Subscription::where('provider', 'polar') ->where('provider_subscription_id', $polarSubscription['id']) ->update([ 'status' => 'cancelled', 'cancelled_at' => now(), 'cancellation_reason' => 'Polar webhook cancellation', ]); return [ 'event_type' => 'subscription.cancelled', 'processed' => true, 'data' => [ 'subscription_id' => $polarSubscription['id'], ], ]; } protected function handleSubscriptionPaused(array $webhookData): array { $polarSubscription = $webhookData['data']['object']; Subscription::where('provider', 'polar') ->where('provider_subscription_id', $polarSubscription['id']) ->update([ 'status' => 'paused', 'paused_at' => now(), ]); return [ 'event_type' => 'subscription.paused', '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'], ], ]; } // Additional interface methods public function getSubscriptionMetadata(Subscription $subscription): array { return $subscription->provider_data['polar_subscription'] ?? []; } public function updateSubscriptionMetadata(Subscription $subscription, array $metadata): bool { try { $subscription->update([ 'provider_data' => array_merge($subscription->provider_data ?? [], [ 'metadata' => $metadata, ]), ]); return true; } catch (\Exception $e) { Log::error('Failed to update Polar subscription metadata', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); return false; } } public function startTrial(Subscription $subscription, int $trialDays): bool { // Polar handles trials through checkout creation // This would require creating a new checkout with trial period return false; } 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'); } $response = Http::withHeaders([ 'Authorization' => 'Bearer '.$this->config['api_key'], 'Content-Type' => 'application/json', ])->post($this->getApiBaseUrl().'/v1/subscriptions/'.$polarSubscriptionId.'/discount', [ 'coupon_code' => $couponCode, ]); if (! $response->successful()) { Log::error('Failed to apply Polar coupon: '.$response->body()); } return $response->json(); } catch (\Exception $e) { Log::error('Polar coupon application failed', [ 'subscription_id' => $subscription->id, 'coupon_code' => $couponCode, 'error' => $e->getMessage(), ]); throw $e; } } public function removeCoupon(Subscription $subscription): bool { try { $polarSubscriptionId = $this->getPolarSubscriptionId($subscription); if (! $polarSubscriptionId) { return false; } $response = Http::withHeaders([ 'Authorization' => 'Bearer '.$this->config['api_key'], ])->delete($this->getApiBaseUrl().'/v1/subscriptions/'.$polarSubscriptionId.'/discount'); return $response->successful(); } catch (\Exception $e) { Log::error('Polar coupon removal failed', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); return false; } } public function getUpcomingInvoice(Subscription $subscription): array { try { $polarSubscriptionId = $this->getPolarSubscriptionId($subscription); if (! $polarSubscriptionId) { return [ 'amount_due' => 0, 'currency' => 'USD', 'next_payment_date' => null, ]; } $response = Http::withHeaders([ 'Authorization' => 'Bearer '.$this->config['api_key'], ])->get($this->getApiBaseUrl().'/v1/subscriptions/'.$polarSubscriptionId.'/upcoming-invoice'); if (! $response->successful()) { Log::error('Failed to retrieve Polar upcoming invoice: '.$response->body()); } $invoice = $response->json(); return [ 'amount_due' => $invoice['amount_due'] / 100, // Convert from cents 'currency' => $invoice['currency'], 'next_payment_date' => $invoice['next_payment_date'], ]; } catch (\Exception $e) { Log::error('Polar upcoming invoice retrieval failed', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); throw $e; } } public function retryFailedPayment(Subscription $subscription): array { // Polar doesn't have explicit retry logic - payments are retried automatically return $this->syncSubscriptionStatus($subscription); } public function canModifySubscription(Subscription $subscription): bool { try { $details = $this->getSubscriptionDetails($subscription->provider_subscription_id); return in_array($details['status'], ['active', 'trialing']); } catch (\Exception $e) { return false; } } public function getCancellationTerms(Subscription $subscription): array { return [ 'immediate_cancellation' => true, 'refund_policy' => 'no_pro_rated_refunds', 'cancellation_effective' => 'immediately', 'billing_cycle_proration' => false, ]; } public function exportSubscriptionData(Subscription $subscription): array { return [ 'provider' => 'polar', 'provider_subscription_id' => $subscription->provider_subscription_id, 'data' => $subscription->provider_data, ]; } public function importSubscriptionData(User $user, array $subscriptionData): array { Log::error('Import to Polar payments not implemented'); todo('Write import subscription data'); return []; } }