diff --git a/app/Filament/Resources/PlanResource.php b/app/Filament/Resources/PlanResource.php index 7c559b9..432195b 100644 --- a/app/Filament/Resources/PlanResource.php +++ b/app/Filament/Resources/PlanResource.php @@ -56,11 +56,13 @@ class PlanResource extends Resource Select::make('billing_cycle_days') ->label('Billing Cycle') ->options([ + 1 => 'Daily', + 7 => 'Weekly', 30 => 'Monthly', - 90 => 'Quarterly', - 365 => 'Yearly', 60 => 'Bi-Monthly', + 90 => 'Quarterly', 180 => 'Semi-Annual', + 365 => 'Yearly', ]) ->default(30) ->required(), @@ -225,8 +227,12 @@ class PlanResource extends Resource Tables\Filters\SelectFilter::make('billing_cycle_days') ->label('Billing Cycle') ->options([ + 1 => 'Daily', + 7 => 'Weekly', 30 => 'Monthly', + 60 => 'Bi-Monthly', 90 => 'Quarterly', + 180 => 'Semi-Annual', 365 => 'Yearly', ]), @@ -239,22 +245,76 @@ class PlanResource extends Resource ViewAction::make(), EditAction::make(), DeleteAction::make() - ->before(function (Plan $record) { - // Prevent deletion if plan has active subscriptions - if ($record->subscriptions()->where('status', 'active')->exists()) { - Log::error('Cannot delete plan with active subscriptions'); + ->requiresConfirmation() + ->before(function (DeleteAction $action, Plan $record) { + // Prevent deletion if plan has any subscriptions (active, cancelled, etc.) + if ($record->subscriptions()->exists()) { + Log::warning('Attempted to delete plan with existing subscriptions', [ + 'plan_id' => $record->id, + 'plan_name' => $record->name, + 'subscription_count' => $record->subscriptions()->count(), + ]); + + // Show warning notification + \Filament\Notifications\Notification::make() + ->warning() + ->title('Cannot Delete Plan') + ->body("Plan '{$record->name}' has {$record->subscriptions()->count()} subscription(s). Please cancel or remove all subscriptions first.") + ->persistent() + ->send(); + + // Halt the deletion process + $action->halt(); } + }) + ->action(function (Plan $record) { + // This action will only run if not halted + $record->delete(); + + // Show success notification + \Filament\Notifications\Notification::make() + ->success() + ->title('Plan Deleted') + ->body("Plan '{$record->name}' has been deleted successfully.") + ->send(); }), ]) - ->toolbarActions([ + ->bulkActions([ BulkActionGroup::make([ DeleteBulkAction::make() - ->before(function ($records) { + ->requiresConfirmation() + ->before(function (DeleteBulkAction $action, $records) { foreach ($records as $record) { - if ($record->subscriptions()->where('status', 'active')->exists()) { - Log::error('Cannot delete plan(s) with active subscriptions'); + if ($record->subscriptions()->exists()) { + Log::warning('Attempted to bulk delete plan with existing subscriptions', [ + 'plan_id' => $record->id, + 'plan_name' => $record->name, + 'subscription_count' => $record->subscriptions()->count(), + ]); + + \Filament\Notifications\Notification::make() + ->warning() + ->title('Cannot Delete Plans') + ->body("Plan '{$record->name}' has {$record->subscriptions()->count()} subscription(s). Please cancel or remove all subscriptions first.") + ->persistent() + ->send(); + + // Halt the bulk deletion process + $action->halt(); + return; } } + }) + ->action(function ($records) { + foreach ($records as $record) { + $record->delete(); + } + + \Filament\Notifications\Notification::make() + ->success() + ->title('Plans Deleted') + ->body(count($records).' plan(s) have been deleted successfully.') + ->send(); }), ]), ]) diff --git a/app/Filament/Resources/PlanResource/Pages/EditPlan.php b/app/Filament/Resources/PlanResource/Pages/EditPlan.php index b1475e1..7037a98 100644 --- a/app/Filament/Resources/PlanResource/Pages/EditPlan.php +++ b/app/Filament/Resources/PlanResource/Pages/EditPlan.php @@ -62,12 +62,15 @@ class EditPlan extends EditRecord Select::make('billing_cycle_days') ->label('Billing Cycle') ->options([ + 1 => 'Daily', + 7 => 'Weekly', 30 => 'Monthly', - 90 => 'Quarterly', - 365 => 'Yearly', 60 => 'Bi-Monthly', + 90 => 'Quarterly', 180 => 'Semi-Annual', + 365 => 'Yearly', ]) + ->default(30) ->required(), ]), diff --git a/app/Livewire/PaymentConfirmation.php b/app/Livewire/PaymentConfirmation.php index 3765388..a7f9956 100644 --- a/app/Livewire/PaymentConfirmation.php +++ b/app/Livewire/PaymentConfirmation.php @@ -3,7 +3,6 @@ namespace App\Livewire; use App\Models\Subscription; -use App\Services\Payments\PaymentOrchestrator; use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\Log; @@ -25,6 +24,8 @@ class PaymentConfirmation extends Component public $errorMessage = null; + public $isChecking = false; + protected $listeners = ['$refresh']; public function mount($subscription = null, $sessionToken = null): void @@ -100,95 +101,60 @@ class PaymentConfirmation extends Component $this->redirect(route('dashboard')); } + $this->isChecking = false; + return; } + // Set loading state + $this->isChecking = true; + // Increment poll count first $this->pollCount++; try { - $orchestrator = app(PaymentOrchestrator::class); - $user = auth()->user(); - - Log::info('PaymentConfirmation: Checking subscription status', [ + Log::info('PaymentConfirmation: Syncing subscription with provider', [ 'subscription_id' => $this->subscription->id, 'provider_subscription_id' => $this->subscription->provider_subscription_id, 'provider' => $this->subscription->provider, + 'provider_checkout_id' => $this->subscription->provider_checkout_id, 'poll_count' => $this->pollCount, ]); - // Check status via provider (only if we have a provider subscription ID) - if (empty($this->subscription->provider_subscription_id)) { - Log::info('PaymentConfirmation: Skipping provider status check - no provider subscription ID yet', [ + // Use the same sync method as the billing page + $syncSuccess = $this->subscription->syncWithProvider(); + + // Refresh the subscription from database to get updated status + $this->subscription->refresh(); + + Log::info('PaymentConfirmation: Subscription sync completed', [ + 'subscription_id' => $this->subscription->id, + 'sync_success' => $syncSuccess, + 'current_status' => $this->subscription->status, + 'provider_subscription_id' => $this->subscription->provider_subscription_id, + ]); + + // Check if subscription is now active + if (in_array($this->subscription->status, ['active', 'trialing'])) { + $this->status = 'activated'; + $this->showConfetti = true; + + // Stop polling when activated + $this->pollCount = $this->maxPolls; + + Log::info('PaymentConfirmation: Subscription activated successfully', [ 'subscription_id' => $this->subscription->id, - 'provider' => $this->subscription->provider, - 'provider_checkout_id' => $this->subscription->provider_checkout_id, - 'poll_count' => $this->pollCount, + 'final_status' => $this->subscription->status, ]); - // Don't update status, just continue polling return; } - $statusResult = $orchestrator->checkSubscriptionStatus( - $user, - $this->subscription->provider, - $this->subscription->provider_subscription_id - ); - - if ($statusResult['success']) { - $providerStatus = $statusResult['status']; - - Log::info('PaymentConfirmation: Provider status received', [ - 'provider_status' => $providerStatus, - 'subscription_id' => $this->subscription->id, - ]); - - // Update local subscription if status changed - if ($providerStatus !== $this->subscription->status) { - $this->subscription->status = $providerStatus; - $this->subscription->save(); - - Log::info('PaymentConfirmation: Updated local subscription status', [ - 'old_status' => $this->subscription->getOriginal('status'), - 'new_status' => $providerStatus, - ]); - } - - // Check if subscription is now active - if ($providerStatus === 'active') { - $this->status = 'activated'; - $this->showConfetti = true; - - // Stop polling when activated - $this->pollCount = $this->maxPolls; - - Log::info('PaymentConfirmation: Subscription activated successfully', [ - 'subscription_id' => $this->subscription->id, - ]); - - return; - } - } else { - // Handle status check failure - Log::warning('PaymentConfirmation: Provider status check failed', [ - 'error' => $statusResult['error'] ?? 'Unknown error', - 'subscription_id' => $this->subscription->id, - 'retry_suggested' => $statusResult['retry_suggested'] ?? false, - ]); - - // If retry is suggested (e.g., webhook not processed yet), continue polling - if (! ($statusResult['retry_suggested'] ?? false)) { - // If retry is not suggested, we might want to show an error - // but for now, continue polling to be safe - } - } - // Continue polling if not active and max polls not reached if ($this->pollCount < $this->maxPolls) { $this->status = 'verifying'; } else { - // Max polls reached, check final status + // Max polls reached, determine final status $this->status = in_array($this->subscription->status, ['active', 'trialing']) ? 'activated' : 'pending'; @@ -200,7 +166,7 @@ class PaymentConfirmation extends Component } } catch (\Exception $e) { - Log::error('PaymentConfirmation: Error checking subscription status', [ + Log::error('PaymentConfirmation: Error syncing subscription', [ 'subscription_id' => $this->subscription->id, 'error' => $e->getMessage(), 'poll_count' => $this->pollCount, @@ -211,6 +177,9 @@ class PaymentConfirmation extends Component $this->errorMessage = 'Unable to verify payment status after multiple attempts. Please check your subscription page.'; $this->status = 'error'; } + } finally { + // Always reset loading state + $this->isChecking = false; } } diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index a6e25e7..b6af72e 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -690,4 +690,59 @@ class Subscription extends Model // Fallback to legacy monthly_billing return $this->plan && $this->plan->monthly_billing ? 30 : 365; } + + /** + * Get plan display name, handles deleted plans gracefully + */ + public function getPlanDisplayName(): string + { + if ($this->plan) { + return $this->plan->name; + } + + // Check provider data for stored plan name + if ($this->provider_data) { + // Check for Polar plan name + if (isset($this->provider_data['polar_subscription']['product']['name'])) { + return $this->provider_data['polar_subscription']['product']['name']; + } + + // Check for stored plan details + if (isset($this->provider_data['plan_details']['name'])) { + return $this->provider_data['plan_details']['name']; + } + + // Check metadata + if (isset($this->provider_data['plan_name'])) { + return $this->provider_data['plan_name']; + } + } + + return 'Deleted Plan'; + } + + /** + * Get plan price, handles deleted plans gracefully + */ + public function getPlanPrice(): float + { + if ($this->plan) { + return $this->plan->price; + } + + // Check provider data for stored plan price + if ($this->provider_data) { + // Check for Polar subscription amount + if (isset($this->provider_data['polar_subscription']['amount'])) { + return $this->provider_data['polar_subscription']['amount'] / 100; // Convert from cents + } + + // Check for stored plan details + if (isset($this->provider_data['plan_details']['price'])) { + return (float) $this->provider_data['plan_details']['price']; + } + } + + return 0.0; + } } diff --git a/app/Services/Payments/Providers/PolarProvider.php b/app/Services/Payments/Providers/PolarProvider.php index 294a838..ba21f81 100644 --- a/app/Services/Payments/Providers/PolarProvider.php +++ b/app/Services/Payments/Providers/PolarProvider.php @@ -1106,24 +1106,28 @@ class PolarProvider implements PaymentProviderContract ]); } + // Convert billing cycle to Polar's format + $billingInterval = $this->convertBillingCycleToPolarInterval($plan->billing_cycle_days ?? 30); + // Create new product with correct structure $productData = [ 'name' => $plan->name, 'description' => $plan->description ?? 'Subscription plan', - 'recurring_interval' => 'month', - 'recurring_interval_count' => 1, + 'recurring_interval' => $billingInterval['interval'], + 'recurring_interval_count' => $billingInterval['interval_count'], 'prices' => [ [ 'amount_type' => 'fixed', 'price_amount' => (int) ($plan->price * 100), // Convert to cents 'price_currency' => 'usd', - 'recurring_interval' => 'month', - 'recurring_interval_count' => 1, + 'recurring_interval' => $billingInterval['interval'], + 'recurring_interval_count' => $billingInterval['interval_count'], ], ], 'metadata' => [ 'plan_id' => $plan->id, 'plan_name' => $plan->name, + 'billing_cycle_days' => $plan->billing_cycle_days ?? 30, ], ]; @@ -2361,4 +2365,21 @@ class PolarProvider implements PaymentProviderContract // For now, return basic stats - could be expanded with database tracking return $stats; } + + /** + * Convert billing cycle days to Polar's recurring interval format + */ + protected function convertBillingCycleToPolarInterval(int $billingCycleDays): array + { + return match ($billingCycleDays) { + 1 => ['interval' => 'day', 'interval_count' => 1], // Daily + 7 => ['interval' => 'week', 'interval_count' => 1], // Weekly + 30 => ['interval' => 'month', 'interval_count' => 1], // Monthly + 60 => ['interval' => 'month', 'interval_count' => 2], // Bi-monthly + 90 => ['interval' => 'month', 'interval_count' => 3], // Quarterly + 180 => ['interval' => 'month', 'interval_count' => 6], // Semi-annual + 365 => ['interval' => 'year', 'interval_count' => 1], // Yearly + default => ['interval' => 'month', 'interval_count' => 1], // Default to monthly + }; + } } diff --git a/resources/views/livewire/payment-confirmation.blade.php b/resources/views/livewire/payment-confirmation.blade.php index 44adb5e..453e003 100644 --- a/resources/views/livewire/payment-confirmation.blade.php +++ b/resources/views/livewire/payment-confirmation.blade.php @@ -27,8 +27,32 @@

- Check {{ $pollCount }} of {{ $maxPolls }} • Retrying in 30 seconds... + Check {{ $pollCount }} of {{ $maxPolls }} • Checking every 30 seconds...

+ + @if ($pollCount > 1) +

+ This is taking longer than expected. Webhook processing may be delayed. +

+ +
+ +
+ @endif @endif @@ -198,11 +222,11 @@ @endif - + @if ($this->shouldContinuePolling) - + @endif diff --git a/resources/views/livewire/settings/billing.blade.php b/resources/views/livewire/settings/billing.blade.php index a833699..33ab377 100644 --- a/resources/views/livewire/settings/billing.blade.php +++ b/resources/views/livewire/settings/billing.blade.php @@ -52,7 +52,7 @@
- {{ $latestActiveSubscription->plan->name }} + {{ $latestActiveSubscription->getPlanDisplayName() }} {{ ucfirst($latestActiveSubscription->status) }} @@ -62,7 +62,7 @@
- {{ $latestActiveSubscription->plan->description ?? 'Subscription plan' }} + {{ $latestActiveSubscription->plan?->description ?? 'Subscription plan' }} @if($latestActiveSubscription->isActive()) @@ -152,7 +152,7 @@
- {{ $subscription->plan->name }} + {{ $subscription->getPlanDisplayName() }} @@ -190,7 +190,7 @@
- ${{ number_format($subscription->plan->price, 2) }} + ${{ number_format($subscription->getPlanPrice(), 2) }} @if($isWithin30Minutes && $subscription->id === $latestSubscription->id) @@ -232,7 +232,7 @@ @if($subscriptionToCancel)
- {{ $subscriptionToCancel->plan->name }} + {{ $subscriptionToCancel->getPlanDisplayName() }} {{ $subscriptionToCancel->getProviderDisplayName() }}