From 2b019ff9e2e58701065de226c3d6fafd81d3e7a8 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 30 Jul 2024 21:17:35 -0400 Subject: [PATCH] collab: Add `GET /billing/subscriptions` endpoint (#15516) This PR adds a new `GET /billing/subscriptions` endpoint to collab for retrieving the subscriptions to display on the account settings page. Release Notes: - N/A --- crates/collab/src/api/billing.rs | 59 +++++++++++++++++-- .../src/db/tables/billing_subscription.rs | 19 +++++- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index d61f471e718de73623d5491515bcf5890c5510a2..0e4c85907b3adf30e601deec13720f67f9738e83 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -3,7 +3,11 @@ use std::sync::Arc; use std::time::Duration; use anyhow::{anyhow, bail, Context}; -use axum::{extract, routing::post, Extension, Json, Router}; +use axum::{ + extract::{self, Query}, + routing::{get, post}, + Extension, Json, Router, +}; use reqwest::StatusCode; use sea_orm::ActiveValue; use serde::{Deserialize, Serialize}; @@ -27,13 +31,60 @@ use crate::{AppState, Error, Result}; pub fn router() -> Router { Router::new() - .route("/billing/subscriptions", post(create_billing_subscription)) + .route( + "/billing/subscriptions", + get(list_billing_subscriptions).post(create_billing_subscription), + ) .route( "/billing/subscriptions/manage", post(manage_billing_subscription), ) } +#[derive(Debug, Deserialize)] +struct ListBillingSubscriptionsParams { + github_user_id: i32, +} + +#[derive(Debug, Serialize)] +struct BillingSubscriptionJson { + id: BillingSubscriptionId, + name: String, + status: StripeSubscriptionStatus, + /// Whether this subscription can be canceled. + is_cancelable: bool, +} + +#[derive(Debug, Serialize)] +struct ListBillingSubscriptionsResponse { + subscriptions: Vec, +} + +async fn list_billing_subscriptions( + Extension(app): Extension>, + Query(params): Query, +) -> Result> { + let user = app + .db + .get_user_by_github_user_id(params.github_user_id) + .await? + .ok_or_else(|| anyhow!("user not found"))?; + + let subscriptions = app.db.get_billing_subscriptions(user.id).await?; + + Ok(Json(ListBillingSubscriptionsResponse { + subscriptions: subscriptions + .into_iter() + .map(|subscription| BillingSubscriptionJson { + id: subscription.id, + name: "Zed Pro".to_string(), + status: subscription.stripe_subscription_status, + is_cancelable: subscription.stripe_subscription_status.is_cancelable(), + }) + .collect(), + })) +} + #[derive(Debug, Deserialize)] struct CreateBillingSubscriptionBody { github_user_id: i32, @@ -179,7 +230,7 @@ async fn manage_billing_subscription( after_completion: Some(CreateBillingPortalSessionFlowDataAfterCompletion { type_: stripe::CreateBillingPortalSessionFlowDataAfterCompletionType::Redirect, redirect: Some(CreateBillingPortalSessionFlowDataAfterCompletionRedirect { - return_url: "https://zed.dev/billing".into(), + return_url: "https://zed.dev/settings".into(), }), ..Default::default() }), @@ -195,7 +246,7 @@ async fn manage_billing_subscription( let mut params = CreateBillingPortalSession::new(customer_id); params.flow_data = Some(flow); - params.return_url = Some("https://zed.dev/billing"); + params.return_url = Some("https://zed.dev/settings"); let session = BillingPortalSession::create(&stripe_client, params).await?; diff --git a/crates/collab/src/db/tables/billing_subscription.rs b/crates/collab/src/db/tables/billing_subscription.rs index 4cbde6bec04869219a8670d72fe8cf028b7e6590..d5d61c3420ed3c8d1ab309cd984b45e46f3838a2 100644 --- a/crates/collab/src/db/tables/billing_subscription.rs +++ b/crates/collab/src/db/tables/billing_subscription.rs @@ -1,5 +1,6 @@ use crate::db::{BillingCustomerId, BillingSubscriptionId}; use sea_orm::entity::prelude::*; +use serde::Serialize; /// A billing subscription. #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] @@ -34,8 +35,11 @@ impl ActiveModelBehavior for ActiveModel {} /// The status of a Stripe subscription. /// /// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-status) -#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)] +#[derive( + Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash, Serialize, +)] #[sea_orm(rs_type = "String", db_type = "String(None)")] +#[serde(rename_all = "snake_case")] pub enum StripeSubscriptionStatus { #[default] #[sea_orm(string_value = "incomplete")] @@ -55,3 +59,16 @@ pub enum StripeSubscriptionStatus { #[sea_orm(string_value = "paused")] Paused, } + +impl StripeSubscriptionStatus { + pub fn is_cancelable(&self) -> bool { + match self { + Self::Trialing | Self::Active | Self::PastDue => true, + Self::Incomplete + | Self::IncompleteExpired + | Self::Canceled + | Self::Unpaid + | Self::Paused => false, + } + } +}