update payment method

This commit is contained in:
Brandon Thomas committed 2025-09-16 07:12:27 -04:00
1 parent 01053b7ccc
commit 68e77fd7d3
11 files changed
+251 -11

No files matched your search

@@ -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<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalUpdatePaymentMethodResponse {
pub success: bool,
pub stripe_portal_url: String,
}
@@ -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;
pub mod customer_portal_switch_plan;
pub mod customer_portal_update_payment_method;
@@ -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<StripeArtcraftCustomerPortalUpdatePaymentMethodResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
CUSTOMER_PORTAL_UPDATE_PAYMENT_METHOD_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -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;
@@ -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;
pub mod storyteller_open_customer_portal_switch_plan_command;
pub mod storyteller_open_customer_portal_update_payment_method_command;
@@ -23,13 +23,6 @@ pub struct StorytellerOpenCustomerPortalSwitchPlanCommand {
pub cadence: Option<PlanBillingCadenceConfirmation>,
}
#[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,
@@ -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<String, String> {
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(())
}
@@ -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<Url> {
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,
@@ -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;
pub mod stripe_artcraft_customer_portal_switch_plan_handler;
pub mod stripe_artcraft_customer_portal_update_payment_method_handler;
@@ -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<StripeArtcraftCustomerPortalUpdatePaymentMethodRequest>,
stripe_config: Data<ArtcraftStripeConfigWithClient>,
server_environment: Data<ServerEnvironment>,
internal_user_lookup: Data<dyn InternalUserLookup>,
mysql_pool: Data<MySqlPool>,
) -> Result<Json<StripeArtcraftCustomerPortalUpdatePaymentMethodResponse>, 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,
}))
}
@@ -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<T, B> (app: App<T>) -> App<T>
@@ -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()))
)
)
}