diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index 086529f..3f6a01e 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -197,7 +197,7 @@ class Subscription extends Model { try { // For Polar provider, check if we need to fetch subscription ID first - if ($this->provider === 'polar' && empty($this->provider_subscription_id) && !empty($this->user->polar_cust_id)) { + if ($this->provider === 'polar' && empty($this->provider_subscription_id) && ! empty($this->user->polar_cust_id)) { $this->fetchPolarSubscriptionId(); } @@ -252,7 +252,7 @@ class Subscription extends Model $data = $response->json(); $subscriptions = $data['items'] ?? []; - if (!empty($subscriptions)) { + if (! empty($subscriptions)) { // Find the subscription that matches our plan or take the most recent active one $matchingSubscription = null; @@ -265,23 +265,55 @@ class Subscription extends Model } // If no exact match, take the most recent active subscription - if (!$matchingSubscription && !empty($subscriptions)) { + if (! $matchingSubscription && ! empty($subscriptions)) { $matchingSubscription = $subscriptions[0]; } if ($matchingSubscription) { + // Parse dates from Polar response + $startsAt = null; + $endsAt = null; + $cancelledAt = null; + + // Handle current_period_start + if (isset($matchingSubscription['current_period_start'])) { + $startsAt = \Carbon\Carbon::parse($matchingSubscription['current_period_start']); + } + + // Handle current_period_end (renewal date) + if (isset($matchingSubscription['current_period_end'])) { + $endsAt = \Carbon\Carbon::parse($matchingSubscription['current_period_end']); + } + // Handle ends_at (cancellation/expiry date) + elseif (isset($matchingSubscription['ends_at'])) { + $endsAt = \Carbon\Carbon::parse($matchingSubscription['ends_at']); + } + // Handle expires_at (expiry date) + elseif (isset($matchingSubscription['expires_at'])) { + $endsAt = \Carbon\Carbon::parse($matchingSubscription['expires_at']); + } + + // Handle cancelled_at + if (isset($matchingSubscription['cancelled_at'])) { + $cancelledAt = \Carbon\Carbon::parse($matchingSubscription['cancelled_at']); + } + $this->update([ 'provider_subscription_id' => $matchingSubscription['id'], 'status' => $matchingSubscription['status'], - 'starts_at' => isset($matchingSubscription['current_period_start']) - ? \Carbon\Carbon::parse($matchingSubscription['current_period_start']) - : null, - 'ends_at' => isset($matchingSubscription['current_period_end']) - ? \Carbon\Carbon::parse($matchingSubscription['current_period_end']) - : null, + 'starts_at' => $startsAt, + 'ends_at' => $endsAt, + 'cancelled_at' => $cancelledAt, 'provider_data' => array_merge($this->provider_data ?? [], [ 'polar_subscription' => $matchingSubscription, 'subscription_id_fetched_at' => now()->toISOString(), + 'polar_dates' => [ + 'current_period_start' => $matchingSubscription['current_period_start'] ?? null, + 'current_period_end' => $matchingSubscription['current_period_end'] ?? null, + 'ends_at' => $matchingSubscription['ends_at'] ?? null, + 'expires_at' => $matchingSubscription['expires_at'] ?? null, + 'cancelled_at' => $matchingSubscription['cancelled_at'] ?? null, + ], ]), ]); @@ -289,6 +321,9 @@ class Subscription extends Model 'subscription_id' => $this->id, 'polar_subscription_id' => $matchingSubscription['id'], 'customer_id' => $this->user->polar_cust_id, + 'starts_at' => $startsAt?->toISOString(), + 'ends_at' => $endsAt?->toISOString(), + 'cancelled_at' => $cancelledAt?->toISOString(), ]); } } diff --git a/app/Services/Payments/PaymentOrchestrator.php b/app/Services/Payments/PaymentOrchestrator.php index 1de5b95..997114c 100644 --- a/app/Services/Payments/PaymentOrchestrator.php +++ b/app/Services/Payments/PaymentOrchestrator.php @@ -698,15 +698,96 @@ class PaymentOrchestrator */ protected function updateLocalSubscriptionFromProvider(Subscription $subscription, array $providerData): void { - $subscription->update([ + $updateData = [ 'status' => $providerData['status'] ?? $subscription->status, - 'ends_at' => $providerData['ends_at'] ?? $subscription->ends_at, - 'trial_ends_at' => $providerData['trial_ends_at'] ?? $subscription->trial_ends_at, 'provider_data' => array_merge($subscription->provider_data ?? [], $providerData), 'synced_at' => now(), + ]; + + // Handle Polar-specific date mapping + if ($subscription->provider === 'polar') { + // Map Polar's date fields to our database columns + $updateData['starts_at'] = $this->parseDateTime($providerData['current_period_start'] ?? null); + + // Check if subscription is scheduled for cancellation + $isScheduledForCancellation = $providerData['cancel_at_period_end'] ?? false; + + // For ends_at and cancelled_at logic: + if (! empty($providerData['cancelled_at'])) { + // Already cancelled subscription - use Polar's actual cancellation data + $updateData['ends_at'] = $this->parseDateTime($providerData['ends_at'] ?? $providerData['current_period_end'] ?? null); + $updateData['cancelled_at'] = $this->parseDateTime($providerData['cancelled_at']); + $updateData['status'] = 'cancelled'; + } elseif ($isScheduledForCancellation) { + // Scheduled for cancellation - treat as cancelled with expiry at period end + $updateData['ends_at'] = $this->parseDateTime($providerData['current_period_end'] ?? null); + $updateData['cancelled_at'] = now(); // Set cancellation time to now when detected + $updateData['status'] = 'cancelled'; + $updateData['cancellation_reason'] = $updateData['cancellation_reason'] ?? 'Customer cancelled via Polar portal (cancel at period end)'; + } else { + // Active subscription + $updateData['ends_at'] = $this->parseDateTime($providerData['current_period_end'] ?? null); + // Don't overwrite existing cancelled_at for active subscriptions + } + + $updateData['trial_ends_at'] = $this->parseDateTime($providerData['trial_end'] ?? null); + + // Map cancellation reason if available + if (! empty($providerData['customer_cancellation_reason'])) { + $updateData['cancellation_reason'] = $providerData['customer_cancellation_reason']; + + // Also store the comment if available + if (! empty($providerData['customer_cancellation_comment'])) { + $updateData['cancellation_reason'] .= ' - Comment: '.$providerData['customer_cancellation_comment']; + } + } + } else { + // Generic date mapping for other providers + $updateData['ends_at'] = $this->parseDateTime($providerData['ends_at'] ?? $subscription->ends_at); + $updateData['trial_ends_at'] = $this->parseDateTime($providerData['trial_ends_at'] ?? $subscription->trial_ends_at); + } + + // Only update fields that are actually provided (not null) + $updateData = array_filter($updateData, function ($value, $key) use ($providerData) { + // Keep null values from API if they're explicitly provided + if (array_key_exists($key, $providerData)) { + return true; + } + + // Otherwise don't overwrite existing values with null + return $value !== null; + }, ARRAY_FILTER_USE_BOTH); + + $subscription->update($updateData); + + Log::info('Subscription updated from provider data', [ + 'subscription_id' => $subscription->id, + 'provider' => $subscription->provider, + 'update_data' => $updateData, ]); } + /** + * Parse datetime from various formats + */ + protected function parseDateTime($dateTime): ?\Carbon\Carbon + { + if (empty($dateTime)) { + return null; + } + + try { + return \Carbon\Carbon::parse($dateTime); + } catch (\Exception $e) { + Log::warning('Failed to parse datetime', [ + 'datetime' => $dateTime, + 'error' => $e->getMessage(), + ]); + + return null; + } + } + /** * Get all available providers */ diff --git a/app/Services/Payments/Providers/PolarProvider.php b/app/Services/Payments/Providers/PolarProvider.php index 5bac3bf..9766bc2 100644 --- a/app/Services/Payments/Providers/PolarProvider.php +++ b/app/Services/Payments/Providers/PolarProvider.php @@ -482,6 +482,13 @@ class PolarProvider implements PaymentProviderContract $polarSubscription = $response->json(); + // Log the full Polar subscription response for debugging + Log::info('Polar subscription response received', [ + 'subscription_id' => $providerSubscriptionId, + 'response_keys' => array_keys($polarSubscription), + 'full_response' => $polarSubscription, + ]); + if (! $polarSubscription || ! isset($polarSubscription['id'])) { Log::error('Invalid Polar subscription response', [ 'subscription_id' => $providerSubscriptionId, @@ -495,13 +502,18 @@ class PolarProvider implements PaymentProviderContract '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'], + 'current_period_start' => $polarSubscription['current_period_start'] ?? null, + 'current_period_end' => $polarSubscription['current_period_end'] ?? null, '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'], + 'created_at' => $polarSubscription['created_at'] ?? null, 'updated_at' => $polarSubscription['modified_at'] ?? null, + 'ends_at' => $polarSubscription['ends_at'] ?? null, // Check if Polar has ends_at + 'expires_at' => $polarSubscription['expires_at'] ?? null, // Check if Polar has expires_at + 'cancelled_at' => $polarSubscription['cancelled_at'] ?? null, // Check if Polar has cancelled_at + 'customer_cancellation_reason' => $polarSubscription['customer_cancellation_reason'] ?? null, + 'customer_cancellation_comment' => $polarSubscription['customer_cancellation_comment'] ?? null, ]; } catch (\Exception $e) { diff --git a/resources/views/livewire/settings/billing.blade.php b/resources/views/livewire/settings/billing.blade.php index da5ad12..a833699 100644 --- a/resources/views/livewire/settings/billing.blade.php +++ b/resources/views/livewire/settings/billing.blade.php @@ -68,7 +68,11 @@ @if($latestActiveSubscription->isActive()) @if($latestActiveSubscription->ends_at) - {{ __('Renews on :date', ['date' => $latestActiveSubscription->ends_at->format('M j, Y')]) }} + @if($latestActiveSubscription->cancelled_at) + {{ __('Expires on :date', ['date' => $latestActiveSubscription->ends_at->format('M j, Y')]) }} + @else + {{ __('Renews on :date', ['date' => $latestActiveSubscription->ends_at->format('M j, Y')]) }} + @endif @else {{ __('Active subscription') }} @endif @@ -93,7 +97,7 @@ wire:click="managePolarSubscription" variant="outline" size="sm" - class="w-full sm:w-auto cursor-pointer" + class="w-full sm:w-auto cursor-pointer sm:mr-2" > {{ __('Manage in Polar') }} @@ -112,7 +116,7 @@ @endif - @if($latestActiveSubscription->isActive() && $latestActiveSubscription->provider !== 'activation_key') + @if($latestActiveSubscription->isActive() && in_array($latestActiveSubscription->provider, ['activation_key']))