collab: Remove POST /billing/subscriptions/manage
endpoint (#34986)
This PR removes the `POST /billing/subscriptions/manage` endpoint, as it has been moved to `cloud.zed.dev`. Release Notes: - N/A
This commit is contained in:
parent
4a87397d37
commit
3d4266bb8f
1 changed files with 7 additions and 265 deletions
|
@ -5,16 +5,8 @@ use collections::{HashMap, HashSet};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use sea_orm::ActiveValue;
|
use sea_orm::ActiveValue;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{str::FromStr, sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
use stripe::{
|
use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus};
|
||||||
BillingPortalSession, CancellationDetailsReason, CreateBillingPortalSession,
|
|
||||||
CreateBillingPortalSessionFlowData, CreateBillingPortalSessionFlowDataAfterCompletion,
|
|
||||||
CreateBillingPortalSessionFlowDataAfterCompletionRedirect,
|
|
||||||
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirm,
|
|
||||||
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirmItems,
|
|
||||||
CreateBillingPortalSessionFlowDataType, CustomerId, EventObject, EventType, ListEvents,
|
|
||||||
PaymentMethod, Subscription, SubscriptionId, SubscriptionStatus,
|
|
||||||
};
|
|
||||||
use util::{ResultExt, maybe};
|
use util::{ResultExt, maybe};
|
||||||
use zed_llm_client::LanguageModelProvider;
|
use zed_llm_client::LanguageModelProvider;
|
||||||
|
|
||||||
|
@ -31,7 +23,7 @@ use crate::{AppState, Error, Result};
|
||||||
use crate::{db::UserId, llm::db::LlmDatabase};
|
use crate::{db::UserId, llm::db::LlmDatabase};
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{
|
db::{
|
||||||
BillingSubscriptionId, CreateBillingCustomerParams, CreateBillingSubscriptionParams,
|
CreateBillingCustomerParams, CreateBillingSubscriptionParams,
|
||||||
CreateProcessedStripeEventParams, UpdateBillingCustomerParams,
|
CreateProcessedStripeEventParams, UpdateBillingCustomerParams,
|
||||||
UpdateBillingSubscriptionParams, billing_customer,
|
UpdateBillingSubscriptionParams, billing_customer,
|
||||||
},
|
},
|
||||||
|
@ -39,262 +31,12 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn router() -> Router {
|
pub fn router() -> Router {
|
||||||
Router::new()
|
Router::new().route(
|
||||||
.route(
|
|
||||||
"/billing/subscriptions/manage",
|
|
||||||
post(manage_billing_subscription),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/billing/subscriptions/sync",
|
"/billing/subscriptions/sync",
|
||||||
post(sync_billing_subscription),
|
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),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct SyncBillingSubscriptionBody {
|
struct SyncBillingSubscriptionBody {
|
||||||
github_user_id: i32,
|
github_user_id: i32,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue