collab: Allow starting a trial from Zed Free (#30970)

This PR makes it so a user can initiate a checkout session for a Zed Pro
trial while on the Zed Free plan.

Release Notes:

- N/A

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Marshall Bowers 2025-05-19 15:39:01 -04:00 committed by GitHub
parent 5c4f9e57d8
commit b440e1a467
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 56 additions and 29 deletions

View file

@ -280,7 +280,7 @@ async fn list_billing_subscriptions(
})) }))
} }
#[derive(Debug, Clone, Copy, Deserialize)] #[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
enum ProductCode { enum ProductCode {
ZedPro, ZedPro,
@ -325,11 +325,16 @@ async fn create_billing_subscription(
))? ))?
}; };
if app.db.has_active_billing_subscription(user.id).await? { if let Some(existing_subscription) = app.db.get_active_billing_subscription(user.id).await? {
return Err(Error::http( let is_checkout_allowed = body.product == ProductCode::ZedProTrial
StatusCode::CONFLICT, && existing_subscription.kind == Some(SubscriptionKind::ZedFree);
"user already has an active subscription".into(),
)); if !is_checkout_allowed {
return Err(Error::http(
StatusCode::CONFLICT,
"user already has an active subscription".into(),
));
}
} }
let existing_billing_customer = app.db.get_billing_customer_by_user_id(user.id).await?; let existing_billing_customer = app.db.get_billing_customer_by_user_id(user.id).await?;
@ -1136,31 +1141,51 @@ async fn sync_subscription(
) )
.await?; .await?;
} else { } else {
// If the user already has an active billing subscription, ignore the if let Some(existing_subscription) = app
// event and return an `Ok` to signal that it was processed
// successfully.
//
// There is the possibility that this could cause us to not create a
// subscription in the following scenario:
//
// 1. User has an active subscription A
// 2. User cancels subscription A
// 3. User creates a new subscription B
// 4. We process the new subscription B before the cancellation of subscription A
// 5. User ends up with no subscriptions
//
// In theory this situation shouldn't arise as we try to process the events in the order they occur.
if app
.db .db
.has_active_billing_subscription(billing_customer.user_id) .get_active_billing_subscription(billing_customer.user_id)
.await? .await?
{ {
log::info!( if existing_subscription.kind == Some(SubscriptionKind::ZedFree)
"user {user_id} already has an active subscription, skipping creation of subscription {subscription_id}", && subscription_kind == Some(SubscriptionKind::ZedProTrial)
user_id = billing_customer.user_id, {
subscription_id = subscription.id let stripe_subscription_id = existing_subscription
); .stripe_subscription_id
return Ok(billing_customer); .parse::<stripe::SubscriptionId>()
.context("failed to parse Stripe subscription ID from database")?;
Subscription::cancel(
&stripe_client,
&stripe_subscription_id,
stripe::CancelSubscription {
invoice_now: None,
..Default::default()
},
)
.await?;
} else {
// If the user already has an active billing subscription, ignore the
// event and return an `Ok` to signal that it was processed
// successfully.
//
// There is the possibility that this could cause us to not create a
// subscription in the following scenario:
//
// 1. User has an active subscription A
// 2. User cancels subscription A
// 3. User creates a new subscription B
// 4. We process the new subscription B before the cancellation of subscription A
// 5. User ends up with no subscriptions
//
// In theory this situation shouldn't arise as we try to process the events in the order they occur.
log::info!(
"user {user_id} already has an active subscription, skipping creation of subscription {subscription_id}",
user_id = billing_customer.user_id,
subscription_id = subscription.id
);
return Ok(billing_customer);
}
} }
app.db app.db

View file

@ -236,7 +236,9 @@ impl Database {
.filter( .filter(
billing_customer::Column::UserId.eq(user_id).and( billing_customer::Column::UserId.eq(user_id).and(
billing_subscription::Column::StripeSubscriptionStatus billing_subscription::Column::StripeSubscriptionStatus
.eq(StripeSubscriptionStatus::Active), .eq(StripeSubscriptionStatus::Active)
.or(billing_subscription::Column::StripeSubscriptionStatus
.eq(StripeSubscriptionStatus::Trialing)),
), ),
) )
.count(&*tx) .count(&*tx)