validate([ 'plan_id' => 'required|exists:plans,id', 'provider' => 'nullable|string|in:stripe,lemon_squeezy,polar,oxapay,crypto,activation_key', 'options' => 'nullable|array', 'is_trial' => 'nullable|boolean', ]); $user = $request->user(); $plan = Plan::with(['planProviders', 'trialConfiguration'])->findOrFail($validated['plan_id']); $provider = $validated['provider'] ?? null; $options = $validated['options'] ?? []; $isTrial = $validated['is_trial'] ?? false; // Validate provider support if ($provider && ! $plan?->supportsProvider($provider)) { return response()->json([ 'success' => false, 'error' => "Provider '{$provider}' is not supported for this plan.", ], 400); } // Validate trial requirements if ($isTrial) { if (! $plan?->hasTrial()) { return response()->json([ 'success' => false, 'error' => 'This plan does not offer trials.', ], 400); } $trialConfig = $plan?->getTrialConfig(); if ($trialConfig && $trialConfig->trial_requires_payment_method && ! $provider) { return response()->json([ 'success' => false, 'error' => 'Payment method is required for trial. Please specify a provider.', ], 400); } } // Enhance options with plan-specific data $options = array_merge($options, [ 'is_trial' => $isTrial, 'plan_features' => $plan?->getFeaturesWithLimits() ?? [], 'billing_cycle' => $plan?->getBillingCycleDisplay() ?? 'Unknown', 'plan_tier' => $plan?->planTier?->name, ]); if (! $plan) { return response()->json([ 'success' => false, 'error' => 'Plan not found.', ], 404); } $result = $this->orchestrator->createCheckoutSession($user, $plan, $provider, $options); return response()->json([ 'success' => true, 'data' => $result, ]); } catch (ValidationException $e) { return response()->json([ 'success' => false, 'errors' => $e->errors(), ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'error' => $e->getMessage(), ], 500); } } /** * Create a new subscription */ public function createSubscription(Request $request): JsonResponse { try { $validated = $request->validate([ 'plan_id' => 'required|exists:plans,id', 'provider' => 'nullable|string|in:stripe,lemon_squeezy,polar', 'options' => 'nullable|array', ]); $user = $request->user(); $plan = Plan::findOrFail($validated['plan_id']); $provider = $validated['provider'] ?? null; $options = $validated['options'] ?? []; // Only recurring providers can create subscriptions if (! $plan?->monthly_billing) { return response()->json([ 'success' => false, 'error' => 'This plan does not support recurring subscriptions. Use checkout instead.', ], 400); } if (! $plan) { return response()->json([ 'success' => false, 'error' => 'Plan not found.', ], 404); } $result = $this->orchestrator->createSubscription($user, $plan, $provider, $options); return response()->json([ 'success' => true, 'data' => $result, ]); } catch (ValidationException $e) { return response()->json([ 'success' => false, 'errors' => $e->errors(), ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'error' => $e->getMessage(), ], 500); } } /** * Get available payment methods for a plan */ public function getPaymentMethods(Request $request): JsonResponse { try { $validated = $request->validate([ 'plan_id' => 'required|exists:plans,id', ]); $plan = Plan::findOrFail($validated['plan_id']); if (! $plan) { return response()->json([ 'success' => false, 'error' => 'Plan not found.', ], 404); } $providers = $this->orchestrator->getActiveProvidersForPlan($plan); $methods = $providers->map(function ($provider) use ($plan) { return [ 'provider' => $provider->getName(), 'name' => $provider->getName(), 'supports_recurring' => $provider->supportsRecurring(), 'supports_one_time' => $provider->supportsOneTime(), 'supported_currencies' => $provider->getSupportedCurrencies(), 'fees' => $provider->calculateFees($plan?->price ?? 0), 'active' => $provider->isActive(), ]; })->values()->toArray(); return response()->json([ 'success' => true, 'data' => [ 'plan' => [ 'id' => $plan?->id, 'name' => $plan?->name, 'price' => $plan?->price, 'monthly_billing' => $plan?->monthly_billing, ], 'payment_methods' => $methods, ], ]); } catch (ValidationException $e) { return response()->json([ 'success' => false, 'errors' => $e->errors(), ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'error' => $e->getMessage(), ], 500); } } /** * Get user's payment/subscription history */ public function getHistory(Request $request): JsonResponse { try { $validated = $request->validate([ 'limit' => 'nullable|integer|min:1|max:100', 'offset' => 'nullable|integer|min:0', 'filters' => 'nullable|array', ]); $user = $request->user(); $limit = $validated['limit'] ?? 20; $filters = $validated['filters'] ?? []; $history = $this->orchestrator->getTransactionHistory($user, $filters); // Apply pagination $offset = $validated['offset'] ?? 0; $paginatedHistory = array_slice($history, $offset, $limit); return response()->json([ 'success' => true, 'data' => [ 'transactions' => $paginatedHistory, 'pagination' => [ 'total' => count($history), 'limit' => $limit, 'offset' => $offset, 'has_more' => $offset + $limit < count($history), ], ], ]); } catch (ValidationException $e) { return response()->json([ 'success' => false, 'errors' => $e->errors(), ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'error' => $e->getMessage(), ], 500); } } /** * Handle successful payment redirect */ public function success(Request $request): JsonResponse { return response()->json([ 'status' => 'success', 'message' => 'Payment completed successfully', ]); } /** * Handle cancelled payment redirect */ public function cancel(Request $request): JsonResponse { return response()->json([ 'status' => 'cancelled', 'message' => 'Payment was cancelled', ]); } /** * Handle payment provider webhooks */ public function webhook(Request $request, string $provider): JsonResponse { try { $result = $this->orchestrator->processWebhook($provider, $request); return response()->json([ 'status' => 'processed', 'result' => $result, ]); } catch (\Exception $e) { return response()->json([ 'status' => 'error', 'message' => $e->getMessage(), ], 400); } } /** * Start a trial subscription */ public function startTrial(Request $request): JsonResponse { try { $validated = $request->validate([ 'plan_id' => 'required|exists:plans,id', 'provider' => 'required|string|in:stripe,lemon_squeezy,polar', 'options' => 'nullable|array', ]); $user = $request->user(); $plan = Plan::with(['trialConfiguration', 'planProviders'])->findOrFail($validated['plan_id']); $provider = $validated['provider']; $options = $validated['options'] ?? []; // Validate trial availability if (! $plan?->hasTrial()) { return response()->json([ 'success' => false, 'error' => 'This plan does not offer trials.', ], 400); } // Validate provider support if (! $plan?->supportsProvider($provider)) { return response()->json([ 'success' => false, 'error' => "Provider '{$provider}' is not supported for this plan.", ], 400); } $trialConfig = $plan?->getTrialConfig(); // Check if user already has an active trial for this plan $existingTrial = $user->subscriptions() ->where('plan_id', $plan?->id) ->where('status', 'trialing') ->first(); if ($existingTrial) { return response()->json([ 'success' => false, 'error' => 'You already have an active trial for this plan.', ], 400); } if (! $plan) { return response()->json([ 'success' => false, 'error' => 'Plan not found.', ], 404); } // Create trial subscription using checkout session with trial options $trialOptions = array_merge($options, [ 'is_trial' => true, 'trial_duration_days' => $trialConfig?->trial_duration_days ?? 14, 'trial_requires_payment_method' => $trialConfig?->trial_requires_payment_method ?? true, ]); $result = $this->orchestrator->createCheckoutSession($user, $plan, $provider, $trialOptions); return response()->json([ 'success' => true, 'data' => $result, ]); } catch (ValidationException $e) { return response()->json([ 'success' => false, 'errors' => $e->errors(), ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'error' => $e->getMessage(), ], 500); } } /** * Get plan comparison data */ public function getPlanComparison(Request $request): JsonResponse { try { $validated = $request->validate([ 'plan_ids' => 'required|array|min:1|max:5', 'plan_ids.*' => 'exists:plans,id', ]); $plans = Plan::with([ 'planFeatureLimits.planFeature', 'planProviders', 'trialConfiguration', 'planTier', ]) ->whereIn('id', $validated['plan_ids']) ->active() ->ordered() ->get(); $comparison = $plans->map(function ($plan) { $trialConfig = $plan?->getTrialConfig(); return [ 'id' => $plan?->id, 'name' => $plan?->name, 'description' => $plan?->description, 'price' => $plan?->price, 'billing_cycle' => $plan?->getBillingCycleDisplay() ?? 'Unknown', 'tier' => $plan?->planTier?->name, 'providers' => $plan?->getAllowedProviders() ?? [], 'features' => $plan?->getFeaturesWithLimits() ?? [], 'trial' => $plan?->hasTrial() ? [ 'available' => true, 'duration_days' => $trialConfig?->trial_duration_days ?? 0, 'requires_payment_method' => $trialConfig?->trial_requires_payment_method ?? false, ] : ['available' => false], ]; }); return response()->json([ 'success' => true, 'data' => $comparison, ]); } catch (ValidationException $e) { return response()->json([ 'success' => false, 'errors' => $e->errors(), ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'error' => $e->getMessage(), ], 500); } } /** * Get upgrade paths for user's current subscription */ public function getUpgradePaths(Request $request): JsonResponse { try { $user = $request->user(); $currentSubscription = $user->subscription('default'); if (! $currentSubscription) { return response()->json([ 'success' => false, 'error' => 'No active subscription found.', ], 404); } $currentPlan = $currentSubscription->plan; $upgradePaths = Plan::with(['planTier', 'planFeatureLimits.planFeature']) ->where('id', '!=', $currentPlan->id) ->where(function ($query) use ($currentPlan) { $query->where('price', '>', $currentPlan->price) ->orWhereHas('planTier', function ($q) use ($currentPlan) { $q->where('sort_order', '>', $currentPlan->planTier?->sort_order ?? 0); }); }) ->active() ->ordered() ->get(); $paths = $upgradePaths->map(function ($plan) use ($currentPlan) { return [ 'plan' => [ 'id' => $plan->id, 'name' => $plan->name, 'price' => $plan->price, 'billing_cycle' => $plan->getBillingCycleDisplay(), 'tier' => $plan->planTier?->name, ], 'upgrade_benefits' => $this->getUpgradeBenefits($currentPlan, $plan), 'providers' => $plan->getAllowedProviders(), 'can_migrate' => true, ]; }); return response()->json([ 'success' => true, 'data' => [ 'current_plan' => [ 'id' => $currentPlan->id, 'name' => $currentPlan->name, 'price' => $currentPlan->price, ], 'upgrade_paths' => $paths, ], ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'error' => $e->getMessage(), ], 500); } } /** * Get upgrade benefits between two plans */ private function getUpgradeBenefits(Plan $currentPlan, Plan $newPlan): array { $currentFeatures = $currentPlan->getFeaturesWithLimits(); $newFeatures = $newPlan->getFeaturesWithLimits(); $benefits = []; foreach ($newFeatures as $featureData) { $feature = $featureData['feature']; $newLimit = $featureData['limit']; $currentLimit = collect($currentFeatures) ->firstWhere('feature.id', $feature->id)['limit'] ?? null; if (! $currentLimit || $this->isUpgradeBenefit($currentLimit, $newLimit)) { $benefits[] = [ 'feature' => $feature->display_name, 'from' => $this->formatLimitDisplay($currentLimit), 'to' => $this->formatLimitDisplay($newLimit), 'improvement' => $this->getImprovementType($currentLimit, $newLimit), ]; } } return $benefits; } /** * Check if a limit change is an upgrade benefit */ private function isUpgradeBenefit($currentLimit, $newLimit): bool { if (! $currentLimit) { return true; } if (! $newLimit) { return false; } // Boolean upgrades if ($newLimit->limit_type === 'boolean') { return ! $currentLimit->limit_value && $newLimit->limit_value; } // Numeric upgrades if ($newLimit->limit_type === 'numeric') { return ($newLimit->limit_value ?? 0) > ($currentLimit->limit_value ?? 0); } return false; } /** * Format limit value for display */ private function formatLimitDisplay($limit): string { if (! $limit) { return 'Not Available'; } if ($limit->limit_type === 'boolean') { return $limit->limit_value ? 'Enabled' : 'Disabled'; } if ($limit->limit_type === 'numeric') { return $limit->limit_value ? (string) $limit->limit_value : 'Unlimited'; } return 'Limited'; } /** * Get improvement type for upgrade benefit */ private function getImprovementType($currentLimit, $newLimit): string { if (! $currentLimit) { return 'New Feature'; } if ($newLimit->limit_type === 'boolean') { return 'Enabled'; } if ($newLimit->limit_type === 'numeric') { return 'Increased Limit'; } return 'Improved'; } }