diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index a43146b938..b6bff810b0 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -438,7 +438,8 @@ CREATE TABLE IF NOT EXISTS billing_subscriptions ( billing_customer_id INTEGER NOT NULL REFERENCES billing_customers(id), stripe_subscription_id TEXT NOT NULL, stripe_subscription_status TEXT NOT NULL, - stripe_cancel_at TIMESTAMP + stripe_cancel_at TIMESTAMP, + stripe_cancellation_reason TEXT ); CREATE INDEX "ix_billing_subscriptions_on_billing_customer_id" ON billing_subscriptions (billing_customer_id); diff --git a/crates/collab/migrations/20250108184547_add_stripe_cancellation_reason_to_billing_subscriptions.sql b/crates/collab/migrations/20250108184547_add_stripe_cancellation_reason_to_billing_subscriptions.sql new file mode 100644 index 0000000000..31686f56bb --- /dev/null +++ b/crates/collab/migrations/20250108184547_add_stripe_cancellation_reason_to_billing_subscriptions.sql @@ -0,0 +1,2 @@ +alter table billing_subscriptions +add column stripe_cancellation_reason text; diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index 2a866879a9..9d720147ff 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -12,8 +12,8 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use std::{str::FromStr, sync::Arc, time::Duration}; use stripe::{ - BillingPortalSession, CreateBillingPortalSession, CreateBillingPortalSessionFlowData, - CreateBillingPortalSessionFlowDataAfterCompletion, + BillingPortalSession, CancellationDetailsReason, CreateBillingPortalSession, + CreateBillingPortalSessionFlowData, CreateBillingPortalSessionFlowDataAfterCompletion, CreateBillingPortalSessionFlowDataAfterCompletionRedirect, CreateBillingPortalSessionFlowDataType, CreateCustomer, Customer, CustomerId, EventObject, EventType, Expandable, ListEvents, Subscription, SubscriptionId, SubscriptionStatus, @@ -21,8 +21,10 @@ use stripe::{ use util::ResultExt; use crate::api::events::SnowflakeRow; +use crate::db::billing_subscription::{StripeCancellationReason, StripeSubscriptionStatus}; use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT}; use crate::rpc::{ResultExt as _, Server}; +use crate::{db::UserId, llm::db::LlmDatabase}; use crate::{ db::{ billing_customer, BillingSubscriptionId, CreateBillingCustomerParams, @@ -32,10 +34,6 @@ use crate::{ }, stripe_billing::StripeBilling, }; -use crate::{ - db::{billing_subscription::StripeSubscriptionStatus, UserId}, - llm::db::LlmDatabase, -}; use crate::{AppState, Cents, Error, Result}; pub fn router() -> Router { @@ -679,6 +677,12 @@ async fn handle_customer_subscription_event( .and_then(|cancel_at| DateTime::from_timestamp(cancel_at, 0)) .map(|time| time.naive_utc()), ), + stripe_cancellation_reason: ActiveValue::set( + subscription + .cancellation_details + .and_then(|details| details.reason) + .map(|reason| reason.into()), + ), }, ) .await?; @@ -791,6 +795,16 @@ impl From for StripeSubscriptionStatus { } } +impl From for StripeCancellationReason { + fn from(value: CancellationDetailsReason) -> Self { + match value { + CancellationDetailsReason::CancellationRequested => Self::CancellationRequested, + CancellationDetailsReason::PaymentDisputed => Self::PaymentDisputed, + CancellationDetailsReason::PaymentFailed => Self::PaymentFailed, + } + } +} + /// Finds or creates a billing customer using the provided customer. async fn find_or_create_billing_customer( app: &Arc, diff --git a/crates/collab/src/db/queries/billing_subscriptions.rs b/crates/collab/src/db/queries/billing_subscriptions.rs index 53a17f9c53..027f46f6b7 100644 --- a/crates/collab/src/db/queries/billing_subscriptions.rs +++ b/crates/collab/src/db/queries/billing_subscriptions.rs @@ -1,4 +1,4 @@ -use crate::db::billing_subscription::StripeSubscriptionStatus; +use crate::db::billing_subscription::{StripeCancellationReason, StripeSubscriptionStatus}; use super::*; @@ -15,6 +15,7 @@ pub struct UpdateBillingSubscriptionParams { pub stripe_subscription_id: ActiveValue, pub stripe_subscription_status: ActiveValue, pub stripe_cancel_at: ActiveValue>, + pub stripe_cancellation_reason: ActiveValue>, } impl Database { @@ -51,6 +52,7 @@ impl Database { stripe_subscription_id: params.stripe_subscription_id.clone(), stripe_subscription_status: params.stripe_subscription_status.clone(), stripe_cancel_at: params.stripe_cancel_at.clone(), + stripe_cancellation_reason: params.stripe_cancellation_reason.clone(), ..Default::default() }) .exec(&*tx) diff --git a/crates/collab/src/db/tables/billing_subscription.rs b/crates/collab/src/db/tables/billing_subscription.rs index 5eb0e427de..1ed2bfce84 100644 --- a/crates/collab/src/db/tables/billing_subscription.rs +++ b/crates/collab/src/db/tables/billing_subscription.rs @@ -12,6 +12,7 @@ pub struct Model { pub stripe_subscription_id: String, pub stripe_subscription_status: StripeSubscriptionStatus, pub stripe_cancel_at: Option, + pub stripe_cancellation_reason: Option, pub created_at: DateTime, } @@ -73,3 +74,18 @@ impl StripeSubscriptionStatus { } } } + +/// The cancellation reason for a Stripe subscription. +/// +/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-cancellation_details-reason) +#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)] +#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] +#[serde(rename_all = "snake_case")] +pub enum StripeCancellationReason { + #[sea_orm(string_value = "cancellation_requested")] + CancellationRequested, + #[sea_orm(string_value = "payment_disputed")] + PaymentDisputed, + #[sea_orm(string_value = "payment_failed")] + PaymentFailed, +}