From e0400b651e88c772b0cab291ce2d0254dcc4e28f Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 16 Sep 2025 11:53:40 -0400 Subject: [PATCH] credits checkout from existing subscriber record --- Cargo.lock | 1 + ...ft_create_credits_pack_checkout_handler.rs | 33 +++++++++++++++++-- .../plugins/component_traits/Cargo.toml | 3 +- .../src/traits/internal_user_lookup.rs | 10 +++++- .../stripe_internal_user_lookup_impl.rs | 10 ++++-- 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 417c6dfb27..7ba6cd13fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2292,6 +2292,7 @@ dependencies = [ "serde_derive", "serde_json", "sqlx", + "tokens", "tokio", "user_traits_component", "workspace-build-acceleration", diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_credits_pack_checkout_handler.rs b/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_credits_pack_checkout_handler.rs index 8f252d9425..2ad1023fb5 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_credits_pack_checkout_handler.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_credits_pack_checkout_handler.rs @@ -1,6 +1,7 @@ use crate::configs::credits_packs::get_artcraft_credits_pack_by_slug_and_env::get_artcraft_credits_pack_by_slug_and_env; 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::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient; use crate::utils::common_web_error::CommonWebError; use actix_web::web::{Data, Json}; @@ -11,7 +12,9 @@ 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; use reusable_types::server_environment::ServerEnvironment; +use sqlx::MySqlPool; use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; @@ -19,7 +22,6 @@ use stripe_checkout::checkout_session::{CreateCheckoutSession, CreateCheckoutSes use stripe_checkout::CheckoutSessionMode; use stripe_core::CustomerId; use user_traits_component::traits::internal_session_cache_purge::InternalSessionCachePurge; - //use utoipa::ToSchema; // /// Create a Stripe Checkout session and return the redirect URL in Json. @@ -41,6 +43,7 @@ pub async fn stripe_artcraft_create_credits_pack_checkout_handler( server_environment: Data, internal_user_lookup: Data, internal_session_cache_purge: Data, + mysql_pool: Data, ) -> Result, CommonWebError> { let slug = match request.credits_pack { @@ -50,8 +53,16 @@ pub async fn stripe_artcraft_create_credits_pack_checkout_handler( let credits_pack = get_artcraft_credits_pack_by_slug_and_env(slug, **server_environment); + 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) + .lookup_user_from_http_request_and_mysql_connection(&http_request, &mut mysql_connection) .await .map_err(|err| { error!("Error looking up user: {:?}", err); @@ -64,6 +75,21 @@ pub async fn stripe_artcraft_create_credits_pack_checkout_handler( Some(user_metadata) => user_metadata, }; + // NB: Currently the stripe customer id field in the `users` table is only for FakeYou subscriptions, + // so we need to look up any existing Artcraft subscription separately. This is needed to pre-fill + // the Stripe billing form. + let maybe_subscription = find_subscription_for_owner_user_using_connection( + &user_metadata.user_token_typed, + PaymentsNamespace::Artcraft, + &mut mysql_connection + ).await.map_err(|err| { + error!("Error looking up user's ({}) existing subscription: {:?}", &user_metadata.user_token_typed, err); + CommonWebError::ServerError // NB: This was probably *our* fault. + })?; + + let maybe_existing_stripe_customer_id = maybe_subscription.as_ref() + .map(|sub| sub.stripe_customer_id.as_str()); + let success_url = stripe_config.checkout_success_url.clone(); let cancel_url = stripe_config.checkout_cancel_url.clone(); @@ -135,7 +161,8 @@ pub async fn stripe_artcraft_create_credits_pack_checkout_handler( ..Default::default() }); - if let Some(existing_stripe_customer_id) = user_metadata.maybe_existing_stripe_customer_id.as_deref() { + if let Some(existing_stripe_customer_id) = maybe_existing_stripe_customer_id{ + info!("Adding existing stripe customer id to checkout session: {}", existing_stripe_customer_id); match CustomerId::from_str(existing_stripe_customer_id) { Ok(customer_id) => { checkout_builder = checkout_builder.customer(customer_id); diff --git a/crates/service/plugins/component_traits/Cargo.toml b/crates/service/plugins/component_traits/Cargo.toml index 0b31ef6d22..411f57fd3b 100644 --- a/crates/service/plugins/component_traits/Cargo.toml +++ b/crates/service/plugins/component_traits/Cargo.toml @@ -20,14 +20,15 @@ normal = ["workspace-build-acceleration"] workspace-build-acceleration.workspace = true # Internal +#url_config = { path = "../../../lib/url_config" } artcraft_api_defs.workspace = true enums.workspace = true errors.workspace = true http_server_common = { path = "../../../lib/deprecated/http_server_common" } mysql_queries = { path = "../../../schema/database/mysql_queries" } reusable_types = { path = "../../../schema/public/reusable_types" } +tokens.workspace = true user_traits_component = { path = "../user_traits_component" } -#url_config = { path = "../../../lib/url_config" } # External actix-http.workspace = true diff --git a/crates/service/plugins/component_traits/src/traits/internal_user_lookup.rs b/crates/service/plugins/component_traits/src/traits/internal_user_lookup.rs index a05872bc52..a65c0cf4ea 100644 --- a/crates/service/plugins/component_traits/src/traits/internal_user_lookup.rs +++ b/crates/service/plugins/component_traits/src/traits/internal_user_lookup.rs @@ -4,6 +4,9 @@ use std::fmt::{Display, Formatter}; use actix_web::HttpRequest; use async_trait::async_trait; use enums::common::payments_namespace::PaymentsNamespace; +use sqlx::pool::PoolConnection; +use sqlx::MySql; +use tokens::tokens::users::UserToken; //#[cfg(test)] //use mockall::automock; @@ -35,9 +38,12 @@ impl Error for InternalUserLookupError {} #[derive(Clone, Default)] pub struct UserMetadata { - /// Internal system primary key identifier of the user. + /// Internal system primary key identifier of the user, stringly typed. pub user_token: String, + /// Internal system primary key identifier of the user, strongly typed. + pub user_token_typed: UserToken, + /// Internal system username for the user. /// We will associate this to Stripe objects if available. pub username: Option, @@ -80,4 +86,6 @@ pub trait InternalUserLookup { /// Lookup a user's session details from an HTTP request, then return the /// relevant pieces for the Stripe integration. async fn lookup_user_from_http_request(&self, http_request: &HttpRequest) -> Result, InternalUserLookupError>; + + async fn lookup_user_from_http_request_and_mysql_connection(&self, http_request: &HttpRequest, mysql_connection: &mut PoolConnection) -> Result, InternalUserLookupError>; } diff --git a/crates/service/web/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs b/crates/service/web/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs index 3fa283c25b..21b88ece3e 100644 --- a/crates/service/web/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs +++ b/crates/service/web/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs @@ -4,7 +4,8 @@ use async_trait::async_trait; use chrono::Utc; use component_traits::traits::internal_user_lookup::{InternalUserLookup, InternalUserLookupError, SubscriptionKey, UserMetadata}; use log::warn; -use sqlx::MySqlPool; +use sqlx::{MySql, MySqlPool}; +use sqlx::pool::PoolConnection; /// A simple Actix injectable action #[derive(Clone)] @@ -33,8 +34,12 @@ impl InternalUserLookup for StripeInternalUserLookupImpl { InternalUserLookupError::ServerError })?; + self.lookup_user_from_http_request_and_mysql_connection(http_request, &mut mysql_connection).await + } + + async fn lookup_user_from_http_request_and_mysql_connection(&self, http_request: &HttpRequest, mysql_connection: &mut PoolConnection) -> Result, InternalUserLookupError> { let maybe_user_session = self.session_checker - .maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection) + .maybe_get_user_session_extended_from_connection(&http_request, mysql_connection) .await .map_err(|e| { warn!("Session checker error: {:?}", e); @@ -47,6 +52,7 @@ impl InternalUserLookup for StripeInternalUserLookupImpl { None => Ok(None), Some(user_session) => Ok(Some(UserMetadata { user_token: user_session.user_token, + user_token_typed: user_session.user_token_typed, username: Some(user_session.user.username), user_email: Some(user_session.user.email_address), maybe_existing_stripe_customer_id: user_session.premium.maybe_stripe_customer_id,