From eebfdd3d00612d11bbbd60216dbb03f59dda3aa8 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 11 Sep 2025 12:42:21 -0400 Subject: [PATCH] subscription flow --- .../create_customer_portal_session.rs | 15 +++++- ..._create_customer_portal_session_handler.rs | 53 +++++++++++++++++-- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/crates/schema/public/api/artcraft_api_defs/src/stripe_artcraft/create_customer_portal_session.rs b/crates/schema/public/api/artcraft_api_defs/src/stripe_artcraft/create_customer_portal_session.rs index dbe3bb1f8d..69171e1442 100644 --- a/crates/schema/public/api/artcraft_api_defs/src/stripe_artcraft/create_customer_portal_session.rs +++ b/crates/schema/public/api/artcraft_api_defs/src/stripe_artcraft/create_customer_portal_session.rs @@ -1,13 +1,26 @@ -use enums::common::artcraft_credits_pack_slug::ArtcraftCreditsPackSlug; use serde_derive::{Deserialize, Serialize}; use utoipa::ToSchema; pub const CREATE_CUSTOMER_PORTAL_URL_PATH: &str = "/v1/stripe_artcraft/portal/create_session"; +/// See https://docs.stripe.com/customer-management/portal-deep-links +#[derive(Serialize, Deserialize, ToSchema, Copy, Clone)] +pub enum StripeArtcraftCreateCustomerPortalFlowState { + // subscription_update + SubscriptionUpdate, + + // subscription_cancel + SubscriptionCancel, +} + #[derive(Serialize, Deserialize, ToSchema)] pub struct StripeArtcraftCreateCustomerPortalSessionRequest { // TODO: Not sure if this is needed pub portal_config_id: Option, + + /// An optional deep link into the customer portal + /// This sets the initial state of the portal + pub flow: Option, } #[derive(Serialize, Deserialize, ToSchema)] diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/stripe_artcraft_create_customer_portal_session_handler.rs b/crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/stripe_artcraft_create_customer_portal_session_handler.rs index 27faa2bdfb..31891f4e38 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/stripe_artcraft_create_customer_portal_session_handler.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/stripe_artcraft_create_customer_portal_session_handler.rs @@ -5,7 +5,7 @@ use crate::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient; use crate::utils::common_web_error::CommonWebError; use actix_web::web::{Data, Json}; use actix_web::{web, HttpRequest}; -use artcraft_api_defs::stripe_artcraft::create_customer_portal_session::{StripeArtcraftCreateCustomerPortalSessionRequest, StripeArtcraftCreateCustomerPortalSessionResponse}; +use artcraft_api_defs::stripe_artcraft::create_customer_portal_session::{StripeArtcraftCreateCustomerPortalFlowState, StripeArtcraftCreateCustomerPortalSessionRequest, StripeArtcraftCreateCustomerPortalSessionResponse}; use artcraft_api_defs::stripe_artcraft::create_subscription_checkout::{PlanBillingCadence, StripeArtcraftCreateSubscriptionCheckoutRequest, StripeArtcraftCreateSubscriptionCheckoutResponse}; use component_traits::traits::internal_user_lookup::InternalUserLookup; use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug; @@ -17,7 +17,7 @@ use sqlx::MySqlPool; use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; -use stripe_billing::billing_portal_session::CreateBillingPortalSession; +use stripe_billing::billing_portal_session::{CreateBillingPortalSession, CreateBillingPortalSessionFlowData, CreateBillingPortalSessionFlowDataAfterCompletion, CreateBillingPortalSessionFlowDataAfterCompletionRedirect, CreateBillingPortalSessionFlowDataAfterCompletionType, CreateBillingPortalSessionFlowDataSubscriptionCancel, CreateBillingPortalSessionFlowDataSubscriptionCancelRetention, CreateBillingPortalSessionFlowDataSubscriptionCancelRetentionCouponOffer, CreateBillingPortalSessionFlowDataSubscriptionCancelRetentionType, CreateBillingPortalSessionFlowDataSubscriptionUpdate, CreateBillingPortalSessionFlowDataType}; use stripe_billing::BillingPortalSession; use stripe_checkout::checkout_session::{CreateCheckoutSession, CreateCheckoutSessionAutomaticTax, CreateCheckoutSessionLineItems, CreateCheckoutSessionSubscriptionData}; use stripe_checkout::CheckoutSessionMode; @@ -88,11 +88,54 @@ pub async fn stripe_artcraft_create_customer_portal_session_handler( Ok(Some(subscription)) => subscription, }; - // TODO: Set the configuration id. - - let portal_builder = CreateBillingPortalSession::new(subscription.stripe_customer_id) + // TODO: Set the configuration id. + + let mut portal_builder = CreateBillingPortalSession::new(subscription.stripe_customer_id) .return_url(stripe_config.portal_return_url.clone()); + match request.flow { + None => {} + Some(StripeArtcraftCreateCustomerPortalFlowState::SubscriptionCancel) => { + portal_builder = portal_builder.flow_data(CreateBillingPortalSessionFlowData { + type_: CreateBillingPortalSessionFlowDataType::SubscriptionCancel, + subscription_cancel: Some( + CreateBillingPortalSessionFlowDataSubscriptionCancel { + retention: None, + subscription: subscription.stripe_subscription_id.clone(), + } + ), + after_completion: Some(CreateBillingPortalSessionFlowDataAfterCompletion { + type_: CreateBillingPortalSessionFlowDataAfterCompletionType::Redirect, + redirect: Some(CreateBillingPortalSessionFlowDataAfterCompletionRedirect { + return_url: stripe_config.portal_return_url.clone(), // TODO: This can be a different URL. + }), + hosted_confirmation: None, + }), + subscription_update: None, + subscription_update_confirm: None, + }); + } + Some(StripeArtcraftCreateCustomerPortalFlowState::SubscriptionUpdate) => { + portal_builder = portal_builder.flow_data(CreateBillingPortalSessionFlowData { + type_: CreateBillingPortalSessionFlowDataType::SubscriptionUpdate, + subscription_update: Some( + CreateBillingPortalSessionFlowDataSubscriptionUpdate { + subscription: subscription.stripe_subscription_id.clone(), + } + ), + after_completion: Some(CreateBillingPortalSessionFlowDataAfterCompletion { + type_: CreateBillingPortalSessionFlowDataAfterCompletionType::Redirect, + redirect: Some(CreateBillingPortalSessionFlowDataAfterCompletionRedirect { + return_url: stripe_config.portal_return_url.clone(), // TODO: This can be a different URL. + }), + hosted_confirmation: None, + }), + subscription_cancel: None, + subscription_update_confirm: None, + }); + } + } + let portal_session = portal_builder .send(&stripe_config.client) .await