@@ -5,16 +5,8 @@ use collections::{HashMap, HashSet};
use reqwest::StatusCode;
use sea_orm::ActiveValue;
use serde::{Deserialize, Serialize};
-use std::{str::FromStr, sync::Arc, time::Duration};
-use stripe::{
- BillingPortalSession, CancellationDetailsReason, CreateBillingPortalSession,
- CreateBillingPortalSessionFlowData, CreateBillingPortalSessionFlowDataAfterCompletion,
- CreateBillingPortalSessionFlowDataAfterCompletionRedirect,
- CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirm,
- CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirmItems,
- CreateBillingPortalSessionFlowDataType, CustomerId, EventObject, EventType, ListEvents,
- PaymentMethod, Subscription, SubscriptionId, SubscriptionStatus,
-};
+use std::{sync::Arc, time::Duration};
+use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus};
use util::{ResultExt, maybe};
use zed_llm_client::LanguageModelProvider;
@@ -31,7 +23,7 @@ use crate::{AppState, Error, Result};
use crate::{db::UserId, llm::db::LlmDatabase};
use crate::{
db::{
- BillingSubscriptionId, CreateBillingCustomerParams, CreateBillingSubscriptionParams,
+ CreateBillingCustomerParams, CreateBillingSubscriptionParams,
CreateProcessedStripeEventParams, UpdateBillingCustomerParams,
UpdateBillingSubscriptionParams, billing_customer,
},
@@ -39,260 +31,10 @@ use crate::{
};
pub fn router() -> Router {
- Router::new()
- .route(
- "/billing/subscriptions/manage",
- post(manage_billing_subscription),
- )
- .route(
- "/billing/subscriptions/sync",
- post(sync_billing_subscription),
- )
-}
-
-#[derive(Debug, PartialEq, Deserialize)]
-#[serde(rename_all = "snake_case")]
-enum ManageSubscriptionIntent {
- /// The user intends to manage their subscription.
- ///
- /// This will open the Stripe billing portal without putting the user in a specific flow.
- ManageSubscription,
- /// The user intends to update their payment method.
- UpdatePaymentMethod,
- /// The user intends to upgrade to Zed Pro.
- UpgradeToPro,
- /// The user intends to cancel their subscription.
- Cancel,
- /// The user intends to stop the cancellation of their subscription.
- StopCancellation,
-}
-
-#[derive(Debug, Deserialize)]
-struct ManageBillingSubscriptionBody {
- github_user_id: i32,
- intent: ManageSubscriptionIntent,
- /// The ID of the subscription to manage.
- subscription_id: BillingSubscriptionId,
- redirect_to: Option<String>,
-}
-
-#[derive(Debug, Serialize)]
-struct ManageBillingSubscriptionResponse {
- billing_portal_session_url: Option<String>,
-}
-
-/// Initiates a Stripe customer portal session for managing a billing subscription.
-async fn manage_billing_subscription(
- Extension(app): Extension<Arc<AppState>>,
- extract::Json(body): extract::Json<ManageBillingSubscriptionBody>,
-) -> Result<Json<ManageBillingSubscriptionResponse>> {
- let user = app
- .db
- .get_user_by_github_user_id(body.github_user_id)
- .await?
- .context("user not found")?;
-
- let Some(stripe_client) = app.real_stripe_client.clone() else {
- log::error!("failed to retrieve Stripe client");
- Err(Error::http(
- StatusCode::NOT_IMPLEMENTED,
- "not supported".into(),
- ))?
- };
-
- let Some(stripe_billing) = app.stripe_billing.clone() else {
- log::error!("failed to retrieve Stripe billing object");
- Err(Error::http(
- StatusCode::NOT_IMPLEMENTED,
- "not supported".into(),
- ))?
- };
-
- let customer = app
- .db
- .get_billing_customer_by_user_id(user.id)
- .await?
- .context("billing customer not found")?;
- let customer_id = CustomerId::from_str(&customer.stripe_customer_id)
- .context("failed to parse customer ID")?;
-
- let subscription = app
- .db
- .get_billing_subscription_by_id(body.subscription_id)
- .await?
- .context("subscription not found")?;
- let subscription_id = SubscriptionId::from_str(&subscription.stripe_subscription_id)
- .context("failed to parse subscription ID")?;
-
- if body.intent == ManageSubscriptionIntent::StopCancellation {
- let updated_stripe_subscription = Subscription::update(
- &stripe_client,
- &subscription_id,
- stripe::UpdateSubscription {
- cancel_at_period_end: Some(false),
- ..Default::default()
- },
- )
- .await?;
-
- app.db
- .update_billing_subscription(
- subscription.id,
- &UpdateBillingSubscriptionParams {
- stripe_cancel_at: ActiveValue::set(
- updated_stripe_subscription
- .cancel_at
- .and_then(|cancel_at| DateTime::from_timestamp(cancel_at, 0))
- .map(|time| time.naive_utc()),
- ),
- ..Default::default()
- },
- )
- .await?;
-
- return Ok(Json(ManageBillingSubscriptionResponse {
- billing_portal_session_url: None,
- }));
- }
-
- let flow = match body.intent {
- ManageSubscriptionIntent::ManageSubscription => None,
- ManageSubscriptionIntent::UpgradeToPro => {
- let zed_pro_price_id: stripe::PriceId =
- stripe_billing.zed_pro_price_id().await?.try_into()?;
- let zed_free_price_id: stripe::PriceId =
- stripe_billing.zed_free_price_id().await?.try_into()?;
-
- let stripe_subscription =
- Subscription::retrieve(&stripe_client, &subscription_id, &[]).await?;
-
- let is_on_zed_pro_trial = stripe_subscription.status == SubscriptionStatus::Trialing
- && stripe_subscription.items.data.iter().any(|item| {
- item.price
- .as_ref()
- .map_or(false, |price| price.id == zed_pro_price_id)
- });
- if is_on_zed_pro_trial {
- let payment_methods = PaymentMethod::list(
- &stripe_client,
- &stripe::ListPaymentMethods {
- customer: Some(stripe_subscription.customer.id()),
- ..Default::default()
- },
- )
- .await?;
-
- let has_payment_method = !payment_methods.data.is_empty();
- if !has_payment_method {
- return Err(Error::http(
- StatusCode::BAD_REQUEST,
- "missing payment method".into(),
- ));
- }
-
- // If the user is already on a Zed Pro trial and wants to upgrade to Pro, we just need to end their trial early.
- Subscription::update(
- &stripe_client,
- &stripe_subscription.id,
- stripe::UpdateSubscription {
- trial_end: Some(stripe::Scheduled::now()),
- ..Default::default()
- },
- )
- .await?;
-
- return Ok(Json(ManageBillingSubscriptionResponse {
- billing_portal_session_url: None,
- }));
- }
-
- let subscription_item_to_update = stripe_subscription
- .items
- .data
- .iter()
- .find_map(|item| {
- let price = item.price.as_ref()?;
-
- if price.id == zed_free_price_id {
- Some(item.id.clone())
- } else {
- None
- }
- })
- .context("No subscription item to update")?;
-
- Some(CreateBillingPortalSessionFlowData {
- type_: CreateBillingPortalSessionFlowDataType::SubscriptionUpdateConfirm,
- subscription_update_confirm: Some(
- CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirm {
- subscription: subscription.stripe_subscription_id,
- items: vec![
- CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirmItems {
- id: subscription_item_to_update.to_string(),
- price: Some(zed_pro_price_id.to_string()),
- quantity: Some(1),
- },
- ],
- discounts: None,
- },
- ),
- ..Default::default()
- })
- }
- ManageSubscriptionIntent::UpdatePaymentMethod => Some(CreateBillingPortalSessionFlowData {
- type_: CreateBillingPortalSessionFlowDataType::PaymentMethodUpdate,
- after_completion: Some(CreateBillingPortalSessionFlowDataAfterCompletion {
- type_: stripe::CreateBillingPortalSessionFlowDataAfterCompletionType::Redirect,
- redirect: Some(CreateBillingPortalSessionFlowDataAfterCompletionRedirect {
- return_url: format!(
- "{}{path}",
- app.config.zed_dot_dev_url(),
- path = body.redirect_to.unwrap_or_else(|| "/account".to_string())
- ),
- }),
- ..Default::default()
- }),
- ..Default::default()
- }),
- ManageSubscriptionIntent::Cancel => {
- if subscription.kind == Some(SubscriptionKind::ZedFree) {
- return Err(Error::http(
- StatusCode::BAD_REQUEST,
- "free subscription cannot be canceled".into(),
- ));
- }
-
- Some(CreateBillingPortalSessionFlowData {
- type_: CreateBillingPortalSessionFlowDataType::SubscriptionCancel,
- after_completion: Some(CreateBillingPortalSessionFlowDataAfterCompletion {
- type_: stripe::CreateBillingPortalSessionFlowDataAfterCompletionType::Redirect,
- redirect: Some(CreateBillingPortalSessionFlowDataAfterCompletionRedirect {
- return_url: format!("{}/account", app.config.zed_dot_dev_url()),
- }),
- ..Default::default()
- }),
- subscription_cancel: Some(
- stripe::CreateBillingPortalSessionFlowDataSubscriptionCancel {
- subscription: subscription.stripe_subscription_id,
- retention: None,
- },
- ),
- ..Default::default()
- })
- }
- ManageSubscriptionIntent::StopCancellation => unreachable!(),
- };
-
- let mut params = CreateBillingPortalSession::new(customer_id);
- params.flow_data = flow;
- let return_url = format!("{}/account", app.config.zed_dot_dev_url());
- params.return_url = Some(&return_url);
-
- let session = BillingPortalSession::create(&stripe_client, params).await?;
-
- Ok(Json(ManageBillingSubscriptionResponse {
- billing_portal_session_url: Some(session.url),
- }))
+ Router::new().route(
+ "/billing/subscriptions/sync",
+ post(sync_billing_subscription),
+ )
}
#[derive(Debug, Deserialize)]