feat(payment): implement comprehensive Polar subscription sync with proper date and cancellation handling

- Add Polar-specific date field mapping in PaymentOrchestrator (current_period_start, current_period_end, cancelled_at, trial_end)
  - Handle both cancellation scenarios: cancel_at_period_end=true and existing cancelled_at timestamp
  - Map customer_cancellation_reason and customer_cancellation_comment from Polar to database
  - Update billing page to show correct renewal vs expiry dates based on cancellation status
  - Restrict cancel button to activation_key provider only (Polar uses customer portal)
  - Fix button spacing between "Manage in Polar" and "Sync" buttons
  - Ensure both "Sync" and "Recheck Status" buttons use identical sync functionality
This commit is contained in:
idevakk
2025-12-06 10:42:25 -08:00
parent 0724e6da43
commit 15e018eb88
4 changed files with 150 additions and 18 deletions

View File

@@ -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
*/

View File

@@ -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) {