feat: implement comprehensive multi-provider payment processing system

- Add unified payment provider architecture with contract-based design
  - Implement 6 payment providers: Stripe, Lemon Squeezy, Polar, Oxapay, Crypto, Activation Keys
  - Create subscription management with lifecycle handling (create, cancel, pause, resume, update)
  - Add coupon system with usage tracking and trial extensions
  - Build Filament admin resources for payment providers, subscriptions, coupons, and trials
  - Implement payment orchestration service with provider registry and configuration management
  - Add comprehensive payment logging and webhook handling for all providers
  - Create customer analytics dashboard with revenue, churn, and lifetime value metrics
  - Add subscription migration service for provider switching
  - Include extensive test coverage for all payment functionality
This commit is contained in:
idevakk
2025-11-19 09:37:00 -08:00
parent 0560016f33
commit 27ac13948c
83 changed files with 15613 additions and 103 deletions

View File

@@ -0,0 +1,985 @@
<?php
namespace App\Services\Payments\Providers;
use App\Contracts\Payments\PaymentProviderContract;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class PolarProvider implements PaymentProviderContract
{
protected array $config;
protected string $apiBaseUrl = 'https://api.polar.sh';
public function __construct(array $config = [])
{
$this->config = array_merge([
'api_key' => config('services.polar.api_key'),
'webhook_secret' => config('services.polar.webhook_secret'),
'success_url' => route('payment.success'),
'cancel_url' => route('payment.cancel'),
'webhook_url' => route('webhook.payment', 'polar'),
], $config);
}
public function getName(): string
{
return 'polar';
}
public function isActive(): bool
{
return ! empty($this->config['api_key']);
}
public function createSubscription(User $user, Plan $plan, array $options = []): array
{
try {
// Get or create Polar customer
$customer = $this->getOrCreateCustomer($user);
// Get or create Polar product/price
$priceId = $this->getOrCreatePrice($plan);
// Create checkout session
$checkoutData = [
'customer_id' => $customer['id'],
'price_id' => $priceId,
'success_url' => $this->config['success_url'],
'cancel_url' => $this->config['cancel_url'],
'customer_email' => $user->email,
'customer_name' => $user->name,
'metadata' => [
'user_id' => $user->id,
'plan_id' => $plan->id,
'plan_name' => $plan->name,
],
];
// Add trial information if specified
if (isset($options['trial_days']) && $options['trial_days'] > 0) {
$checkoutData['trial_period_days'] = $options['trial_days'];
}
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
'Content-Type' => 'application/json',
])->post($this->apiBaseUrl.'/v1/checkouts', $checkoutData);
if (! $response->successful()) {
throw new \Exception('Polar checkout creation failed: '.$response->body());
}
$checkout = $response->json();
// Create subscription record
$subscription = Subscription::create([
'user_id' => $user->id,
'plan_id' => $plan->id,
'type' => 'recurring',
'stripe_id' => $checkout['id'], // Using stripe_id field for Polar checkout ID
'stripe_status' => 'pending',
'provider' => $this->getName(),
'provider_subscription_id' => $checkout['id'],
'status' => 'pending_payment',
'starts_at' => null,
'ends_at' => null,
'provider_data' => [
'checkout_id' => $checkout['id'],
'checkout_url' => $checkout['url'],
'customer_id' => $customer['id'],
'price_id' => $priceId,
'created_at' => now()->toISOString(),
],
]);
return [
'provider_subscription_id' => $checkout['id'],
'status' => 'pending_payment',
'checkout_url' => $checkout['url'],
'customer_id' => $customer['id'],
'price_id' => $priceId,
'type' => 'polar_checkout',
'expires_at' => $checkout['expires_at'] ?? now()->addHours(24)->toISOString(),
];
} catch (\Exception $e) {
Log::error('Polar subscription creation failed', [
'user_id' => $user->id,
'plan_id' => $plan->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function cancelSubscription(Subscription $subscription, string $reason = ''): bool
{
try {
$polarSubscriptionId = $this->getPolarSubscriptionId($subscription);
if (! $polarSubscriptionId) {
// Local cancellation only
$subscription->update([
'status' => 'cancelled',
'cancelled_at' => now(),
'cancellation_reason' => $reason,
]);
return true;
}
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
'Content-Type' => 'application/json',
])->delete($this->apiBaseUrl.'/v1/subscriptions/'.$polarSubscriptionId, [
'cancellation_reason' => $reason,
]);
if (! $response->successful()) {
throw new \Exception('Polar subscription cancellation failed: '.$response->body());
}
// Update local subscription
$subscription->update([
'status' => 'cancelled',
'cancelled_at' => now(),
'cancellation_reason' => $reason,
]);
return true;
} catch (\Exception $e) {
Log::error('Polar subscription cancellation failed', [
'subscription_id' => $subscription->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function updateSubscription(Subscription $subscription, Plan $newPlan): array
{
try {
$polarSubscriptionId = $this->getPolarSubscriptionId($subscription);
if (! $polarSubscriptionId) {
throw new \Exception('No Polar subscription found to update');
}
$newPriceId = $this->getOrCreatePrice($newPlan);
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
'Content-Type' => 'application/json',
])->patch($this->apiBaseUrl.'/v1/subscriptions/'.$polarSubscriptionId, [
'price_id' => $newPriceId,
'proration_behavior' => 'create_prorations',
]);
if (! $response->successful()) {
throw new \Exception('Polar subscription update failed: '.$response->body());
}
$updatedSubscription = $response->json();
// Update local subscription
$subscription->update([
'plan_id' => $newPlan->id,
'provider_data' => array_merge($subscription->provider_data ?? [], [
'updated_at' => now()->toISOString(),
'polar_subscription' => $updatedSubscription,
]),
]);
return [
'provider_subscription_id' => $updatedSubscription['id'],
'status' => $updatedSubscription['status'],
'price_id' => $newPriceId,
'updated_at' => $updatedSubscription['updated_at'],
];
} catch (\Exception $e) {
Log::error('Polar subscription update failed', [
'subscription_id' => $subscription->id,
'new_plan_id' => $newPlan->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function pauseSubscription(Subscription $subscription): bool
{
try {
$polarSubscriptionId = $this->getPolarSubscriptionId($subscription);
if (! $polarSubscriptionId) {
return false;
}
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
'Content-Type' => 'application/json',
])->post($this->apiBaseUrl.'/v1/subscriptions/'.$polarSubscriptionId.'/pause');
if (! $response->successful()) {
throw new \Exception('Polar subscription pause failed: '.$response->body());
}
$subscription->update([
'status' => 'paused',
'paused_at' => now(),
]);
return true;
} catch (\Exception $e) {
Log::error('Polar subscription pause failed', [
'subscription_id' => $subscription->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function resumeSubscription(Subscription $subscription): bool
{
try {
$polarSubscriptionId = $this->getPolarSubscriptionId($subscription);
if (! $polarSubscriptionId) {
return false;
}
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
'Content-Type' => 'application/json',
])->post($this->apiBaseUrl.'/v1/subscriptions/'.$polarSubscriptionId.'/resume');
if (! $response->successful()) {
throw new \Exception('Polar subscription resume failed: '.$response->body());
}
$subscription->update([
'status' => 'active',
'resumed_at' => now(),
]);
return true;
} catch (\Exception $e) {
Log::error('Polar subscription resume failed', [
'subscription_id' => $subscription->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function getSubscriptionDetails(string $providerSubscriptionId): array
{
try {
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
])->get($this->apiBaseUrl.'/v1/subscriptions/'.$providerSubscriptionId);
if (! $response->successful()) {
throw new \Exception('Failed to retrieve Polar subscription: '.$response->body());
}
$polarSubscription = $response->json();
return [
'id' => $polarSubscription['id'],
'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'],
'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'],
'updated_at' => $polarSubscription['updated_at'],
];
} catch (\Exception $e) {
Log::error('Polar subscription details retrieval failed', [
'subscription_id' => $providerSubscriptionId,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function createCheckoutSession(User $user, Plan $plan, array $options = []): array
{
return $this->createSubscription($user, $plan, $options);
}
public function createCustomerPortalSession(User $user): array
{
try {
$customer = $this->getOrCreateCustomer($user);
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
'Content-Type' => 'application/json',
])->post($this->apiBaseUrl.'/v1/customer-portal', [
'customer_id' => $customer['id'],
'return_url' => route('dashboard'),
]);
if (! $response->successful()) {
throw new \Exception('Polar customer portal creation failed: '.$response->body());
}
$portal = $response->json();
return [
'portal_url' => $portal['url'],
'customer_id' => $customer['id'],
];
} catch (\Exception $e) {
Log::error('Polar customer portal creation failed', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function processWebhook(Request $request): array
{
try {
$payload = $request->getContent();
$signature = $request->header('Polar-Signature');
if (! $this->validateWebhook($request)) {
throw new \Exception('Invalid Polar webhook signature');
}
$webhookData = json_decode($payload, true);
$eventType = $webhookData['type'] ?? 'unknown';
$result = [
'event_type' => $eventType,
'processed' => false,
'data' => [],
];
switch ($eventType) {
case 'checkout.created':
$result = $this->handleCheckoutCreated($webhookData);
break;
case 'subscription.created':
$result = $this->handleSubscriptionCreated($webhookData);
break;
case 'subscription.updated':
$result = $this->handleSubscriptionUpdated($webhookData);
break;
case 'subscription.cancelled':
$result = $this->handleSubscriptionCancelled($webhookData);
break;
case 'subscription.paused':
$result = $this->handleSubscriptionPaused($webhookData);
break;
case 'subscription.resumed':
$result = $this->handleSubscriptionResumed($webhookData);
break;
default:
Log::info('Unhandled Polar webhook event', ['event_type' => $eventType]);
}
return $result;
} catch (\Exception $e) {
Log::error('Polar webhook processing failed', [
'error' => $e->getMessage(),
'payload' => $request->getContent(),
]);
throw $e;
}
}
public function validateWebhook(Request $request): bool
{
try {
$signature = $request->header('Polar-Signature');
$payload = $request->getContent();
if (! $signature || ! $this->config['webhook_secret']) {
return false;
}
$expectedSignature = hash_hmac('sha256', $payload, $this->config['webhook_secret']);
return hash_equals($signature, $expectedSignature);
} catch (\Exception $e) {
Log::warning('Polar webhook validation failed', [
'error' => $e->getMessage(),
]);
return false;
}
}
public function getConfiguration(): array
{
return $this->config;
}
public function syncSubscriptionStatus(Subscription $subscription): array
{
return $this->getSubscriptionDetails($subscription->provider_subscription_id);
}
public function getPaymentMethodDetails(string $paymentMethodId): array
{
try {
// Polar doesn't have separate payment method IDs like Stripe
// Return subscription details instead
return $this->getSubscriptionDetails($paymentMethodId);
} catch (\Exception $e) {
Log::error('Polar payment method details retrieval failed', [
'payment_method_id' => $paymentMethodId,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function processRefund(string $paymentId, float $amount, string $reason = ''): array
{
try {
// Polar handles refunds through their dashboard or API
// For now, we'll return a NotImplementedError
throw new \Exception('Polar refunds must be processed through Polar dashboard or API directly');
} catch (\Exception $e) {
Log::error('Polar refund processing failed', [
'payment_id' => $paymentId,
'amount' => $amount,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function getTransactionHistory(User $user, array $filters = []): array
{
try {
$customer = $this->getOrCreateCustomer($user);
$params = [
'customer_id' => $customer['id'],
'limit' => $filters['limit'] ?? 50,
];
if (isset($filters['start_date'])) {
$params['start_date'] = $filters['start_date'];
}
if (isset($filters['end_date'])) {
$params['end_date'] = $filters['end_date'];
}
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
])->get($this->apiBaseUrl.'/v1/subscriptions', $params);
if (! $response->successful()) {
throw new \Exception('Failed to retrieve Polar transaction history: '.$response->body());
}
$polarSubscriptions = $response->json();
$transactions = [];
foreach ($polarSubscriptions['data'] ?? [] as $subscription) {
$transactions[] = [
'id' => $subscription['id'],
'status' => $subscription['status'],
'amount' => $subscription['amount'] ?? 0,
'currency' => $subscription['currency'] ?? 'USD',
'created_at' => $subscription['created_at'],
'current_period_start' => $subscription['current_period_start'],
'current_period_end' => $subscription['current_period_end'],
];
}
return $transactions;
} catch (\Exception $e) {
Log::error('Polar transaction history retrieval failed', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function calculateFees(float $amount): array
{
// Polar fees vary by plan and region (typically 5-8%)
// Using 6% as default for calculation
$percentageFee = $amount * 0.06;
$totalFee = $percentageFee; // Polar typically doesn't have fixed fees
return [
'fixed_fee' => 0,
'percentage_fee' => $percentageFee,
'total_fee' => $totalFee,
'net_amount' => $amount - $totalFee,
];
}
public function getSupportedCurrencies(): array
{
return ['USD']; // Polar supports USD, EUR, and other currencies, but USD is most common
}
public function supportsRecurring(): bool
{
return true;
}
public function supportsOneTime(): bool
{
return true;
}
// Helper methods
protected function getOrCreateCustomer(User $user): array
{
// First, try to find existing customer by email
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
])->get($this->apiBaseUrl.'/v1/customers', [
'email' => $user->email,
]);
if ($response->successful() && ! empty($response->json()['data'])) {
return $response->json()['data'][0];
}
// Create new customer
$customerData = [
'email' => $user->email,
'name' => $user->name,
'metadata' => [
'user_id' => $user->id,
],
];
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
'Content-Type' => 'application/json',
])->post($this->apiBaseUrl.'/v1/customers', $customerData);
if (! $response->successful()) {
throw new \Exception('Failed to create Polar customer: '.$response->body());
}
return $response->json();
}
protected function getOrCreatePrice(Plan $plan): string
{
// Look for existing price by plan metadata
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
])->get($this->apiBaseUrl.'/v1/products', [
'metadata[plan_id]' => $plan->id,
]);
if ($response->successful() && ! empty($response->json()['data'])) {
$product = $response->json()['data'][0];
// Get the price for this product
$priceResponse = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
])->get($this->apiBaseUrl.'/v1/prices', [
'product_id' => $product['id'],
'recurring_interval' => 'month',
]);
if ($priceResponse->successful() && ! empty($priceResponse->json()['data'])) {
return $priceResponse->json()['data'][0]['id'];
}
}
// Create new product and price
$productData = [
'name' => $plan->name,
'description' => $plan->description ?? 'Subscription plan',
'type' => 'service',
'metadata' => [
'plan_id' => $plan->id,
'plan_name' => $plan->name,
],
];
$productResponse = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
'Content-Type' => 'application/json',
])->post($this->apiBaseUrl.'/v1/products', $productData);
if (! $productResponse->successful()) {
throw new \Exception('Failed to create Polar product: '.$productResponse->body());
}
$product = $productResponse->json();
// Create price for the product
$priceData = [
'product_id' => $product['id'],
'amount' => (int) ($plan->price * 100), // Convert to cents
'currency' => 'usd',
'recurring' => [
'interval' => 'month',
'interval_count' => 1,
],
];
$priceResponse = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
'Content-Type' => 'application/json',
])->post($this->apiBaseUrl.'/v1/prices', $priceData);
if (! $priceResponse->successful()) {
throw new \Exception('Failed to create Polar price: '.$priceResponse->body());
}
$price = $priceResponse->json();
return $price['id'];
}
protected function getPolarSubscriptionId(Subscription $subscription): ?string
{
$providerData = $subscription->provider_data ?? [];
return $providerData['polar_subscription']['id'] ?? null;
}
// Webhook handlers
protected function handleCheckoutCreated(array $webhookData): array
{
$checkout = $webhookData['data']['object'];
// Update local subscription with checkout ID
Subscription::where('stripe_id', $checkout['id'])->update([
'provider_data' => [
'checkout_id' => $checkout['id'],
'customer_id' => $checkout['customer_id'],
'polar_checkout' => $checkout,
],
]);
return [
'event_type' => 'checkout.created',
'processed' => true,
'data' => [
'checkout_id' => $checkout['id'],
'customer_id' => $checkout['customer_id'],
],
];
}
protected function handleSubscriptionCreated(array $webhookData): array
{
$polarSubscription = $webhookData['data']['object'];
// Find and update local subscription
$localSubscription = Subscription::where('provider', 'polar')
->where('provider_subscription_id', $polarSubscription['checkout_id'])
->first();
if ($localSubscription) {
$localSubscription->update([
'stripe_id' => $polarSubscription['id'],
'provider_subscription_id' => $polarSubscription['id'],
'status' => $polarSubscription['status'],
'starts_at' => Carbon::parse($polarSubscription['current_period_start']),
'ends_at' => Carbon::parse($polarSubscription['current_period_end']),
'provider_data' => array_merge($localSubscription->provider_data ?? [], [
'polar_subscription' => $polarSubscription,
'activated_at' => now()->toISOString(),
]),
]);
}
return [
'event_type' => 'subscription.created',
'processed' => true,
'data' => [
'subscription_id' => $polarSubscription['id'],
'status' => $polarSubscription['status'],
],
];
}
protected function handleSubscriptionUpdated(array $webhookData): array
{
$polarSubscription = $webhookData['data']['object'];
Subscription::where('provider', 'polar')
->where('provider_subscription_id', $polarSubscription['id'])
->update([
'status' => $polarSubscription['status'],
'provider_data' => [
'polar_subscription' => $polarSubscription,
'updated_at' => now()->toISOString(),
],
]);
return [
'event_type' => 'subscription.updated',
'processed' => true,
'data' => [
'subscription_id' => $polarSubscription['id'],
'status' => $polarSubscription['status'],
],
];
}
protected function handleSubscriptionCancelled(array $webhookData): array
{
$polarSubscription = $webhookData['data']['object'];
Subscription::where('provider', 'polar')
->where('provider_subscription_id', $polarSubscription['id'])
->update([
'status' => 'cancelled',
'cancelled_at' => now(),
'cancellation_reason' => 'Polar webhook cancellation',
]);
return [
'event_type' => 'subscription.cancelled',
'processed' => true,
'data' => [
'subscription_id' => $polarSubscription['id'],
],
];
}
protected function handleSubscriptionPaused(array $webhookData): array
{
$polarSubscription = $webhookData['data']['object'];
Subscription::where('provider', 'polar')
->where('provider_subscription_id', $polarSubscription['id'])
->update([
'status' => 'paused',
'paused_at' => now(),
]);
return [
'event_type' => 'subscription.paused',
'processed' => true,
'data' => [
'subscription_id' => $polarSubscription['id'],
],
];
}
protected function handleSubscriptionResumed(array $webhookData): array
{
$polarSubscription = $webhookData['data']['object'];
Subscription::where('provider', 'polar')
->where('provider_subscription_id', $polarSubscription['id'])
->update([
'status' => 'active',
'resumed_at' => now(),
]);
return [
'event_type' => 'subscription.resumed',
'processed' => true,
'data' => [
'subscription_id' => $polarSubscription['id'],
],
];
}
// Additional interface methods
public function getSubscriptionMetadata(Subscription $subscription): array
{
return $subscription->provider_data['polar_subscription'] ?? [];
}
public function updateSubscriptionMetadata(Subscription $subscription, array $metadata): bool
{
try {
$subscription->update([
'provider_data' => array_merge($subscription->provider_data ?? [], [
'metadata' => $metadata,
]),
]);
return true;
} catch (\Exception $e) {
Log::error('Failed to update Polar subscription metadata', [
'subscription_id' => $subscription->id,
'error' => $e->getMessage(),
]);
return false;
}
}
public function startTrial(Subscription $subscription, int $trialDays): bool
{
// Polar handles trials through checkout creation
// This would require creating a new checkout with trial period
return false;
}
public function applyCoupon(Subscription $subscription, string $couponCode): array
{
// Polar supports discount codes
try {
$polarSubscriptionId = $this->getPolarSubscriptionId($subscription);
if (! $polarSubscriptionId) {
throw new \Exception('No Polar subscription found');
}
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
'Content-Type' => 'application/json',
])->post($this->apiBaseUrl.'/v1/subscriptions/'.$polarSubscriptionId.'/discount', [
'coupon_code' => $couponCode,
]);
if (! $response->successful()) {
throw new \Exception('Failed to apply Polar coupon: '.$response->body());
}
return $response->json();
} catch (\Exception $e) {
Log::error('Polar coupon application failed', [
'subscription_id' => $subscription->id,
'coupon_code' => $couponCode,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function removeCoupon(Subscription $subscription): bool
{
try {
$polarSubscriptionId = $this->getPolarSubscriptionId($subscription);
if (! $polarSubscriptionId) {
return false;
}
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
])->delete($this->apiBaseUrl.'/v1/subscriptions/'.$polarSubscriptionId.'/discount');
return $response->successful();
} catch (\Exception $e) {
Log::error('Polar coupon removal failed', [
'subscription_id' => $subscription->id,
'error' => $e->getMessage(),
]);
return false;
}
}
public function getUpcomingInvoice(Subscription $subscription): array
{
try {
$polarSubscriptionId = $this->getPolarSubscriptionId($subscription);
if (! $polarSubscriptionId) {
return [
'amount_due' => 0,
'currency' => 'USD',
'next_payment_date' => null,
];
}
$response = Http::withHeaders([
'Authorization' => 'Bearer '.$this->config['api_key'],
])->get($this->apiBaseUrl.'/v1/subscriptions/'.$polarSubscriptionId.'/upcoming-invoice');
if (! $response->successful()) {
throw new \Exception('Failed to retrieve Polar upcoming invoice: '.$response->body());
}
$invoice = $response->json();
return [
'amount_due' => $invoice['amount_due'] / 100, // Convert from cents
'currency' => $invoice['currency'],
'next_payment_date' => $invoice['next_payment_date'],
];
} catch (\Exception $e) {
Log::error('Polar upcoming invoice retrieval failed', [
'subscription_id' => $subscription->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
public function retryFailedPayment(Subscription $subscription): array
{
// Polar doesn't have explicit retry logic - payments are retried automatically
return $this->syncSubscriptionStatus($subscription);
}
public function canModifySubscription(Subscription $subscription): bool
{
try {
$details = $this->getSubscriptionDetails($subscription->provider_subscription_id);
return in_array($details['status'], ['active', 'trialing']);
} catch (\Exception $e) {
return false;
}
}
public function getCancellationTerms(Subscription $subscription): array
{
return [
'immediate_cancellation' => true,
'refund_policy' => 'no_pro_rated_refunds',
'cancellation_effective' => 'immediately',
'billing_cycle_proration' => false,
];
}
public function exportSubscriptionData(Subscription $subscription): array
{
return [
'provider' => 'polar',
'provider_subscription_id' => $subscription->provider_subscription_id,
'data' => $subscription->provider_data,
];
}
public function importSubscriptionData(User $user, array $subscriptionData): array
{
throw new \Exception('Import to Polar payments not implemented');
}
}