From 68e77fd7d3df01fa882db02910ef97d781cd2dab Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 16 Sep 2025 07:12:27 -0400 Subject: [PATCH] update payment method --- .../customer_portal_update_payment_method.rs | 17 +++ .../src/stripe_artcraft/mod.rs | 3 +- .../customer_portal_update_payment_method.rs | 18 +++ .../src/stripe_artcraft/mod.rs | 1 + .../commands/stripe_customer_portal/mod.rs | 3 +- ...pen_customer_portal_switch_plan_command.rs | 7 -- ...er_portal_update_payment_method_command.rs | 62 ++++++++++ .../open_storyteller_billing_window.rs | 33 +++++- .../src/endpoints/customer_portal/mod.rs | 3 +- ...er_portal_update_payment_method_handler.rs | 110 ++++++++++++++++++ .../routes/add_stripe_artcraft_routes.rs | 5 + 11 files changed, 251 insertions(+), 11 deletions(-) create mode 100644 crates/api_clients/artcraft_api_defs/src/stripe_artcraft/customer_portal_update_payment_method.rs create mode 100644 crates/api_clients/storyteller_client/src/stripe_artcraft/customer_portal_update_payment_method.rs create mode 100644 crates/desktop/artcraft/src/services/storyteller/commands/stripe_customer_portal/storyteller_open_customer_portal_update_payment_method_command.rs create mode 100644 crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/stripe_artcraft_customer_portal_update_payment_method_handler.rs diff --git a/crates/api_clients/artcraft_api_defs/src/stripe_artcraft/customer_portal_update_payment_method.rs b/crates/api_clients/artcraft_api_defs/src/stripe_artcraft/customer_portal_update_payment_method.rs new file mode 100644 index 0000000000..4ea9f423e3 --- /dev/null +++ b/crates/api_clients/artcraft_api_defs/src/stripe_artcraft/customer_portal_update_payment_method.rs @@ -0,0 +1,17 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +pub const CUSTOMER_PORTAL_UPDATE_PAYMENT_METHOD_URL_PATH: &str = "/v1/stripe_artcraft/portal/update_payment_method"; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct StripeArtcraftCustomerPortalUpdatePaymentMethodRequest { + // TODO: Not sure if this is needed + pub portal_config_id: Option, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct StripeArtcraftCustomerPortalUpdatePaymentMethodResponse { + pub success: bool, + pub stripe_portal_url: String, +} + diff --git a/crates/api_clients/artcraft_api_defs/src/stripe_artcraft/mod.rs b/crates/api_clients/artcraft_api_defs/src/stripe_artcraft/mod.rs index b81cd14270..3a5d3a98cb 100644 --- a/crates/api_clients/artcraft_api_defs/src/stripe_artcraft/mod.rs +++ b/crates/api_clients/artcraft_api_defs/src/stripe_artcraft/mod.rs @@ -2,4 +2,5 @@ pub mod create_credits_pack_checkout; pub mod create_subscription_checkout; pub mod customer_portal_cancel_plan; pub mod customer_portal_manage_plan; -pub mod customer_portal_switch_plan; \ No newline at end of file +pub mod customer_portal_switch_plan; +pub mod customer_portal_update_payment_method; \ No newline at end of file diff --git a/crates/api_clients/storyteller_client/src/stripe_artcraft/customer_portal_update_payment_method.rs b/crates/api_clients/storyteller_client/src/stripe_artcraft/customer_portal_update_payment_method.rs new file mode 100644 index 0000000000..08a6d5b3c5 --- /dev/null +++ b/crates/api_clients/storyteller_client/src/stripe_artcraft/customer_portal_update_payment_method.rs @@ -0,0 +1,18 @@ +use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; +use crate::error::storyteller_error::StorytellerError; +use crate::utils::api_host::ApiHost; +use crate::utils::basic_json_post_request::basic_json_post_request; +use artcraft_api_defs::stripe_artcraft::customer_portal_update_payment_method::{StripeArtcraftCustomerPortalUpdatePaymentMethodRequest, StripeArtcraftCustomerPortalUpdatePaymentMethodResponse, CUSTOMER_PORTAL_UPDATE_PAYMENT_METHOD_URL_PATH}; + +pub async fn customer_portal_update_payment_method( + api_host: &ApiHost, + maybe_creds: Option<&StorytellerCredentialSet>, + request: StripeArtcraftCustomerPortalUpdatePaymentMethodRequest, +) -> Result { + Ok(basic_json_post_request( + api_host, + CUSTOMER_PORTAL_UPDATE_PAYMENT_METHOD_URL_PATH, + maybe_creds, + request, + ).await?) +} diff --git a/crates/api_clients/storyteller_client/src/stripe_artcraft/mod.rs b/crates/api_clients/storyteller_client/src/stripe_artcraft/mod.rs index d51d5bdec1..538dc98f37 100644 --- a/crates/api_clients/storyteller_client/src/stripe_artcraft/mod.rs +++ b/crates/api_clients/storyteller_client/src/stripe_artcraft/mod.rs @@ -3,3 +3,4 @@ pub mod create_subscription_checkout; pub mod customer_portal_cancel_plan; pub mod customer_portal_manage_plan; pub mod customer_portal_switch_plan; +pub mod customer_portal_update_payment_method; diff --git a/crates/desktop/artcraft/src/services/storyteller/commands/stripe_customer_portal/mod.rs b/crates/desktop/artcraft/src/services/storyteller/commands/stripe_customer_portal/mod.rs index 97bc46e496..6d4213482e 100644 --- a/crates/desktop/artcraft/src/services/storyteller/commands/stripe_customer_portal/mod.rs +++ b/crates/desktop/artcraft/src/services/storyteller/commands/stripe_customer_portal/mod.rs @@ -1,3 +1,4 @@ pub mod storyteller_open_customer_portal_cancel_plan_command; pub mod storyteller_open_customer_portal_manage_plan_command; -pub mod storyteller_open_customer_portal_switch_plan_command; \ No newline at end of file +pub mod storyteller_open_customer_portal_switch_plan_command; +pub mod storyteller_open_customer_portal_update_payment_method_command; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/storyteller/commands/stripe_customer_portal/storyteller_open_customer_portal_switch_plan_command.rs b/crates/desktop/artcraft/src/services/storyteller/commands/stripe_customer_portal/storyteller_open_customer_portal_switch_plan_command.rs index 4d936f93d6..ddcc700fee 100644 --- a/crates/desktop/artcraft/src/services/storyteller/commands/stripe_customer_portal/storyteller_open_customer_portal_switch_plan_command.rs +++ b/crates/desktop/artcraft/src/services/storyteller/commands/stripe_customer_portal/storyteller_open_customer_portal_switch_plan_command.rs @@ -23,13 +23,6 @@ pub struct StorytellerOpenCustomerPortalSwitchPlanCommand { pub cadence: Option, } -#[derive(Deserialize, Debug, Clone, Copy)] -#[serde(rename_all = "snake_case")] -pub enum PortalReason { - UpdateSubscription, - CancelSubscription, -} - #[tauri::command] pub async fn storyteller_open_customer_portal_switch_plan_command( app: AppHandle, diff --git a/crates/desktop/artcraft/src/services/storyteller/commands/stripe_customer_portal/storyteller_open_customer_portal_update_payment_method_command.rs b/crates/desktop/artcraft/src/services/storyteller/commands/stripe_customer_portal/storyteller_open_customer_portal_update_payment_method_command.rs new file mode 100644 index 0000000000..762b4ae839 --- /dev/null +++ b/crates/desktop/artcraft/src/services/storyteller/commands/stripe_customer_portal/storyteller_open_customer_portal_update_payment_method_command.rs @@ -0,0 +1,62 @@ +use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{ContextualImageEditModel, EditImageQuality, EditImageSize}; +use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs; +use crate::core::state::data_dir::app_data_root::AppDataRoot; +use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager; +use crate::services::midjourney::windows::open_midjourney_login_window::open_midjourney_login_window; +use crate::services::sora::windows::sora_login_window::open_sora_login_window::open_sora_login_window; +use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; +use crate::services::storyteller::windows::open_storyteller_billing_window::{open_storyteller_billing_window, BillingWindowCase, OpenStorytellerBillingWindowArgs}; +use artcraft_api_defs::stripe_artcraft::create_subscription_checkout::PlanBillingCadence; +use artcraft_api_defs::stripe_artcraft::customer_portal_switch_plan::PlanBillingCadenceConfirmation; +use enums::common::artcraft_credits_pack_slug::ArtcraftCreditsPackSlug; +use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug; +use enums::tauri::ux::tauri_command_caller::TauriCommandCaller; +use errors::AnyhowResult; +use log::{error, info}; +use serde_derive::Deserialize; +use tauri::{AppHandle, State}; +use tokens::tokens::media_files::MediaFileToken; + +#[tauri::command] +pub async fn storyteller_open_customer_portal_update_payment_method_command( + app: AppHandle, + app_data_root: State<'_, AppDataRoot>, + app_env_configs: State<'_, AppEnvConfigs>, + storyteller_creds_manager: State<'_, StorytellerCredentialManager>, +) -> Result { + info!("storyteller_open_customer_portal_update_payment_method_command called"); + + do_open_portal( + &app, + &app_data_root, + &app_env_configs, + &storyteller_creds_manager, + ) + .await + .map_err(|err| { + error!("Error opening customer portal window: {:?}", err); + format!("Error opening customer portal window: {:?}", err) + })?; + + Ok("result".to_string()) +} + +async fn do_open_portal( + app: &AppHandle, + app_data_root: &AppDataRoot, + app_env_configs: &AppEnvConfigs, + storyteller_creds_manager: &StorytellerCredentialManager, +) -> AnyhowResult<()> { + info!("Building billing window..."); + + open_storyteller_billing_window(OpenStorytellerBillingWindowArgs { + app, + app_data_root, + app_env_configs, + storyteller_creds_manager, + billing_window_case: BillingWindowCase::CustomerPortalUpdatePaymentMethod, + }).await?; + + info!("Done."); + Ok(()) +} diff --git a/crates/desktop/artcraft/src/services/storyteller/windows/open_storyteller_billing_window.rs b/crates/desktop/artcraft/src/services/storyteller/windows/open_storyteller_billing_window.rs index f0a6b43304..f910aecd3d 100644 --- a/crates/desktop/artcraft/src/services/storyteller/windows/open_storyteller_billing_window.rs +++ b/crates/desktop/artcraft/src/services/storyteller/windows/open_storyteller_billing_window.rs @@ -9,6 +9,7 @@ use artcraft_api_defs::stripe_artcraft::create_subscription_checkout::{PlanBilli use artcraft_api_defs::stripe_artcraft::customer_portal_cancel_plan::StripeArtcraftCustomerPortalCancelPlanRequest; use artcraft_api_defs::stripe_artcraft::customer_portal_manage_plan::StripeArtcraftCustomerPortalManagePlanRequest; use artcraft_api_defs::stripe_artcraft::customer_portal_switch_plan::{PlanBillingCadenceConfirmation, StripeArtcraftCustomerPortalSwitchPlanRequest}; +use artcraft_api_defs::stripe_artcraft::customer_portal_update_payment_method::StripeArtcraftCustomerPortalUpdatePaymentMethodRequest; use enums::common::artcraft_credits_pack_slug::ArtcraftCreditsPackSlug; use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug; use errors::AnyhowResult; @@ -20,6 +21,7 @@ use storyteller_client::stripe_artcraft::create_subscription_checkout::create_su use storyteller_client::stripe_artcraft::customer_portal_cancel_plan::customer_portal_cancel_plan; use storyteller_client::stripe_artcraft::customer_portal_manage_plan::customer_portal_manage_plan; use storyteller_client::stripe_artcraft::customer_portal_switch_plan::customer_portal_switch_plan; +use storyteller_client::stripe_artcraft::customer_portal_update_payment_method::customer_portal_update_payment_method; use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; pub const BILLING_WINDOW_NAME: &str = "artcraft_billing_window"; @@ -58,6 +60,9 @@ pub enum BillingWindowCase { plan: ArtcraftSubscriptionSlug, cadence: PlanBillingCadenceConfirmation, }, + + /// Change the payment method attached to the customer / subscription. + CustomerPortalUpdatePaymentMethod, } pub async fn open_storyteller_billing_window( @@ -108,6 +113,12 @@ pub async fn open_storyteller_billing_window( cadence, ).await? }, + BillingWindowCase::CustomerPortalUpdatePaymentMethod => { + get_customer_portal_update_payment_method_url( + args.app_env_configs, + &creds, + ).await? + } }; do_open_window(args.app, args.app_data_root, checkout_url).await?; @@ -205,7 +216,6 @@ async fn get_customer_portal_manage_plan_url( Ok(Url::parse(&result.stripe_portal_url)?) } - async fn get_customer_portal_switch_plan_url( app_env_configs: &AppEnvConfigs, storyteller_creds: &StorytellerCredentialSet, @@ -231,6 +241,27 @@ async fn get_customer_portal_switch_plan_url( Ok(Url::parse(&result.stripe_portal_url)?) } +async fn get_customer_portal_update_payment_method_url( + app_env_configs: &AppEnvConfigs, + storyteller_creds: &StorytellerCredentialSet, +) -> AnyhowResult { + + info!("Getting customer portal update payment method session..."); + + let request = StripeArtcraftCustomerPortalUpdatePaymentMethodRequest { + portal_config_id: None, + }; + + let result = customer_portal_update_payment_method( + &app_env_configs.storyteller_host, + Some(&storyteller_creds), + request, + ).await?; + + info!("Customer portal update payment method created..."); + Ok(Url::parse(&result.stripe_portal_url)?) +} + async fn do_open_window( app: &AppHandle, app_data_root: &AppDataRoot, diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/mod.rs b/crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/mod.rs index b7582376a0..0e5b87732c 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/mod.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/mod.rs @@ -1,3 +1,4 @@ pub mod stripe_artcraft_customer_portal_cancel_plan_handler; pub mod stripe_artcraft_customer_portal_manage_plan_handler; -pub mod stripe_artcraft_customer_portal_switch_plan_handler; \ No newline at end of file +pub mod stripe_artcraft_customer_portal_switch_plan_handler; +pub mod stripe_artcraft_customer_portal_update_payment_method_handler; \ No newline at end of file diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/stripe_artcraft_customer_portal_update_payment_method_handler.rs b/crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/stripe_artcraft_customer_portal_update_payment_method_handler.rs new file mode 100644 index 0000000000..d9041e0eab --- /dev/null +++ b/crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/stripe_artcraft_customer_portal_update_payment_method_handler.rs @@ -0,0 +1,110 @@ +use crate::configs::stripe_artcraft_metadata_keys::{STRIPE_ARTCRAFT_METADATA_EMAIL, STRIPE_ARTCRAFT_METADATA_USERNAME, STRIPE_ARTCRAFT_METADATA_USER_TOKEN}; +use crate::configs::subscriptions::get_artcraft_subscription_by_slug_and_env::get_artcraft_subscription_by_slug_and_env; +use crate::endpoints::webhook::common::stripe_artcraft_webhook_error::StripeArtcraftWebhookError; +use crate::requests::lookup_subscription_from_subscription_id::lookup_subscription_from_subscription_id; +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_subscription_checkout::{PlanBillingCadence, StripeArtcraftCreateSubscriptionCheckoutRequest, StripeArtcraftCreateSubscriptionCheckoutResponse}; +use artcraft_api_defs::stripe_artcraft::customer_portal_switch_plan::{PlanBillingCadenceConfirmation, StripeArtcraftCustomerPortalSwitchPlanRequest, StripeArtcraftCustomerPortalSwitchPlanResponse}; +use artcraft_api_defs::stripe_artcraft::customer_portal_update_payment_method::{StripeArtcraftCustomerPortalUpdatePaymentMethodRequest, StripeArtcraftCustomerPortalUpdatePaymentMethodResponse}; +use component_traits::traits::internal_user_lookup::InternalUserLookup; +use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug; +use enums::common::payments_namespace::PaymentsNamespace; +use log::{error, info, warn}; +use mysql_queries::queries::users::user_subscriptions::find_subscription_for_owner_user::{find_subscription_for_owner_user_using_connection, UserSubscription}; +use reusable_types::server_environment::ServerEnvironment; +use sqlx::MySqlPool; +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; +use stripe_billing::billing_portal_session::{CreateBillingPortalSession, CreateBillingPortalSessionFlowData, CreateBillingPortalSessionFlowDataAfterCompletion, CreateBillingPortalSessionFlowDataAfterCompletionRedirect, CreateBillingPortalSessionFlowDataAfterCompletionType, CreateBillingPortalSessionFlowDataSubscriptionCancel, CreateBillingPortalSessionFlowDataSubscriptionCancelRetention, CreateBillingPortalSessionFlowDataSubscriptionCancelRetentionCouponOffer, CreateBillingPortalSessionFlowDataSubscriptionCancelRetentionType, CreateBillingPortalSessionFlowDataSubscriptionUpdate, CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirm, CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirmItems, CreateBillingPortalSessionFlowDataType}; +use stripe_billing::BillingPortalSession; +use stripe_checkout::checkout_session::{CreateCheckoutSession, CreateCheckoutSessionAutomaticTax, CreateCheckoutSessionLineItems, CreateCheckoutSessionSubscriptionData}; +use stripe_checkout::CheckoutSessionMode; +use stripe_core::CustomerId; +use tokens::tokens::users::UserToken; +use user_traits_component::traits::internal_session_cache_purge::InternalSessionCachePurge; + +pub async fn stripe_artcraft_customer_portal_update_payment_method_handler( + http_request: HttpRequest, + request: Json, + stripe_config: Data, + server_environment: Data, + internal_user_lookup: Data, + mysql_pool: Data, +) -> Result, CommonWebError> +{ + let mut mysql_connection = mysql_pool + .acquire() + .await + .map_err(|err| { + error!("Could not acquire mysql connection: {:?}", err); + CommonWebError::ServerError + })?; + + let maybe_user_metadata = internal_user_lookup + .lookup_user_from_http_request(&http_request) + .await + .map_err(|err| { + error!("Error looking up user: {:?}", err); + CommonWebError::ServerError // NB: This was probably *our* fault. + })?; + + // NB: Our integration relies on an internal user token being present. + let user_metadata = match maybe_user_metadata { + None => return Err(CommonWebError::NotAuthorized), + Some(user_metadata) => user_metadata, + }; + + let user_token = UserToken::new_from_str(&user_metadata.user_token); + + let result = find_subscription_for_owner_user_using_connection( + &user_token, + PaymentsNamespace::Artcraft, + &mut mysql_connection + ).await; + + let subscription = match result { + Err(err) => { + error!("Error looking up user's ({}) existing subscription: {:?}", &user_metadata.user_token, err); + return Err(CommonWebError::ServerError); // NB: This was probably *our* fault. + } + Ok(None) => { + return Err(CommonWebError::BadInputWithSimpleMessage("user has no active subscription".to_string())) + } + Ok(Some(subscription)) => subscription, + }; + + // TODO: Set the configuration id. + + let mut portal_builder = CreateBillingPortalSession::new(subscription.stripe_customer_id.clone()) + .return_url(stripe_config.portal_return_url.clone()) // TODO: This can be a different URL. + .flow_data(CreateBillingPortalSessionFlowData { + type_: CreateBillingPortalSessionFlowDataType::PaymentMethodUpdate, + 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: None, + subscription_update_confirm: None, + }); + + let portal_session = portal_builder + .send(&stripe_config.client) + .await + .map_err(|err| { + error!("Stripe Error: {:?}", err); + CommonWebError::ServerError + })?; + + Ok(Json(StripeArtcraftCustomerPortalUpdatePaymentMethodResponse { + success: true, + stripe_portal_url: portal_session.url, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/routes/add_stripe_artcraft_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/add_stripe_artcraft_routes.rs index b29a09775a..88234d9d16 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/add_stripe_artcraft_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/add_stripe_artcraft_routes.rs @@ -7,6 +7,7 @@ use billing_artcraft_component::endpoints::checkout::stripe_artcraft_create_subs use billing_artcraft_component::endpoints::customer_portal::stripe_artcraft_customer_portal_cancel_plan_handler::stripe_artcraft_customer_portal_cancel_plan_handler; use billing_artcraft_component::endpoints::customer_portal::stripe_artcraft_customer_portal_manage_plan_handler::stripe_artcraft_customer_portal_manage_plan_handler; use billing_artcraft_component::endpoints::customer_portal::stripe_artcraft_customer_portal_switch_plan_handler::stripe_artcraft_customer_portal_switch_plan_handler; +use billing_artcraft_component::endpoints::customer_portal::stripe_artcraft_customer_portal_update_payment_method_handler::stripe_artcraft_customer_portal_update_payment_method_handler; use billing_artcraft_component::endpoints::webhook::stripe_artcraft_webhook_handler::stripe_artcraft_webhook_handler; pub fn add_stripe_artcraft_routes (app: App) -> App @@ -45,5 +46,9 @@ where .route(web::post().to(stripe_artcraft_customer_portal_switch_plan_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service(web::resource("/portal/update_payment_method") + .route(web::post().to(stripe_artcraft_customer_portal_update_payment_method_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) ) }