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:
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user