From 16ff05170ec9b47f43dec05d87f92a6e026349c0 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 28 Jan 2026 21:25:59 -0500 Subject: [PATCH] endpoints wip --- ..._user_account_and_subscription_checkout.rs | 54 ++++++ .../src/stripe_artcraft/mod.rs | 1 + ...ft_create_subscription_checkout_handler.rs | 148 +--------------- .../checkout_with_user_signup/mod.rs | 3 + ...ption_checkout_with_user_signup_handler.rs | 115 ++++++++++++ .../user_creation_case.rs | 13 ++ .../user_exists_case.rs | 164 ++++++++++++++++++ .../billing_artcraft/src/endpoints/mod.rs | 1 + 8 files changed, 359 insertions(+), 140 deletions(-) create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/stripe_artcraft/create_create_new_user_account_and_subscription_checkout.rs create mode 100644 crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/mod.rs create mode 100644 crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/stripe_artcraft_create_subscription_checkout_with_user_signup_handler.rs create mode 100644 crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/user_creation_case.rs create mode 100644 crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/user_exists_case.rs diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/stripe_artcraft/create_create_new_user_account_and_subscription_checkout.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/stripe_artcraft/create_create_new_user_account_and_subscription_checkout.rs new file mode 100644 index 0000000000..14683887b9 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/stripe_artcraft/create_create_new_user_account_and_subscription_checkout.rs @@ -0,0 +1,54 @@ +use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug; +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +pub const CREATE_NEW_USER_ACCOUNT_AND_SUBSCRIPTION_CHECKOUT_URL_PATH: &str = "/v1/stripe_artcraft/user_signup/subscription_checkout"; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct StripeArtcraftCreateSubscriptionCheckoutWithUserSignupRequest { + /// The (non-Stripe) internal identifier for the product or subscription. + /// This will be translated into a Stripe identifier. + pub plan: Option, + + pub cadence: Option, +} + +#[derive(Serialize, Deserialize, ToSchema, Debug, Copy, Clone)] +pub enum PlanBillingCadence { + #[serde(rename = "monthly")] + Monthly, + + #[serde(rename = "yearly")] + Yearly, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct StripeArtcraftCreateSubscriptionCheckoutWithUserSignupResponse { + pub success: bool, + + /// The checkout session URL. + pub stripe_checkout_redirect_url: String, + + /// If a user account was created, these are the details. + pub generated_user: Option, + + /// If a session was created, these are the details. + pub session: Option, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct UserDetails { + /// The generated username. + pub username: String, + + /// The generated display name. + pub display_name: String, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct SessionDetails { + /// A signed session that can be sent as a header, bypassing cookies. + /// This is useful for API clients that don't support cookies or Google + /// browsers killing cross-domain cookies. + pub signed_session: String, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/stripe_artcraft/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/stripe_artcraft/mod.rs index 3a5d3a98cb..0083af57ca 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/stripe_artcraft/mod.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/stripe_artcraft/mod.rs @@ -1,3 +1,4 @@ +pub mod create_create_new_user_account_and_subscription_checkout; pub mod create_credits_pack_checkout; pub mod create_subscription_checkout; pub mod customer_portal_cancel_plan; diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_subscription_checkout_handler.rs b/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_subscription_checkout_handler.rs index 175ac01020..02e757fc21 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_subscription_checkout_handler.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_subscription_checkout_handler.rs @@ -24,7 +24,6 @@ use user_traits_component::traits::internal_session_cache_purge::InternalSession //use utoipa::ToSchema; -// /// Create a Stripe Checkout session and return the redirect URL in Json. // #[utoipa::path( // get, // tag = "Stripe (Artcraft)", @@ -36,6 +35,10 @@ use user_traits_component::traits::internal_session_cache_purge::InternalSession // (status = 200, description = "Success Delete", body = CreateCheckoutSessionSuccessResponse), // ), // )] + + +/// Create a Stripe Checkout session *with* user signup and return the redirect URL in Json. +/// If the user is already logged in, we just attach it to that record instead. pub async fn stripe_artcraft_create_subscription_session_handler( http_request: HttpRequest, request: Json, @@ -82,145 +85,10 @@ pub async fn stripe_artcraft_create_subscription_session_handler( // 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, - }; - - // 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_active_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. - })?; - - if maybe_active_subscription.is_some() { - return Err(CommonWebError::BadInputWithSimpleMessage( - "user already has an active subscription plan; use the portal to update the plan" - .to_string())) - } - - // NB: This works, but it feels really weird to have the customer's old email shown in a - // completely immutable state in the stripe checkout form. - // - // let maybe_inactive_subscription = find_possibly_inactive_first_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 ({}) possibly existing inactive subscription: {:?}", - // &user_metadata.user_token_typed, err); - // CommonWebError::ServerError // NB: This was probably *our* fault. - // })?; - // let maybe_existing_inactive_stripe_customer_id = maybe_inactive_subscription.as_ref() - // .map(|sub| sub.stripe_customer_id.as_str()); - - let mut maybe_existing_stripe_customer_id = None; - - let result = find_user_stripe_customer_link_using_connection( - &user_metadata.user_token_typed, - PaymentsNamespace::Artcraft, - &mut mysql_connection - ).await; - - // NB: Fail silently. - if let Ok(Some(link)) = result { - maybe_existing_stripe_customer_id = CustomerId::from_str(&link.stripe_customer_id).ok(); - } else if let Err(err) = result { - warn!("Error looking up user's ({}) existing stripe customer link: {:?}", &user_metadata.user_token_typed, err); - } - - let success_url = stripe_config.checkout_success_url.clone(); - let cancel_url = stripe_config.checkout_cancel_url.clone(); - - let checkout_session = { - // `client_reference_id` - // Stripe Docs: - // A unique string to reference the Checkout Session. - // This can be a customer ID, a cart ID, or similar, and can be used to reconcile the session - // with your internal systems. - // - // Our Notes: - // This gets reported back in the Checkout Session (and related webhooks) as - // `client_reference_id`. Passing the same ID on multiple checkouts does not unify or - // cross-correlate customers and only seems to be metadata for the checkout session itself. - // This is probably only useful for tracking checkout session engagement. - //params.client_reference_id = Some("SOME_INTERNAL_ID"); - - // `customer_email` - // Stripe Docs: - // If provided, this value will be used when the Customer object is created. If not provided, - // customers will be asked to enter their email address. Use this parameter to prefill - // customer data if you already have an email on file. To access information about the - // customer once a session is complete, use the customer field. - // - // Our Notes: - // This does not look up previous customers with the same email and will not unify or - // cross-correlate customers. By default the field will be un-editable in the checkout flow - // if this is specified. - //params.customer_email = Some("email@example.com"); - - let mut metadata = HashMap::new(); - - metadata.insert(STRIPE_ARTCRAFT_METADATA_USER_TOKEN.to_string(), user_metadata.user_token.to_string()); - - if let Some(username) = user_metadata.username.as_deref() { - metadata.insert(STRIPE_ARTCRAFT_METADATA_USERNAME.to_string(), username.to_string()); - } - - if let Some(user_email) = user_metadata.user_email.as_deref() { - metadata.insert(STRIPE_ARTCRAFT_METADATA_EMAIL.to_string(), user_email.to_string()); - } - - let mut checkout_builder = CreateCheckoutSession::new() - .success_url(&success_url) - .cancel_url(&cancel_url) - .mode(CheckoutSessionMode::Subscription) - .line_items(vec![ - CreateCheckoutSessionLineItems { - price: Some(price_id.to_string()), - quantity: Some(1), - ..Default::default() - } - ]) - .saved_payment_method_options(CreateCheckoutSessionSavedPaymentMethodOptions { - allow_redisplay_filters: Some(vec![ - CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters::Always, - CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters::Limited, - CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters::Unspecified, - ]), - // The user can choose to tick a checkbox that saves their card for redisplay later. - payment_method_save: Some(CreateCheckoutSessionSavedPaymentMethodOptionsPaymentMethodSave::Enabled), - }) - .allow_promotion_codes(true) // Allow promo codes / coupons - .automatic_tax(CreateCheckoutSessionAutomaticTax { - enabled: true, // This will ask for the customer's location - liability: None, - }) - .metadata(metadata.clone()) - .subscription_data(CreateCheckoutSessionSubscriptionData { - metadata: Some(metadata), - ..Default::default() - }) - ; - - if let Some(customer_id) = maybe_existing_stripe_customer_id { - info!("Adding existing stripe customer id to checkout session: {}", customer_id.as_str()); - checkout_builder = checkout_builder.customer(customer_id); - } - - let checkout_session = checkout_builder - .send(&stripe_config.client) - .await - .map_err(|err| { - error!("Stripe Error: {:?}", err); - CommonWebError::ServerError - })?; - - checkout_session + Some(user_metadata) => { + + user_exists_case + }, }; let url = checkout_session.url.ok_or(CommonWebError::ServerError)?; diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/mod.rs b/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/mod.rs new file mode 100644 index 0000000000..3105e30992 --- /dev/null +++ b/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/mod.rs @@ -0,0 +1,3 @@ +pub mod stripe_artcraft_create_subscription_checkout_with_user_signup_handler; +pub (super) mod user_exists_case; +pub (super) mod user_creation_case; \ No newline at end of file diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/stripe_artcraft_create_subscription_checkout_with_user_signup_handler.rs b/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/stripe_artcraft_create_subscription_checkout_with_user_signup_handler.rs new file mode 100644 index 0000000000..5c3aab4097 --- /dev/null +++ b/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/stripe_artcraft_create_subscription_checkout_with_user_signup_handler.rs @@ -0,0 +1,115 @@ +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::checkout_with_user_signup::user_creation_case::user_creation_case; +use crate::endpoints::checkout_with_user_signup::user_exists_case::user_exists_case; +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_create_new_user_account_and_subscription_checkout::{PlanBillingCadence, StripeArtcraftCreateSubscriptionCheckoutWithUserSignupRequest, StripeArtcraftCreateSubscriptionCheckoutWithUserSignupResponse}; +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_stripe_customer_links::find_user_stripe_customer_link::find_user_stripe_customer_link_using_connection; +use mysql_queries::queries::users::user_subscriptions::find_possibly_inactive_first_subscription_for_owner_user::find_possibly_inactive_first_subscription_for_owner_user_using_connection; +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; +use stripe_checkout::checkout_session::{CreateCheckoutSession, CreateCheckoutSessionAutomaticTax, CreateCheckoutSessionLineItems, CreateCheckoutSessionSavedPaymentMethodOptions, CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters, CreateCheckoutSessionSavedPaymentMethodOptionsPaymentMethodSave, CreateCheckoutSessionSubscriptionData}; +use stripe_checkout::CheckoutSessionMode; +use stripe_core::CustomerId; +use user_traits_component::traits::internal_session_cache_purge::InternalSessionCachePurge; +// /// Create a new user account and Stripe Checkout session and return the redirect URL in Json. +// #[utoipa::path( +// get, +// tag = "Stripe (Artcraft)", +// path = "/v1/stripe_artcraft/user_signup/subscription_checkout", +// params( +// ("request" = CreateCheckoutSessionRequest, description = "Payload for Request"), +// ), +// responses( +// (status = 200, description = "Success Delete", body = CreateCheckoutSessionSuccessResponse), +// ), +// )] + +/// Create a Stripe Checkout session *with* user signup and return the redirect URL in Json. +/// If the user is already logged in, we just attach it to that record instead. +pub async fn stripe_artcraft_create_checkout_with_user_signup_handler( + http_request: HttpRequest, + request: Json, + stripe_config: Data, + server_environment: Data, + internal_user_lookup: Data, + internal_session_cache_purge: Data, + mysql_pool: Data, +) -> Result, CommonWebError> +{ + let slug = match request.plan { + None => return Err(CommonWebError::BadInputWithSimpleMessage("no plan supplied".to_string())), + Some(slug) => slug, + }; + + let cadence = match request.cadence { + None => return Err(CommonWebError::BadInputWithSimpleMessage("no cadence supplied".to_string())), + Some(cadence) => cadence, + }; + + let plan = get_artcraft_subscription_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 price_id = match cadence { + PlanBillingCadence::Monthly => plan.monthly_price_id.clone(), + PlanBillingCadence::Yearly => plan.yearly_price_id.clone(), + }; + + let maybe_user_metadata = internal_user_lookup + .lookup_user_from_http_request_and_mysql_connection(&http_request, &mut mysql_connection) + .await + .map_err(|err| { + error!("Error looking up user: {:?}", err); + CommonWebError::ServerError // NB: This was probably *our* fault. + })?; + + let checkout_session = match maybe_user_metadata { + None => { + info!("Creating new user, then creating checkout session..."); + user_creation_case( + &price_id, + &mut mysql_connection, + &stripe_config, + ).await? + }, + Some(user_metadata) => { + info!("Creating checkout session for user: {:?}", user_metadata.user_token_typed); + user_exists_case( + &price_id, + &user_metadata, + &mut mysql_connection, + &stripe_config, + ).await? + }, + }; + + let url = checkout_session.url.ok_or(CommonWebError::ServerError)?; + + // Best effort to delete Redis session cache + internal_session_cache_purge.best_effort_purge_session_cache(&http_request); + + Ok(Json(StripeArtcraftCreateSubscriptionCheckoutWithUserSignupResponse { + success: true, + stripe_checkout_redirect_url: url, + generated_user: None, + session: None, + })) +} diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/user_creation_case.rs b/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/user_creation_case.rs new file mode 100644 index 0000000000..a315d9c557 --- /dev/null +++ b/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/user_creation_case.rs @@ -0,0 +1,13 @@ +use crate::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient; +use crate::utils::common_web_error::CommonWebError; +use sqlx::pool::PoolConnection; +use sqlx::MySql; +use stripe_shared::CheckoutSession; + +pub (super) async fn user_creation_case( + price_id: &str, + mysql_connection: &mut PoolConnection, + stripe_config: &ArtcraftStripeConfigWithClient, +) -> Result { + Ok(()) +} diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/user_exists_case.rs b/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/user_exists_case.rs new file mode 100644 index 0000000000..f187fc448d --- /dev/null +++ b/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/user_exists_case.rs @@ -0,0 +1,164 @@ +use crate::configs::stripe_artcraft_metadata_keys::{STRIPE_ARTCRAFT_METADATA_EMAIL, STRIPE_ARTCRAFT_METADATA_USERNAME, STRIPE_ARTCRAFT_METADATA_USER_TOKEN}; +use crate::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient; +use crate::utils::common_web_error::CommonWebError; +use actix_web::web::Data; +use component_traits::traits::internal_user_lookup::UserMetadata; +use enums::common::payments_namespace::PaymentsNamespace; +use log::{error, info, warn}; +use mysql_queries::queries::users::user_stripe_customer_links::find_user_stripe_customer_link::find_user_stripe_customer_link_using_connection; +use mysql_queries::queries::users::user_subscriptions::find_subscription_for_owner_user::find_subscription_for_owner_user_using_connection; +use sqlx::pool::PoolConnection; +use sqlx::MySql; +use std::collections::HashMap; +use std::str::FromStr; +use stripe_checkout::checkout_session::{CreateCheckoutSession, CreateCheckoutSessionAutomaticTax, CreateCheckoutSessionLineItems, CreateCheckoutSessionSavedPaymentMethodOptions, CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters, CreateCheckoutSessionSavedPaymentMethodOptionsPaymentMethodSave, CreateCheckoutSessionSubscriptionData}; +use stripe_shared::{CheckoutSession, CheckoutSessionMode, CustomerId, PriceId}; + +pub (super) async fn user_exists_case( + price_id: &str, + user_metadata: &UserMetadata, + mysql_connection: &mut PoolConnection, + stripe_config: &ArtcraftStripeConfigWithClient, +) -> Result { + + // 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_active_subscription = find_subscription_for_owner_user_using_connection( + &user_metadata.user_token_typed, + PaymentsNamespace::Artcraft, + 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. + })?; + + if maybe_active_subscription.is_some() { + return Err(CommonWebError::BadInputWithSimpleMessage( + "user already has an active subscription plan; use the portal to update the plan" + .to_string())) + } + + // NB: This works, but it feels really weird to have the customer's old email shown in a + // completely immutable state in the stripe checkout form. + // + // let maybe_inactive_subscription = find_possibly_inactive_first_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 ({}) possibly existing inactive subscription: {:?}", + // &user_metadata.user_token_typed, err); + // CommonWebError::ServerError // NB: This was probably *our* fault. + // })?; + // let maybe_existing_inactive_stripe_customer_id = maybe_inactive_subscription.as_ref() + // .map(|sub| sub.stripe_customer_id.as_str()); + + let mut maybe_existing_stripe_customer_id = None; + + let result = find_user_stripe_customer_link_using_connection( + &user_metadata.user_token_typed, + PaymentsNamespace::Artcraft, + mysql_connection + ).await; + + // NB: Fail silently. + if let Ok(Some(link)) = result { + maybe_existing_stripe_customer_id = CustomerId::from_str(&link.stripe_customer_id).ok(); + } else if let Err(err) = result { + warn!("Error looking up user's ({}) existing stripe customer link: {:?}", &user_metadata.user_token_typed, err); + } + + let success_url = stripe_config.checkout_success_url.clone(); + let cancel_url = stripe_config.checkout_cancel_url.clone(); + + let checkout_session = { + // `client_reference_id` + // Stripe Docs: + // A unique string to reference the Checkout Session. + // This can be a customer ID, a cart ID, or similar, and can be used to reconcile the session + // with your internal systems. + // + // Our Notes: + // This gets reported back in the Checkout Session (and related webhooks) as + // `client_reference_id`. Passing the same ID on multiple checkouts does not unify or + // cross-correlate customers and only seems to be metadata for the checkout session itself. + // This is probably only useful for tracking checkout session engagement. + //params.client_reference_id = Some("SOME_INTERNAL_ID"); + + // `customer_email` + // Stripe Docs: + // If provided, this value will be used when the Customer object is created. If not provided, + // customers will be asked to enter their email address. Use this parameter to prefill + // customer data if you already have an email on file. To access information about the + // customer once a session is complete, use the customer field. + // + // Our Notes: + // This does not look up previous customers with the same email and will not unify or + // cross-correlate customers. By default the field will be un-editable in the checkout flow + // if this is specified. + //params.customer_email = Some("email@example.com"); + + let mut metadata = HashMap::new(); + + metadata.insert(STRIPE_ARTCRAFT_METADATA_USER_TOKEN.to_string(), user_metadata.user_token.to_string()); + + if let Some(username) = user_metadata.username.as_deref() { + metadata.insert(STRIPE_ARTCRAFT_METADATA_USERNAME.to_string(), username.to_string()); + } + + if let Some(user_email) = user_metadata.user_email.as_deref() { + metadata.insert(STRIPE_ARTCRAFT_METADATA_EMAIL.to_string(), user_email.to_string()); + } + + let mut checkout_builder = CreateCheckoutSession::new() + .success_url(&success_url) + .cancel_url(&cancel_url) + .mode(CheckoutSessionMode::Subscription) + .line_items(vec![ + CreateCheckoutSessionLineItems { + price: Some(price_id.to_string()), + quantity: Some(1), + ..Default::default() + } + ]) + .saved_payment_method_options(CreateCheckoutSessionSavedPaymentMethodOptions { + allow_redisplay_filters: Some(vec![ + CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters::Always, + CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters::Limited, + CreateCheckoutSessionSavedPaymentMethodOptionsAllowRedisplayFilters::Unspecified, + ]), + // The user can choose to tick a checkbox that saves their card for redisplay later. + payment_method_save: Some(CreateCheckoutSessionSavedPaymentMethodOptionsPaymentMethodSave::Enabled), + }) + .allow_promotion_codes(true) // Allow promo codes / coupons + .automatic_tax(CreateCheckoutSessionAutomaticTax { + enabled: true, // This will ask for the customer's location + liability: None, + }) + .metadata(metadata.clone()) + .subscription_data(CreateCheckoutSessionSubscriptionData { + metadata: Some(metadata), + ..Default::default() + }) + ; + + if let Some(customer_id) = maybe_existing_stripe_customer_id { + info!("Adding existing stripe customer id to checkout session: {}", customer_id.as_str()); + checkout_builder = checkout_builder.customer(customer_id); + } + + let checkout_session = checkout_builder + .send(&stripe_config.client) + .await + .map_err(|err| { + error!("Stripe Error: {:?}", err); + CommonWebError::ServerError + })?; + + checkout_session + }; + + + Ok(checkout_session) +} diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/mod.rs b/crates/service/plugins/billing_artcraft/src/endpoints/mod.rs index 7cb14865dc..c5feabace3 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/mod.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/mod.rs @@ -1,3 +1,4 @@ pub mod checkout; +pub mod checkout_with_user_signup; pub mod customer_portal; pub mod webhook; \ No newline at end of file