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