- Add support for subscription.uncanceled webhook event - Fix spelling mismatch for subscription.canceled (Polar) vs subscription.cancelled (code) - Implement proper cancel_at_period_end handling in subscription.canceled events - Add cancelled_at field updates for subscription.updated events - Handle Polar's spelling variants (canceled_at vs cancelled_at) consistently - Remove non-existent pause_reason column from subscription uncanceled handler - Enhance webhook logging with detailed field update tracking - Add comprehensive cancellation metadata storage in provider_data - Gracefully handle null provider_subscription_id in payment confirmation polling All Polar webhook events now properly sync subscription state including cancellation timing, reasons, and billing period details.
280 lines
9.8 KiB
PHP
280 lines
9.8 KiB
PHP
<?php
|
|
|
|
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;
|
|
use Livewire\Component;
|
|
|
|
class PaymentConfirmation extends Component
|
|
{
|
|
public $subscription;
|
|
|
|
public $sessionToken;
|
|
|
|
public $status = 'verifying';
|
|
|
|
public $pollCount = 0;
|
|
|
|
public $maxPolls = 5;
|
|
|
|
public $showConfetti = false;
|
|
|
|
public $errorMessage = null;
|
|
|
|
protected $listeners = ['$refresh'];
|
|
|
|
public function mount($subscription = null, $sessionToken = null): void
|
|
{
|
|
$this->subscription = $subscription;
|
|
$this->sessionToken = $sessionToken;
|
|
|
|
// Validate that we have a subscription to check
|
|
if (! $this->subscription) {
|
|
Log::warning('PaymentConfirmation: No subscription provided', [
|
|
'user_id' => auth()->id(),
|
|
'session_token' => $sessionToken,
|
|
]);
|
|
|
|
$this->status = 'error';
|
|
$this->errorMessage = 'No subscription found for this payment session.';
|
|
|
|
return;
|
|
}
|
|
|
|
// Validate subscription belongs to current user
|
|
if ($this->subscription->user_id !== auth()->id()) {
|
|
Log::warning('PaymentConfirmation: Subscription does not belong to current user', [
|
|
'user_id' => auth()->id(),
|
|
'subscription_user_id' => $this->subscription->user_id,
|
|
'subscription_id' => $this->subscription->id,
|
|
]);
|
|
|
|
$this->status = 'error';
|
|
$this->errorMessage = 'Invalid subscription for this user.';
|
|
|
|
return;
|
|
}
|
|
|
|
Log::info('PaymentConfirmation: Mounted with subscription', [
|
|
'user_id' => auth()->id(),
|
|
'subscription_id' => $this->subscription->id,
|
|
'provider' => $this->subscription->provider,
|
|
'provider_subscription_id' => $this->subscription->provider_subscription_id,
|
|
'current_status' => $this->subscription->status,
|
|
'created_at' => $this->subscription->created_at,
|
|
'minutes_ago' => $this->subscription->created_at->diffInMinutes(now()),
|
|
]);
|
|
|
|
// Initial status check
|
|
$this->checkSubscriptionStatus();
|
|
|
|
// Debug: If subscription is already active, show confetti immediately
|
|
if ($this->subscription && $this->subscription->status === 'active') {
|
|
$this->status = 'activated';
|
|
$this->showConfetti = true;
|
|
$this->pollCount = $this->maxPolls;
|
|
|
|
Log::info('PaymentConfirmation: Active subscription detected, showing confetti immediately', [
|
|
'subscription_id' => $this->subscription->id,
|
|
'status' => $this->subscription->status,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check subscription status via payment provider
|
|
*/
|
|
public function checkSubscriptionStatus(): void
|
|
{
|
|
if (! $this->subscription || $this->pollCount >= $this->maxPolls) {
|
|
if ($this->pollCount >= $this->maxPolls) {
|
|
Log::info('PaymentConfirmation: Max polls reached, redirecting to dashboard', [
|
|
'subscription_id' => $this->subscription?->id,
|
|
'poll_count' => $this->pollCount,
|
|
]);
|
|
|
|
$this->redirect(route('dashboard'));
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Increment poll count first
|
|
$this->pollCount++;
|
|
|
|
try {
|
|
$orchestrator = app(PaymentOrchestrator::class);
|
|
$user = auth()->user();
|
|
|
|
Log::info('PaymentConfirmation: Checking subscription status', [
|
|
'subscription_id' => $this->subscription->id,
|
|
'provider_subscription_id' => $this->subscription->provider_subscription_id,
|
|
'provider' => $this->subscription->provider,
|
|
'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', [
|
|
'subscription_id' => $this->subscription->id,
|
|
'provider' => $this->subscription->provider,
|
|
'provider_checkout_id' => $this->subscription->provider_checkout_id,
|
|
'poll_count' => $this->pollCount,
|
|
]);
|
|
|
|
// 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
|
|
$this->status = in_array($this->subscription->status, ['active', 'trialing'])
|
|
? 'activated'
|
|
: 'pending';
|
|
|
|
Log::info('PaymentConfirmation: Max polls reached, final status determined', [
|
|
'final_status' => $this->status,
|
|
'subscription_status' => $this->subscription->status,
|
|
]);
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('PaymentConfirmation: Error checking subscription status', [
|
|
'subscription_id' => $this->subscription->id,
|
|
'error' => $e->getMessage(),
|
|
'poll_count' => $this->pollCount,
|
|
]);
|
|
|
|
// Don't immediately set error status, continue trying unless max polls reached
|
|
if ($this->pollCount >= $this->maxPolls) {
|
|
$this->errorMessage = 'Unable to verify payment status after multiple attempts. Please check your subscription page.';
|
|
$this->status = 'error';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get polling interval in milliseconds
|
|
*/
|
|
public function getPollingIntervalProperty(): int
|
|
{
|
|
return 30000; // 30 seconds
|
|
}
|
|
|
|
/**
|
|
* Check if polling should continue
|
|
*/
|
|
public function getShouldContinuePollingProperty(): bool
|
|
{
|
|
return $this->status === 'verifying' && $this->pollCount < $this->maxPolls;
|
|
}
|
|
|
|
/**
|
|
* Get status display text
|
|
*/
|
|
public function getStatusTextProperty(): string
|
|
{
|
|
return match ($this->status) {
|
|
'verifying' => 'Verifying your payment...',
|
|
'activated' => 'Payment successful! Your subscription is now active.',
|
|
'pending' => 'Payment is being processed. Please check your subscription page.',
|
|
'error' => 'Unable to verify payment status.',
|
|
default => 'Checking payment status...',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get status icon
|
|
*/
|
|
public function getStatusIconProperty(): string
|
|
{
|
|
return match ($this->status) {
|
|
'verifying' => 'clock',
|
|
'activated' => 'check-circle',
|
|
'pending' => 'clock',
|
|
'error' => 'exclamation-triangle',
|
|
default => 'clock',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get status color
|
|
*/
|
|
public function getStatusColorProperty(): string
|
|
{
|
|
return match ($this->status) {
|
|
'verifying' => 'blue',
|
|
'activated' => 'green',
|
|
'pending' => 'yellow',
|
|
'error' => 'red',
|
|
default => 'gray',
|
|
};
|
|
}
|
|
|
|
public function render(): Factory|View
|
|
{
|
|
return view('livewire.payment-confirmation');
|
|
}
|
|
}
|