user(); $plans = Plan::where('is_active', true)->get(); $currentPlan = $user->currentPlan; $subscriptionData = null; if ($user->subscribed('default')) { $subscription = $user->subscription('default'); $subscriptionData = [ 'status' => $subscription->stripe_status, 'on_trial' => $user->onTrial('default'), 'cancelled' => $subscription->canceled(), 'ends_at' => $subscription->ends_at, 'trial_ends_at' => $subscription->trial_ends_at, ]; } return $this->successResponse('Plans retrieved successfully', [ 'plans' => $plans, 'current_plan' => $currentPlan, 'subscription' => $subscriptionData, ]); } /** * Create Stripe Checkout Session. */ public function checkout(CheckoutRequest $request) { $user = $request->user(); $plan = Plan::findOrFail($request->plan_id); if (!$plan->is_active) { return $this->errorResponse('This plan is not available.', 422); } if ($plan->isFree()) { return $this->errorResponse('Free plan does not require payment.', 422); } if ($user->subscribed('default')) { if (!$user->onTrial('default')) { return $this->errorResponse('You already have an active subscription. Please cancel first or use swap endpoint.', 422); } try { $user->subscription('default')->cancelNow(); } catch (\Exception $e) { Log::warning('Failed to cancel trial subscription: ' . $e->getMessage()); } } try { $checkout = $user->newSubscription('default', $plan->stripe_price_id) ->trialDays(config('subscription.trial_days', 0)) ->checkout([ 'success_url' => config('app.frontend_url') . '/billing/success?session_id={CHECKOUT_SESSION_ID}', 'cancel_url' => config('app.frontend_url') . '/billing/cancel', 'metadata' => [ 'user_id' => $user->id, 'plan_id' => $plan->id, ] ]); return $this->successResponse('Checkout URL retrieved successfully', [ 'checkout_url' => $checkout->url, 'session_id' => $checkout->id, ]); } catch (ApiErrorException $e) { Log::error('Stripe checkout error: ' . $e->getMessage()); return $this->errorResponse('Payment service error. Please try again later.', 500); } catch (\Exception $e) { Log::error('Checkout error: ' . $e->getMessage()); return $this->errorResponse('Could not create checkout session. Please try again.', 500); } } /** * Get subscription status. */ public function status(Request $request) { $user = $request->user(); $user->ensureHasPlan(); $response = [ 'plan' => $user->currentPlan, 'has_subscription' => $user->subscribed('default'), 'on_trial' => $user->onTrial('default'), ]; if ($user->subscribed('default')) { $subscription = $user->subscription('default'); $response['subscription'] = [ 'stripe_id' => $subscription->stripe_id, 'stripe_status' => $subscription->stripe_status, 'stripe_price' => $subscription->stripe_price, 'quantity' => $subscription->quantity, 'trial_ends_at' => $subscription->trial_ends_at, 'ends_at' => $subscription->ends_at, 'created_at' => $subscription->created_at, 'updated_at' => $subscription->updated_at, 'cancelled' => $subscription->canceled(), 'on_grace_period' => $subscription->onGracePeriod(), 'ended' => $subscription->ended(), ]; if (!$subscription->canceled() && !$subscription->onGracePeriod()) { try { $nextPayment = $subscription->asStripeSubscription()->current_period_end; $response['subscription']['next_payment_at'] = date('Y-m-d H:i:s', $nextPayment); } catch (\Exception $e) { Log::warning('Failed to retrieve next payment date: ' . $e->getMessage()); } } } return $this->successResponse('Subscription status retrieved successfully', $response); } /** * Change plan. */ public function swap(SwapPlanRequest $request) { $user = $request->user(); $plan = Plan::findOrFail($request->plan_id); if (!$plan->is_active) { return $this->errorResponse('This plan is not available.', 422); } if ($plan->isFree()) { return $this->errorResponse('Cannot swap to free plan. Please cancel your subscription first.', 422); } if (!$user->subscribed('default')) { return $this->errorResponse('No active subscription found.', 404); } if ($user->onTrial('default')) { return $this->errorResponse('Cannot swap plan during trial period. Please wait for trial to end or cancel first.', 422); } $currentPlan = $user->currentPlan; if ($currentPlan->stripe_price_id === $plan->stripe_price_id) { return $this->errorResponse('You are already subscribed to this plan.', 422); } try { $subscription = $user->subscription('default'); if ($subscription->onGracePeriod()) { $subscription->resume(); } $subscription->swap($plan->stripe_price_id); return $this->successResponse('Plan changed successfully', [ 'new_plan' => $plan, 'effective_date' => now()->toDateTimeString(), ]); } catch (ApiErrorException $e) { Log::error('Stripe swap error: ' . $e->getMessage()); return $this->errorResponse('Payment service error. Please try again later.', 500); } catch (\Exception $e) { Log::error('Swap plan error: ' . $e->getMessage()); return $this->errorResponse('Could not change plan. Please try again.', 500); } } /** * Cancel subscription. */ public function cancel(Request $request) { $user = $request->user(); if ($user->currentPlan->isFree()) { return $this->errorResponse('Free plan cannot be cancelled.', 422); } if (!$user->subscribed('default')) { return $this->errorResponse('No active subscription found.', 404); } $subscription = $user->subscription('default'); if ($subscription->canceled()) { return $this->errorResponse('Subscription is already cancelled.', 422); } try { $subscription->cancel(); return $this->successResponse('Subscription cancelled successfully. You will have access until ' . $subscription->ends_at->format('Y-m-d'), [ 'ends_at' => $subscription->ends_at, 'on_grace_period' => true, ]); } catch (\Exception $e) { Log::error('Cancel subscription error: ' . $e->getMessage()); return $this->errorResponse('Could not cancel subscription. Please try again.', 500); } } /** * Resume subscription. */ public function resume(Request $request) { $user = $request->user(); if ($user->currentPlan->isFree()) { return $this->errorResponse('Cannot resume free plan.', 422); } if (!$user->subscribed('default')) { return $this->errorResponse('No active subscription found.', 404); } $subscription = $user->subscription('default'); if (!$subscription->canceled() || !$subscription->onGracePeriod()) { return $this->errorResponse('Subscription cannot be resumed. It is either active or already ended.', 422); } try { $subscription->resume(); return $this->successResponse('Subscription resumed successfully', [ 'resumes_at' => now()->toDateTimeString(), ]); } catch (\Exception $e) { Log::error('Resume subscription error: ' . $e->getMessage()); return $this->errorResponse('Could not resume subscription. Please try again.', 500); } } /** * Get user invoices. */ public function invoices(Request $request) { $user = $request->user(); if (!$user->subscribed('default')) { return $this->errorResponse('No active subscription found.', 404); } try { if (!$user->stripe_id) { return $this->successResponse('No invoices found', [ 'invoices' => [], 'count' => 0 ]); } $invoices = $user->invoices(); $formattedInvoices = $invoices->map(function ($invoice) { $currency = strtoupper($invoice->currency ?? 'USD'); return [ 'id' => $invoice->id, 'number' => $invoice->number, 'date' => $invoice->date()->toDateTimeString(), 'amount' => $invoice->total(), 'currency' => $currency, 'status' => $invoice->status, 'hosted_invoice_url' => $invoice->hosted_invoice_url, ]; }); return $this->successResponse('Invoices fetched successfully', [ 'invoices' => $formattedInvoices, 'count' => $formattedInvoices->count(), ]); } catch (ApiErrorException $e) { Log::error('Stripe invoices error: ' . $e->getMessage()); return $this->errorResponse('Could not fetch invoices from payment service.', 500); } catch (\Exception $e) { Log::error('Invoices fetch error: ' . $e->getMessage()); return $this->errorResponse('Could not fetch invoices. Please try again.', 500); } } /** * Get billing portal URL. */ public function billingPortal(Request $request) { $user = $request->user(); if (!$user->subscribed('default')) { return $this->errorResponse('No active subscription found.', 404); } try { if (!$user->stripe_id) { return $this->errorResponse('No Stripe customer found. Please subscribe first.', 404); } $returnUrl = config('app.frontend_url') . '/dashboard'; $portalUrl = $user->billingPortalUrl($returnUrl); return $this->successResponse('Billing portal link created successfully', [ 'billing_portal_url' => $portalUrl, ]); } catch (ApiErrorException $e) { Log::error('Stripe billing portal error: ' . $e->getMessage()); return $this->errorResponse('Could not create billing portal session.', 500); } catch (\Exception $e) { Log::error('Billing portal error: ' . $e->getMessage()); return $this->errorResponse('Could not create billing portal link. Please try again.', 500); } } }