$plan['id'], 'slug' => $plan['slug'], 'name' => $plan['name'], 'description' => $plan['description'], 'price_cents' => $plan['price_cents'], 'currency' => $plan['currency'], 'interval' => $plan['billing_interval'], 'features' => $plan['features'], 'checkout_ready' => !empty($plan['stripe_price_id']), ]; } } jsonResponse([ 'subscriptions_enabled' => $subscriptionsEnabled, 'can_manage' => $hasExistingBilling, 'plans' => $availablePlans, 'user' => userPublicPayload($user), ]); case 'checkout': if (!$subscriptionsEnabled) jsonResponse(['error' => 'New subscriptions are not currently available'], 403); if (in_array($user['subscription_status'] ?? 'free', ['active', 'trialing', 'past_due'], true)) { jsonResponse(['error' => 'Use Manage in Stripe to change an existing subscription'], 409); } $planId = (int)($_POST['plan_id'] ?? 0); $plan = planById($planId); if (!$plan || !$plan['active'] || $plan['slug'] === 'free') jsonResponse(['error' => 'Plan not available'], 404); try { jsonResponse(['success' => true, 'url' => createStripeCheckout($user, $plan)]); } catch (Throwable $e) { jsonResponse(['error' => $e->getMessage()], 400); } case 'portal': if (!$hasExistingBilling) jsonResponse(['error' => 'No subscription is connected'], 404); try { jsonResponse(['success' => true, 'url' => createStripePortal($user)]); } catch (Throwable $e) { jsonResponse(['error' => $e->getMessage()], 400); } case 'cancel': if (empty($user['stripe_subscription_id'])) jsonResponse(['error' => 'No active subscription'], 404); try { $subscription = stripeRequest('POST', 'subscriptions/' . rawurlencode($user['stripe_subscription_id']), [ 'cancel_at_period_end' => 'true', ]); syncSubscriptionFromStripe($subscription); jsonResponse(['success' => true, 'user' => userPublicPayload(userById((int)$user['id']))]); } catch (Throwable $e) { jsonResponse(['error' => $e->getMessage()], 400); } default: jsonResponse(['error' => 'Unknown action'], 400); }