Allow users to stop a previously scheduled cancelation of their Zed Pro plan (#15562)

Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-07-31 13:36:46 -07:00 committed by GitHub
parent 3f1c091b87
commit 1b2d4ee132
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 62 additions and 6 deletions

View file

@ -432,7 +432,8 @@ CREATE TABLE IF NOT EXISTS billing_subscriptions (
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
billing_customer_id INTEGER NOT NULL REFERENCES billing_customers(id),
stripe_subscription_id TEXT NOT NULL,
stripe_subscription_status TEXT NOT NULL
stripe_subscription_status TEXT NOT NULL,
stripe_cancel_at TIMESTAMP
);
CREATE INDEX "ix_billing_subscriptions_on_billing_customer_id" ON billing_subscriptions (billing_customer_id);

View file

@ -0,0 +1 @@
ALTER TABLE billing_subscriptions ADD COLUMN stripe_cancel_at TIMESTAMP WITHOUT TIME ZONE;

View file

@ -8,6 +8,7 @@ use axum::{
routing::{get, post},
Extension, Json, Router,
};
use chrono::{DateTime, SecondsFormat};
use reqwest::StatusCode;
use sea_orm::ActiveValue;
use serde::{Deserialize, Serialize};
@ -17,7 +18,7 @@ use stripe::{
CreateBillingPortalSessionFlowDataAfterCompletionRedirect,
CreateBillingPortalSessionFlowDataType, CreateCheckoutSession, CreateCheckoutSessionLineItems,
CreateCustomer, Customer, CustomerId, EventObject, EventType, Expandable, ListEvents,
SubscriptionStatus,
Subscription, SubscriptionId, SubscriptionStatus,
};
use util::ResultExt;
@ -51,6 +52,7 @@ struct BillingSubscriptionJson {
id: BillingSubscriptionId,
name: String,
status: StripeSubscriptionStatus,
cancel_at: Option<String>,
/// Whether this subscription can be canceled.
is_cancelable: bool,
}
@ -79,7 +81,13 @@ async fn list_billing_subscriptions(
id: subscription.id,
name: "Zed Pro".to_string(),
status: subscription.stripe_subscription_status,
is_cancelable: subscription.stripe_subscription_status.is_cancelable(),
cancel_at: subscription.stripe_cancel_at.map(|cancel_at| {
cancel_at
.and_utc()
.to_rfc3339_opts(SecondsFormat::Millis, true)
}),
is_cancelable: subscription.stripe_subscription_status.is_cancelable()
&& subscription.stripe_cancel_at.is_none(),
})
.collect(),
}))
@ -157,11 +165,13 @@ async fn create_billing_subscription(
}))
}
#[derive(Debug, Deserialize)]
#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all = "snake_case")]
enum ManageSubscriptionIntent {
/// The user intends to cancel their subscription.
Cancel,
/// The user intends to stop the cancelation of their subscription.
StopCancelation,
}
#[derive(Debug, Deserialize)]
@ -174,7 +184,7 @@ struct ManageBillingSubscriptionBody {
#[derive(Debug, Serialize)]
struct ManageBillingSubscriptionResponse {
billing_portal_session_url: String,
billing_portal_session_url: Option<String>,
}
/// Initiates a Stripe customer portal session for managing a billing subscription.
@ -210,6 +220,40 @@ async fn manage_billing_subscription(
.await?
.ok_or_else(|| anyhow!("subscription not found"))?;
if body.intent == ManageSubscriptionIntent::StopCancelation {
let subscription_id = SubscriptionId::from_str(&subscription.stripe_subscription_id)
.context("failed to parse subscription ID")?;
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::Cancel => CreateBillingPortalSessionFlowData {
type_: CreateBillingPortalSessionFlowDataType::SubscriptionCancel,
@ -228,6 +272,7 @@ async fn manage_billing_subscription(
),
..Default::default()
},
ManageSubscriptionIntent::StopCancelation => unreachable!(),
};
let mut params = CreateBillingPortalSession::new(customer_id);
@ -237,7 +282,7 @@ async fn manage_billing_subscription(
let session = BillingPortalSession::create(&stripe_client, params).await?;
Ok(Json(ManageBillingSubscriptionResponse {
billing_portal_session_url: session.url,
billing_portal_session_url: Some(session.url),
}))
}
@ -443,6 +488,12 @@ async fn handle_customer_subscription_event(
billing_customer_id: ActiveValue::set(billing_customer.id),
stripe_subscription_id: ActiveValue::set(subscription.id.to_string()),
stripe_subscription_status: ActiveValue::set(subscription.status.into()),
stripe_cancel_at: ActiveValue::set(
subscription
.cancel_at
.and_then(|cancel_at| DateTime::from_timestamp(cancel_at, 0))
.map(|time| time.naive_utc()),
),
},
)
.await?;

View file

@ -14,6 +14,7 @@ pub struct UpdateBillingSubscriptionParams {
pub billing_customer_id: ActiveValue<BillingCustomerId>,
pub stripe_subscription_id: ActiveValue<String>,
pub stripe_subscription_status: ActiveValue<StripeSubscriptionStatus>,
pub stripe_cancel_at: ActiveValue<Option<DateTime>>,
}
impl Database {
@ -49,6 +50,7 @@ impl Database {
billing_customer_id: params.billing_customer_id.clone(),
stripe_subscription_id: params.stripe_subscription_id.clone(),
stripe_subscription_status: params.stripe_subscription_status.clone(),
stripe_cancel_at: params.stripe_cancel_at.clone(),
..Default::default()
})
.exec(&*tx)

View file

@ -11,6 +11,7 @@ pub struct Model {
pub billing_customer_id: BillingCustomerId,
pub stripe_subscription_id: String,
pub stripe_subscription_status: StripeSubscriptionStatus,
pub stripe_cancel_at: Option<DateTime>,
pub created_at: DateTime,
}