providerRegistry = $providerRegistry; $this->logger = $logger; } /** * Create a new subscription using the preferred provider */ public function createSubscription(User $user, Plan $plan, ?string $providerName = null, array $options = []): array { $provider = $this->getProviderForPlan($plan, $providerName); if (! $provider->isActive()) { throw new Exception("Payment provider {$provider->getName()} is not active"); } try { $this->logger->logEvent('subscription_creation_started', [ 'user_id' => $user->id, 'plan_id' => $plan->id, 'provider' => $provider->getName(), 'options' => $options, ]); $result = $provider->createSubscription($user, $plan, $options); // Create local subscription record $subscription = $this->createLocalSubscription($user, $plan, $provider, $result); $this->logger->logEvent('subscription_created', [ 'subscription_id' => $subscription->id, 'provider' => $provider->getName(), 'provider_subscription_id' => $result['provider_subscription_id'] ?? null, ]); return [ 'success' => true, 'subscription' => $subscription, 'provider_data' => $result, ]; } catch (Exception $e) { $this->logger->logError('subscription_creation_failed', [ 'user_id' => $user->id, 'plan_id' => $plan->id, 'provider' => $provider->getName(), 'error' => $e->getMessage(), ]); throw $e; } } /** * Cancel a subscription */ public function cancelSubscription(Subscription $subscription, string $reason = ''): bool { $provider = $this->getProviderForSubscription($subscription); try { $this->logger->logEvent('subscription_cancellation_started', [ 'subscription_id' => $subscription->id, 'provider' => $provider->getName(), 'reason' => $reason, ]); $result = $provider->cancelSubscription($subscription, $reason); if ($result) { $subscription->update([ 'ends_at' => now(), 'cancelled_at' => now(), 'cancellation_reason' => $reason, ]); $this->logger->logEvent('subscription_cancelled', [ 'subscription_id' => $subscription->id, 'provider' => $provider->getName(), ]); } return $result; } catch (Exception $e) { $this->logger->logError('subscription_cancellation_failed', [ 'subscription_id' => $subscription->id, 'provider' => $provider->getName(), 'error' => $e->getMessage(), ]); throw $e; } } /** * Update subscription plan */ public function updateSubscription(Subscription $subscription, Plan $newPlan): array { $provider = $this->getProviderForSubscription($subscription); try { $this->logger->logEvent('subscription_update_started', [ 'subscription_id' => $subscription->id, 'old_plan_id' => $subscription->plan_id, 'new_plan_id' => $newPlan->id, 'provider' => $provider->getName(), ]); $result = $provider->updateSubscription($subscription, $newPlan); $subscription->update([ 'plan_id' => $newPlan->id, 'updated_at' => now(), ]); $this->logger->logEvent('subscription_updated', [ 'subscription_id' => $subscription->id, 'new_plan_id' => $newPlan->id, 'provider' => $provider->getName(), ]); return [ 'success' => true, 'subscription' => $subscription->fresh(), 'provider_data' => $result, ]; } catch (Exception $e) { $this->logger->logError('subscription_update_failed', [ 'subscription_id' => $subscription->id, 'provider' => $provider->getName(), 'error' => $e->getMessage(), ]); throw $e; } } /** * Create checkout session */ public function createCheckoutSession(User $user, Plan $plan, ?string $providerName = null, array $options = []): array { $provider = $this->getProviderForPlan($plan, $providerName); try { $this->logger->logEvent('checkout_session_created', [ 'user_id' => $user->id, 'plan_id' => $plan->id, 'provider' => $provider->getName(), ]); return $provider->createCheckoutSession($user, $plan, $options); } catch (Exception $e) { $this->logger->logError('checkout_session_failed', [ 'user_id' => $user->id, 'plan_id' => $plan->id, 'provider' => $provider->getName(), 'error' => $e->getMessage(), ]); throw $e; } } /** * Create subscription with coupon */ public function createSubscriptionWithCoupon(User $user, Plan $plan, Coupon $coupon, ?string $providerName = null, array $options = []): array { $provider = $this->getProviderForPlan($plan, $providerName); if (! $coupon->isValid($user)) { throw new Exception("Coupon {$coupon->code} is not valid for this user"); } try { $this->logger->logEvent('coupon_subscription_creation_started', [ 'user_id' => $user->id, 'plan_id' => $plan->id, 'coupon_id' => $coupon->id, 'provider' => $provider->getName(), ]); // Add coupon to options $options['coupon'] = $coupon->code; $options['discount_amount'] = $this->calculateDiscountAmount($plan, $coupon); $result = $provider->createSubscription($user, $plan, $options); $subscription = $this->createLocalSubscription($user, $plan, $provider, $result); // Apply coupon to subscription $couponUsage = $subscription->applyCoupon($coupon, $options['discount_amount']); $this->logger->logEvent('coupon_subscription_created', [ 'subscription_id' => $subscription->id, 'coupon_id' => $coupon->id, 'coupon_usage_id' => $couponUsage->id, 'provider' => $provider->getName(), ]); return [ 'success' => true, 'subscription' => $subscription, 'coupon_usage' => $couponUsage, 'provider_data' => $result, ]; } catch (Exception $e) { $this->logger->logError('coupon_subscription_creation_failed', [ 'user_id' => $user->id, 'plan_id' => $plan->id, 'coupon_id' => $coupon->id, 'provider' => $provider->getName(), 'error' => $e->getMessage(), ]); throw $e; } } /** * Extend subscription trial */ public function extendTrial(Subscription $subscription, int $days, string $reason = '', string $extensionType = 'manual', ?User $grantedBy = null): array { $provider = $this->getProviderForSubscription($subscription); if (! $subscription->isOnTrial()) { throw new Exception("Subscription {$subscription->id} is not on trial"); } try { $this->logger->logEvent('trial_extension_started', [ 'subscription_id' => $subscription->id, 'days' => $days, 'reason' => $reason, 'extension_type' => $extensionType, 'granted_by' => $grantedBy?->id, ]); // Create trial extension record $trialExtension = $subscription->extendTrial($days, $reason, $extensionType, $grantedBy); // Update provider if supported if (method_exists($provider, 'extendTrial')) { $provider->extendTrial($subscription, $days, $reason); } $this->logger->logEvent('trial_extended', [ 'subscription_id' => $subscription->id, 'trial_extension_id' => $trialExtension->id, 'new_trial_ends_at' => $trialExtension->new_trial_ends_at, ]); return [ 'success' => true, 'trial_extension' => $trialExtension, 'subscription' => $subscription->fresh(), ]; } catch (Exception $e) { $this->logger->logError('trial_extension_failed', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); throw $e; } } /** * Record subscription change */ public function recordSubscriptionChange(Subscription $subscription, string $changeType, string $description, ?array $oldValues = null, ?array $newValues = null, ?string $reason = null): SubscriptionChange { try { $this->logger->logEvent('subscription_change_recorded', [ 'subscription_id' => $subscription->id, 'change_type' => $changeType, 'description' => $description, ]); return $subscription->recordChange($changeType, $description, $oldValues, $newValues, $reason); } catch (Exception $e) { $this->logger->logError('subscription_change_recording_failed', [ 'subscription_id' => $subscription->id, 'change_type' => $changeType, 'error' => $e->getMessage(), ]); throw $e; } } /** * Process pending subscription changes */ public function processPendingChanges(Subscription $subscription): array { try { $pendingChanges = $subscription->getPendingChanges(); $processedCount = 0; $errors = []; foreach ($pendingChanges as $change) { try { $this->processSubscriptionChange($subscription, $change); $change->markAsProcessed(); $processedCount++; } catch (Exception $e) { $errors[] = [ 'change_id' => $change->id, 'error' => $e->getMessage(), ]; $this->logger->logError('subscription_change_processing_failed', [ 'change_id' => $change->id, 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); } } $this->logger->logEvent('pending_changes_processed', [ 'subscription_id' => $subscription->id, 'processed_count' => $processedCount, 'error_count' => count($errors), ]); return [ 'success' => true, 'processed_count' => $processedCount, 'errors' => $errors, 'subscription' => $subscription->fresh(), ]; } catch (Exception $e) { $this->logger->logError('pending_changes_processing_failed', [ 'subscription_id' => $subscription->id, 'error' => $e->getMessage(), ]); throw $e; } } /** * Migrate subscription between providers */ public function migrateSubscription(Subscription $subscription, string $targetProvider): array { $sourceProvider = $this->getProviderForSubscription($subscription); $targetProviderInstance = $this->providerRegistry->get($targetProvider); if (! $targetProviderInstance) { throw new Exception("Target provider {$targetProvider} not found"); } if (! $targetProviderInstance->isActive()) { throw new Exception("Target provider {$targetProvider} is not active"); } try { $this->logger->logEvent('subscription_migration_started', [ 'subscription_id' => $subscription->id, 'source_provider' => $sourceProvider->getName(), 'target_provider' => $targetProvider, ]); // Record the change $this->recordSubscriptionChange( $subscription, 'migration', "Migrated from {$sourceProvider->getName()} to {$targetProvider}", ['provider' => $sourceProvider->getName()], ['provider' => $targetProvider], 'Provider migration for better service' ); // Cancel with source provider if needed if (method_exists($sourceProvider, 'cancelSubscription')) { $sourceProvider->cancelSubscription($subscription, 'Migration to new provider'); } // Create with target provider $newSubscriptionData = $targetProviderInstance->createSubscription( $subscription->user, $subscription->plan, ['migration' => true] ); // Update local subscription $subscription->update([ 'provider' => $targetProvider, 'provider_subscription_id' => $newSubscriptionData['provider_subscription_id'] ?? null, 'provider_data' => $newSubscriptionData, 'migration_batch_id' => uniqid('migration_', true), 'is_migrated' => true, 'last_provider_sync' => now(), ]); $this->logger->logEvent('subscription_migration_completed', [ 'subscription_id' => $subscription->id, 'migration_batch_id' => $subscription->migration_batch_id, 'target_provider' => $targetProvider, ]); return [ 'success' => true, 'subscription' => $subscription->fresh(), 'provider_data' => $newSubscriptionData, ]; } catch (Exception $e) { $this->logger->logError('subscription_migration_failed', [ 'subscription_id' => $subscription->id, 'source_provider' => $sourceProvider->getName(), 'target_provider' => $targetProvider, 'error' => $e->getMessage(), ]); throw $e; } } /** * Process webhook from any provider */ public function processWebhook(string $providerName, Request $request): array { $provider = $this->providerRegistry->get($providerName); if (! $provider) { $this->logger->logError('webhook_provider_not_found', [ 'provider' => $providerName, ]); throw new Exception("Payment provider {$providerName} not found"); } if (! $provider->validateWebhook($request)) { $this->logger->logError('webhook_validation_failed', [ 'provider' => $providerName, ]); throw new Exception("Webhook validation failed for {$providerName}"); } try { $result = $provider->processWebhook($request); $this->logger->logEvent('webhook_processed', [ 'provider' => $providerName, 'event_type' => $result['event_type'] ?? 'unknown', 'subscription_id' => $result['subscription_id'] ?? null, ]); return $result; } catch (Exception $e) { $this->logger->logError('webhook_processing_failed', [ 'provider' => $providerName, 'error' => $e->getMessage(), ]); throw $e; } } /** * Sync subscription status from provider */ public function syncSubscriptionStatus(Subscription $subscription): array { $provider = $this->getProviderForSubscription($subscription); try { $providerData = $provider->syncSubscriptionStatus($subscription); // Update local subscription based on provider data $this->updateLocalSubscriptionFromProvider($subscription, $providerData); $this->logger->logEvent('subscription_synced', [ 'subscription_id' => $subscription->id, 'provider' => $provider->getName(), 'status' => $providerData['status'] ?? 'unknown', ]); return $providerData; } catch (Exception $e) { $this->logger->logError('subscription_sync_failed', [ 'subscription_id' => $subscription->id, 'provider' => $provider->getName(), 'error' => $e->getMessage(), ]); throw $e; } } /** * Get active providers for a plan */ public function getActiveProvidersForPlan(Plan $plan): Collection { return $this->providerRegistry->getActiveProviders() ->filter(function ($provider) use ($plan) { return $this->isProviderSupportedForPlan($provider, $plan); }); } /** * Get subscription transaction history */ public function getTransactionHistoryOld(User $user, array $filters = []): array { $subscriptions = $user->subscriptions; $history = []; foreach ($subscriptions as $subscription) { $provider = $this->getProviderForSubscription($subscription); $providerHistory = $provider->getTransactionHistory($user, $filters); $history = array_merge($history, $providerHistory); } // Sort by date descending usort($history, function ($a, $b) { return strtotime($b['date']) - strtotime($a['date']); }); return $history; } public function getTransactionHistory(User $user, array $filters = []): array { $subscriptions = $user->subscriptions; $history = []; foreach ($subscriptions as $subscription) { $provider = $this->getProviderForSubscription($subscription); $providerHistory = $provider->getTransactionHistory($user, $filters); // Use array_push with spread operator (PHP 7.4+) or array unpacking array_push($history, ...$providerHistory); // Alternative: Direct array concatenation // foreach ($providerHistory as $transaction) { // $history[] = $transaction; // } } // Sort by date descending usort($history, function ($a, $b) { return strtotime($b['date']) - strtotime($a['date']); }); return $history; } /** * Get provider for a specific plan */ protected function getProviderForPlan(Plan $plan, ?string $providerName = null): PaymentProviderContract { Log::info('PaymentOrchestrator: Getting provider for plan', [ 'plan_id' => $plan->id, 'requested_provider' => $providerName, ]); if ($providerName) { $provider = $this->providerRegistry->get($providerName); Log::info('PaymentOrchestrator: Checking specific provider', [ 'provider_name' => $providerName, 'provider_exists' => $provider ? true : false, 'provider_active' => $provider?->isActive(), 'provider_supported' => $provider ? $this->isProviderSupportedForPlan($provider, $plan) : false, ]); if ($provider && $provider->isActive() && $this->isProviderSupportedForPlan($provider, $plan)) { Log::info('PaymentOrchestrator: Using requested provider', [ 'provider' => $providerName, ]); return $provider; } } // Find the first active provider that supports this plan foreach ($this->providerRegistry->getActiveProviders() as $provider) { Log::info('PaymentOrchestrator: Checking fallback provider', [ 'provider' => $provider->getName(), 'supported' => $this->isProviderSupportedForPlan($provider, $plan), ]); if ($this->isProviderSupportedForPlan($provider, $plan)) { Log::info('PaymentOrchestrator: Using fallback provider', [ 'provider' => $provider->getName(), ]); return $provider; } } throw new Exception("No active payment provider available for plan: {$plan->name}"); } /** * Get provider for existing subscription */ protected function getProviderForSubscription(Subscription $subscription): PaymentProviderContract { $providerName = $subscription->provider ?? 'stripe'; // Default to stripe for existing subscriptions $provider = $this->providerRegistry->get($providerName); if (! $provider) { throw new Exception("Payment provider {$providerName} not found for subscription {$subscription->id}"); } return $provider; } /** * Check if provider supports a specific plan */ protected function isProviderSupportedForPlan(PaymentProviderContract $provider, Plan $plan): bool { // Use the same approach as Plan::supportsProvider() - check database relationship $isSupported = $plan->planProviders() ->where('provider', $provider->getName()) ->where('is_enabled', true) ->exists(); if (! $isSupported) { return false; } // Check if provider supports the plan type if ($plan->monthly_billing && ! $provider->supportsRecurring()) { return false; } if (! $plan->monthly_billing && ! $provider->supportsOneTime()) { return false; } return true; } /** * Create local subscription record */ protected function createLocalSubscription(User $user, Plan $plan, PaymentProviderContract $provider, array $providerData): Subscription { return Subscription::create([ 'user_id' => $user->id, 'plan_id' => $plan->id, 'provider' => $provider->getName(), 'provider_subscription_id' => $providerData['provider_subscription_id'] ?? null, 'status' => $providerData['status'] ?? 'active', 'starts_at' => $providerData['starts_at'] ?? now(), 'ends_at' => $providerData['ends_at'] ?? null, 'trial_ends_at' => $providerData['trial_ends_at'] ?? null, 'provider_data' => $providerData, ]); } /** * Update local subscription from provider data */ protected function updateLocalSubscriptionFromProvider(Subscription $subscription, array $providerData): void { $subscription->update([ '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(), ]); } /** * Get all available providers */ public function getAvailableProviders(): Collection { return $this->providerRegistry->getAllProviders(); } /** * Get active providers only */ public function getActiveProviders(): Collection { return $this->providerRegistry->getActiveProviders(); } /** * Calculate discount amount for a coupon */ protected function calculateDiscountAmount(Plan $plan, Coupon $coupon): float { $planPrice = $plan->price ?? 0; return match ($coupon->discount_type) { 'percentage' => ($planPrice * $coupon->discount_value) / 100, 'fixed' => $coupon->discount_value, default => 0, }; } /** * Process individual subscription change */ protected function processSubscriptionChange(Subscription $subscription, SubscriptionChange $change): void { match ($change->change_type) { 'plan_upgrade', 'plan_downgrade' => $this->processPlanChange($subscription, $change), 'pause' => $this->processPauseChange($subscription, $change), 'resume' => $this->processResumeChange($subscription, $change), 'cancel' => $this->processCancelChange($subscription, $change), default => throw new Exception("Unknown change type: {$change->change_type}"), }; } /** * Process plan change */ protected function processPlanChange(Subscription $subscription, SubscriptionChange $change): void { $newPlanId = $change->new_values['plan_id'] ?? null; if (! $newPlanId) { throw new Exception('Plan ID not found in change values'); } $newPlan = Plan::findOrFail($newPlanId); $result = $this->updateSubscription($subscription, $newPlan); if (! $result['success']) { throw new Exception('Failed to update subscription plan'); } } /** * Process pause change */ protected function processPauseChange(Subscription $subscription, SubscriptionChange $change): void { $provider = $this->getProviderForSubscription($subscription); if (method_exists($provider, 'pauseSubscription')) { $provider->pauseSubscription($subscription); } $subscription->update([ 'status' => 'paused', 'paused_at' => now(), ]); } /** * Process resume change */ protected function processResumeChange(Subscription $subscription, SubscriptionChange $change): void { $provider = $this->getProviderForSubscription($subscription); if (method_exists($provider, 'resumeSubscription')) { $provider->resumeSubscription($subscription); } $subscription->update([ 'status' => 'active', 'resumed_at' => now(), ]); } /** * Process cancel change */ protected function processCancelChange(Subscription $subscription, SubscriptionChange $change): void { $reason = $change->new_values['reason'] ?? 'Scheduled cancellation'; $this->cancelSubscription($subscription, $reason); } /** * Get subscription analytics */ public function getSubscriptionAnalytics(array $filters = []): array { $query = Subscription::query(); if (isset($filters['provider'])) { $query->where('provider', $filters['provider']); } if (isset($filters['status'])) { $query->where('status', $filters['status']); } if (isset($filters['date_from'])) { $query->whereDate('created_at', '>=', $filters['date_from']); } if (isset($filters['date_to'])) { $query->whereDate('created_at', '<=', $filters['date_to']); } $totalSubscriptions = $query->count(); $activeSubscriptions = $query->where('status', 'active')->count(); $trialSubscriptions = $query->where('status', 'trialing')->count(); $cancelledSubscriptions = $query->where('status', 'cancelled')->count(); $mrr = $query->where('status', 'active') ->join('plans', 'subscriptions.plan_id', '=', 'plans.id') ->sum('plans.price'); $totalRevenue = $query->join('plans', 'subscriptions.plan_id', '=', 'plans.id') ->sum('plans.price'); return [ 'total_subscriptions' => $totalSubscriptions, 'active_subscriptions' => $activeSubscriptions, 'trial_subscriptions' => $trialSubscriptions, 'cancelled_subscriptions' => $cancelledSubscriptions, 'monthly_recurring_revenue' => $mrr, 'total_revenue' => $totalRevenue, 'churn_rate' => $totalSubscriptions > 0 ? ($cancelledSubscriptions / $totalSubscriptions) * 100 : 0, 'trial_conversion_rate' => $trialSubscriptions > 0 ? (($activeSubscriptions - $trialSubscriptions) / $trialSubscriptions) * 100 : 0, ]; } /** * Get coupon analytics */ public function getCouponAnalytics(array $filters = []): array { $query = CouponUsage::query(); if (isset($filters['date_from'])) { $query->whereDate('created_at', '>=', $filters['date_from']); } if (isset($filters['date_to'])) { $query->whereDate('created_at', '<=', $filters['date_to']); } $totalUsages = $query->count(); $totalDiscount = $query->sum('discount_amount'); $uniqueUsers = $query->distinct('user_id')->count('user_id'); $conversionRate = $uniqueUsers > 0 ? ($totalUsages / $uniqueUsers) * 100 : 0; $topCoupons = $query->join('coupons', 'coupon_usages.coupon_id', '=', 'coupons.id') ->select('coupons.code', 'coupons.discount_type', 'coupons.discount_value', DB::raw('COUNT(*) as usage_count'), DB::raw('SUM(coupon_usages.discount_amount) as total_discount')) ->groupBy('coupons.id', 'coupons.code', 'coupons.discount_type', 'coupons.discount_value') ->orderBy('usage_count', 'desc') ->limit(10) ->get(); return [ 'total_usages' => $totalUsages, 'total_discount_given' => $totalDiscount, 'unique_users' => $uniqueUsers, 'conversion_rate' => $conversionRate, 'top_performing_coupons' => $topCoupons->toArray(), ]; } /** * Get trial analytics */ public function getTrialAnalytics(array $filters = []): array { $query = TrialExtension::query(); if (isset($filters['date_from'])) { $query->whereDate('granted_at', '>=', $filters['date_from']); } if (isset($filters['date_to'])) { $query->whereDate('granted_at', '<=', $filters['date_to']); } $totalExtensions = $query->count(); $totalDaysExtended = $query->sum('extension_days'); $uniqueUsers = $query->distinct('user_id')->count('user_id'); $extensionTypes = $query->select('extension_type', DB::raw('COUNT(*) as count')) ->groupBy('extension_type') ->pluck('count', 'extension_type') ->toArray(); $commonReasons = $query->select('reason', DB::raw('COUNT(*) as count')) ->whereNotNull('reason') ->groupBy('reason') ->orderBy('count', 'desc') ->limit(5) ->pluck('count', 'reason') ->toArray(); return [ 'total_extensions' => $totalExtensions, 'total_days_extended' => $totalDaysExtended, 'unique_users' => $uniqueUsers, 'extension_types' => $extensionTypes, 'common_reasons' => $commonReasons, 'avg_extension_days' => $totalExtensions > 0 ? $totalDaysExtended / $totalExtensions : 0, ]; } }