diff --git a/crates/api_clients/storyteller_client/src/stripe_artcraft/create_credits_pack_checkout.rs b/crates/api_clients/storyteller_client/src/stripe_artcraft/create_credits_pack_checkout.rs new file mode 100644 index 0000000000..b27204031f --- /dev/null +++ b/crates/api_clients/storyteller_client/src/stripe_artcraft/create_credits_pack_checkout.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::create_credits_pack_checkout::{StripeArtcraftCreateCreditsPackCheckoutRequest, StripeArtcraftCreateCreditsPackCheckoutResponse, CREATE_CREDITS_PACK_CHECKOUT_URL_PATH}; + +pub async fn create_credits_pack_checkout( + api_host: &ApiHost, + maybe_creds: Option<&StorytellerCredentialSet>, + request: StripeArtcraftCreateCreditsPackCheckoutRequest, +) -> Result { + Ok(basic_json_post_request( + api_host, + CREATE_CREDITS_PACK_CHECKOUT_URL_PATH, + maybe_creds, + request, + ).await?) +} diff --git a/crates/api_clients/storyteller_client/src/stripe_artcraft/create_subscription_checkout.rs b/crates/api_clients/storyteller_client/src/stripe_artcraft/create_subscription_checkout.rs index 3c5ee83fb8..59d39b7cb8 100644 --- a/crates/api_clients/storyteller_client/src/stripe_artcraft/create_subscription_checkout.rs +++ b/crates/api_clients/storyteller_client/src/stripe_artcraft/create_subscription_checkout.rs @@ -2,13 +2,13 @@ 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::create_subscription_checkout::{StripeArtcraftCreateCheckoutSessionRequest, StripeArtcraftCreateCheckoutSessionResponse, CREATE_SUBSCRIPTION_CHECKOUT_URL_PATH}; +use artcraft_api_defs::stripe_artcraft::create_subscription_checkout::{StripeArtcraftCreateSubscriptionCheckoutRequest, StripeArtcraftCreateSubscriptionCheckoutResponse, CREATE_SUBSCRIPTION_CHECKOUT_URL_PATH}; pub async fn create_subscription_checkout( api_host: &ApiHost, maybe_creds: Option<&StorytellerCredentialSet>, - request: StripeArtcraftCreateCheckoutSessionRequest, -) -> Result { + request: StripeArtcraftCreateSubscriptionCheckoutRequest, +) -> Result { Ok(basic_json_post_request( api_host, CREATE_SUBSCRIPTION_CHECKOUT_URL_PATH, 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 3f6dca510d..d0dd6bc5b7 100644 --- a/crates/api_clients/storyteller_client/src/stripe_artcraft/mod.rs +++ b/crates/api_clients/storyteller_client/src/stripe_artcraft/mod.rs @@ -1 +1,2 @@ -pub mod create_subscription_checkout; \ No newline at end of file +pub mod create_credits_pack_checkout; +pub mod create_subscription_checkout; diff --git a/crates/desktop/artcraft/src/services/storyteller/commands/mod.rs b/crates/desktop/artcraft/src/services/storyteller/commands/mod.rs index a33db36d60..41c10e0ef4 100644 --- a/crates/desktop/artcraft/src/services/storyteller/commands/mod.rs +++ b/crates/desktop/artcraft/src/services/storyteller/commands/mod.rs @@ -1 +1,2 @@ -pub mod storyteller_open_billing_command; \ No newline at end of file +pub mod storyteller_open_billing_command; +pub mod storyteller_open_credits_purchase_command; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_open_credits_purchase_command.rs b/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_open_credits_purchase_command.rs new file mode 100644 index 0000000000..d93b82e73e --- /dev/null +++ b/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_open_credits_purchase_command.rs @@ -0,0 +1,70 @@ +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; +use artcraft_api_defs::stripe_artcraft::create_subscription_checkout::PlanBillingCadence; +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 enums::common::artcraft_credits_pack_slug::ArtcraftCreditsPackSlug; +use tokens::tokens::media_files::MediaFileToken; + +#[derive(Deserialize, Debug)] +pub struct StorytellerOpenCreditsPurchaseCommand { + pub credits_pack: Option, +} + +#[tauri::command] +pub async fn storyteller_credits_purchase_command( + app: AppHandle, + request: StorytellerOpenCreditsPurchaseCommand, + app_data_root: State<'_, AppDataRoot>, + app_env_configs: State<'_, AppEnvConfigs>, + storyteller_creds_manager: State<'_, StorytellerCredentialManager>, +) -> Result { + info!("storyteller_open_billing_command called"); + + do_open_billing( + &app, + &app_data_root, + &app_env_configs, + &storyteller_creds_manager, + request.credits_pack, + ) + .await + .map_err(|err| { + error!("Error opening login: {:?}", err); + format!("Error opening login: {:?}", err) + })?; + + Ok("result".to_string()) +} + +async fn do_open_billing( + app: &AppHandle, + app_data_root: &AppDataRoot, + app_env_configs: &AppEnvConfigs, + storyteller_creds_manager: &StorytellerCredentialManager, + credits_pack: Option, +) -> AnyhowResult<()> { + info!("Building billing window..."); + + open_storyteller_billing_window( + app, + app_env_configs, + app_data_root, + storyteller_creds_manager, + plan, + cadence, + ).await?; + + info!("Done."); + Ok(()) +} diff --git a/crates/schema/public/api/artcraft_api_defs/src/stripe_artcraft/create_credits_pack_checkout.rs b/crates/schema/public/api/artcraft_api_defs/src/stripe_artcraft/create_credits_pack_checkout.rs new file mode 100644 index 0000000000..4aed91b671 --- /dev/null +++ b/crates/schema/public/api/artcraft_api_defs/src/stripe_artcraft/create_credits_pack_checkout.rs @@ -0,0 +1,17 @@ +use enums::common::artcraft_credits_pack_slug::ArtcraftCreditsPackSlug; +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +pub const CREATE_CREDITS_PACK_CHECKOUT_URL_PATH: &str = "/v1/stripe_artcraft/checkout/credits_pack"; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct StripeArtcraftCreateCreditsPackCheckoutRequest { + pub credits_pack: Option, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct StripeArtcraftCreateCreditsPackCheckoutResponse { + pub success: bool, + pub stripe_checkout_redirect_url: String, +} + diff --git a/crates/schema/public/api/artcraft_api_defs/src/stripe_artcraft/create_subscription_checkout.rs b/crates/schema/public/api/artcraft_api_defs/src/stripe_artcraft/create_subscription_checkout.rs index ce2d2e01f9..2d30193d87 100644 --- a/crates/schema/public/api/artcraft_api_defs/src/stripe_artcraft/create_subscription_checkout.rs +++ b/crates/schema/public/api/artcraft_api_defs/src/stripe_artcraft/create_subscription_checkout.rs @@ -2,10 +2,10 @@ use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug; use serde_derive::{Deserialize, Serialize}; use utoipa::ToSchema; -pub const CREATE_SUBSCRIPTION_CHECKOUT_URL_PATH: &str = "/v1/stripe_artcraft/checkout/create_subscription"; +pub const CREATE_SUBSCRIPTION_CHECKOUT_URL_PATH: &str = "/v1/stripe_artcraft/checkout/subscription"; #[derive(Serialize, Deserialize, ToSchema)] -pub struct StripeArtcraftCreateCheckoutSessionRequest { +pub struct StripeArtcraftCreateSubscriptionCheckoutRequest { /// The (non-Stripe) internal identifier for the product or subscription. /// This will be translated into a Stripe identifier. pub plan: Option, @@ -23,7 +23,7 @@ pub enum PlanBillingCadence { } #[derive(Serialize, Deserialize, ToSchema)] -pub struct StripeArtcraftCreateCheckoutSessionResponse { +pub struct StripeArtcraftCreateSubscriptionCheckoutResponse { pub success: bool, pub stripe_checkout_redirect_url: String, } diff --git a/crates/schema/public/api/artcraft_api_defs/src/stripe_artcraft/mod.rs b/crates/schema/public/api/artcraft_api_defs/src/stripe_artcraft/mod.rs index 3f6dca510d..7463c08653 100644 --- a/crates/schema/public/api/artcraft_api_defs/src/stripe_artcraft/mod.rs +++ b/crates/schema/public/api/artcraft_api_defs/src/stripe_artcraft/mod.rs @@ -1 +1,2 @@ +pub mod create_credits_pack_checkout; pub mod create_subscription_checkout; \ No newline at end of file diff --git a/crates/schema/public/enums/src/common/artcraft_credits_pack_slug.rs b/crates/schema/public/enums/src/common/artcraft_credits_pack_slug.rs new file mode 100644 index 0000000000..6341c83f8a --- /dev/null +++ b/crates/schema/public/enums/src/common/artcraft_credits_pack_slug.rs @@ -0,0 +1,118 @@ +use std::collections::BTreeSet; + +#[cfg(test)] +use strum::EnumCount; +#[cfg(test)] +use strum::EnumIter; +use utoipa::ToSchema; + +/// NB: This will be used by a variety of tables (MySQL and sqlite)! +/// Keep the max length to 16 characters. +#[cfg_attr(test, derive(EnumIter, EnumCount))] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum ArtcraftCreditsPackSlug { + #[serde(rename= "artcraft_1000")] + Artcraft1000, + #[serde(rename= "artcraft_2500")] + Artcraft2500, +} + +impl_enum_display_and_debug_using_to_str!(ArtcraftCreditsPackSlug); +impl_mysql_enum_coders!(ArtcraftCreditsPackSlug); +impl_mysql_from_row!(ArtcraftCreditsPackSlug); + +// NB: We can derive `sqlx::Type` instead of using `impl_mysql_enum_coders` + +impl ArtcraftCreditsPackSlug { + pub fn to_str(&self) -> &'static str { + match self { + Self::Artcraft1000 => "artcraft_1000", + Self::Artcraft2500 => "artcraft_2500", + } + } + + pub fn from_str(s: &str) -> Result { + match s { + "artcraft_1000" => Ok(Self::Artcraft1000), + "artcraft_2500" => Ok(Self::Artcraft2500), + _ => Err(format!("invalid artcraft_credits_pack_slug: {:?}", s)), + } + } + + pub fn all_variants() -> BTreeSet { + // NB: BTreeSet is sorted + // NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc. + BTreeSet::from([ + Self::Artcraft1000, + Self::Artcraft2500, + ]) + } +} + +#[cfg(test)] +mod tests { + use crate::test_helpers::assert_serialization; + use crate::common::artcraft_credits_pack_slug::ArtcraftCreditsPackSlug; + + mod explicit_checks { + use super::*; + + #[test] + fn test_serialization() { + assert_serialization(ArtcraftCreditsPackSlug::Artcraft1000, "artcraft_1000"); + assert_serialization(ArtcraftCreditsPackSlug::Artcraft2500, "artcraft_2500"); + } + + #[test] + fn to_str() { + assert_eq!(ArtcraftCreditsPackSlug::Artcraft1000.to_str(), "artcraft_1000"); + assert_eq!(ArtcraftCreditsPackSlug::Artcraft2500.to_str(), "artcraft_2500"); + } + + #[test] + fn from_str() { + assert_eq!(ArtcraftCreditsPackSlug::from_str("artcraft_1000").unwrap(), ArtcraftCreditsPackSlug::Artcraft1000); + assert_eq!(ArtcraftCreditsPackSlug::from_str("artcraft_2500").unwrap(), ArtcraftCreditsPackSlug::Artcraft2500); + } + + #[test] + fn all_variants() { + let mut variants = ArtcraftCreditsPackSlug::all_variants(); + assert_eq!(variants.len(), 2); + assert_eq!(variants.pop_first(), Some(ArtcraftCreditsPackSlug::Artcraft1000)); + assert_eq!(variants.pop_first(), Some(ArtcraftCreditsPackSlug::Artcraft2500)); + assert_eq!(variants.pop_first(), None); + } + } + + mod mechanical_checks { + use super::*; + + #[test] + fn variant_length() { + use strum::IntoEnumIterator; + assert_eq!(ArtcraftCreditsPackSlug::all_variants().len(), ArtcraftCreditsPackSlug::iter().len()); + } + + #[test] + fn round_trip() { + for variant in ArtcraftCreditsPackSlug::all_variants() { + // Test to_str(), from_str(), Display, and Debug. + assert_eq!(variant, ArtcraftCreditsPackSlug::from_str(variant.to_str()).unwrap()); + assert_eq!(variant, ArtcraftCreditsPackSlug::from_str(&format!("{}", variant)).unwrap()); + assert_eq!(variant, ArtcraftCreditsPackSlug::from_str(&format!("{:?}", variant)).unwrap()); + } + } + + #[test] + fn serialized_length_ok_for_database() { + const MAX_LENGTH : usize = 16; + for variant in ArtcraftCreditsPackSlug::all_variants() { + let serialized = variant.to_str(); + assert!(serialized.len() > 0, "variant {:?} is too short", variant); + assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long", variant); + } + } + } +} diff --git a/crates/schema/public/enums/src/common/mod.rs b/crates/schema/public/enums/src/common/mod.rs index 7486ef408e..e75cc102ff 100644 --- a/crates/schema/public/enums/src/common/mod.rs +++ b/crates/schema/public/enums/src/common/mod.rs @@ -1,3 +1,4 @@ +pub mod artcraft_credits_pack_slug; pub mod artcraft_subscription_slug; pub mod generation_provider; pub mod job_status; diff --git a/crates/service/plugins/billing_artcraft/src/configs/credits_packs/get_artcraft_credits_pack_by_slug_and_env.rs b/crates/service/plugins/billing_artcraft/src/configs/credits_packs/get_artcraft_credits_pack_by_slug_and_env.rs new file mode 100644 index 0000000000..b3d23ec729 --- /dev/null +++ b/crates/service/plugins/billing_artcraft/src/configs/credits_packs/get_artcraft_credits_pack_by_slug_and_env.rs @@ -0,0 +1,15 @@ +use crate::configs::credits_packs::stripe_artcraft_credits_pack_info::StripeArtcraftCreditsPackInfo; +use crate::configs::credits_packs::stripe_artcraft_credits_pack_info_list::{ARTCRAFT_1000_PRODUCTION, ARTCRAFT_1000_SANDBOX, ARTCRAFT_2500_PRODUCTION, ARTCRAFT_2500_SANDBOX}; +use enums::common::artcraft_credits_pack_slug::ArtcraftCreditsPackSlug; +use reusable_types::server_environment::ServerEnvironment; + +pub fn get_artcraft_credits_pack_by_slug_and_env(slug: ArtcraftCreditsPackSlug, env: ServerEnvironment) -> StripeArtcraftCreditsPackInfo { + match (env, slug) { + // Development sandbox + (ServerEnvironment::Development, ArtcraftCreditsPackSlug::Artcraft1000) => ARTCRAFT_1000_SANDBOX, + (ServerEnvironment::Development, ArtcraftCreditsPackSlug::Artcraft2500) => ARTCRAFT_2500_SANDBOX, + // Production + (ServerEnvironment::Production, ArtcraftCreditsPackSlug::Artcraft1000) => ARTCRAFT_1000_PRODUCTION, + (ServerEnvironment::Production, ArtcraftCreditsPackSlug::Artcraft2500) => ARTCRAFT_2500_PRODUCTION, + } +} diff --git a/crates/service/plugins/billing_artcraft/src/configs/credits_packs/mod.rs b/crates/service/plugins/billing_artcraft/src/configs/credits_packs/mod.rs new file mode 100644 index 0000000000..3251e5ff0a --- /dev/null +++ b/crates/service/plugins/billing_artcraft/src/configs/credits_packs/mod.rs @@ -0,0 +1,3 @@ +pub mod get_artcraft_credits_pack_by_slug_and_env; +pub mod stripe_artcraft_credits_pack_info; +pub mod stripe_artcraft_credits_pack_info_list; \ No newline at end of file diff --git a/crates/service/plugins/billing_artcraft/src/configs/credits_packs/stripe_artcraft_credits_pack_info.rs b/crates/service/plugins/billing_artcraft/src/configs/credits_packs/stripe_artcraft_credits_pack_info.rs new file mode 100644 index 0000000000..da703acb9a --- /dev/null +++ b/crates/service/plugins/billing_artcraft/src/configs/credits_packs/stripe_artcraft_credits_pack_info.rs @@ -0,0 +1,9 @@ +use enums::common::artcraft_credits_pack_slug::ArtcraftCreditsPackSlug; + +#[derive(Debug, Clone)] +pub struct StripeArtcraftCreditsPackInfo { + pub slug: ArtcraftCreditsPackSlug, + pub product_id: &'static str, + pub price_id: &'static str, +} + diff --git a/crates/service/plugins/billing_artcraft/src/configs/credits_packs/stripe_artcraft_credits_pack_info_list.rs b/crates/service/plugins/billing_artcraft/src/configs/credits_packs/stripe_artcraft_credits_pack_info_list.rs new file mode 100644 index 0000000000..fdb92887be --- /dev/null +++ b/crates/service/plugins/billing_artcraft/src/configs/credits_packs/stripe_artcraft_credits_pack_info_list.rs @@ -0,0 +1,34 @@ +use crate::configs::credits_packs::stripe_artcraft_credits_pack_info::StripeArtcraftCreditsPackInfo; +use enums::common::artcraft_credits_pack_slug::ArtcraftCreditsPackSlug; + +// +// SANDBOX +// + +pub const ARTCRAFT_1000_SANDBOX : StripeArtcraftCreditsPackInfo = StripeArtcraftCreditsPackInfo { + slug: ArtcraftCreditsPackSlug::Artcraft1000, + product_id: "", + price_id: "", +}; + +pub const ARTCRAFT_2500_SANDBOX : StripeArtcraftCreditsPackInfo = StripeArtcraftCreditsPackInfo { + slug: ArtcraftCreditsPackSlug::Artcraft2500, + product_id: "", + price_id: "", +}; + +// +// PRODUCTION +// + +pub const ARTCRAFT_1000_PRODUCTION : StripeArtcraftCreditsPackInfo = StripeArtcraftCreditsPackInfo { + slug: ArtcraftCreditsPackSlug::Artcraft1000, + product_id: "FIXME_1", + price_id: "FIXME_2", +}; + +pub const ARTCRAFT_2500_PRODUCTION : StripeArtcraftCreditsPackInfo = StripeArtcraftCreditsPackInfo { + slug: ArtcraftCreditsPackSlug::Artcraft2500, + product_id: "FIXME_3", + price_id: "FIXME_4", +}; diff --git a/crates/service/plugins/billing_artcraft/src/configs/get_artcraft_product_by_stripe_id_and_env.rs b/crates/service/plugins/billing_artcraft/src/configs/get_artcraft_product_by_stripe_id_and_env.rs index 0c1dc13e7a..2e9e6ccd55 100644 --- a/crates/service/plugins/billing_artcraft/src/configs/get_artcraft_product_by_stripe_id_and_env.rs +++ b/crates/service/plugins/billing_artcraft/src/configs/get_artcraft_product_by_stripe_id_and_env.rs @@ -1,41 +1,54 @@ -use crate::configs::stripe_artcraft_product_info::StripeArtcraftProductInfo; -use crate::configs::stripe_artcraft_product_info_list::{ARTCRAFT_BASIC_PRODUCTION, ARTCRAFT_BASIC_SANDBOX, ARTCRAFT_MAX_PRODUCTION, ARTCRAFT_MAX_SANDBOX, ARTCRAFT_PRO_PRODUCTION, ARTCRAFT_PRO_SANDBOX}; +use crate::configs::credits_packs::stripe_artcraft_credits_pack_info::StripeArtcraftCreditsPackInfo; +use crate::configs::credits_packs::stripe_artcraft_credits_pack_info_list::{ARTCRAFT_1000_PRODUCTION, ARTCRAFT_1000_SANDBOX, ARTCRAFT_2500_PRODUCTION, ARTCRAFT_2500_SANDBOX}; +use crate::configs::stripe_artcraft_generic_product_info::StripeArtcraftGenericProductInfo; +use crate::configs::subscriptions::stripe_artcraft_subscription_info::StripeArtcraftSubscriptionInfo; +use crate::configs::subscriptions::stripe_artcraft_subscription_info_list::{ARTCRAFT_BASIC_PRODUCTION, ARTCRAFT_BASIC_SANDBOX, ARTCRAFT_MAX_PRODUCTION, ARTCRAFT_MAX_SANDBOX, ARTCRAFT_PRO_PRODUCTION, ARTCRAFT_PRO_SANDBOX}; use once_cell::sync::Lazy; use reusable_types::server_environment::ServerEnvironment; use std::collections::HashMap; - -static PRODUCTION_PRODUCTS_BY_STRIPE_ID : Lazy> = Lazy::new(|| { +static PRODUCTION_PRODUCTS_BY_STRIPE_ID : Lazy> = Lazy::new(|| { let mut plans = HashMap::new(); - - fn add_plan(plans: &mut HashMap, product: &StripeArtcraftProductInfo) { - plans.insert(product.product_id.to_string(), product.clone()); - } - + + // Subscriptions add_plan(&mut plans, &ARTCRAFT_BASIC_PRODUCTION); add_plan(&mut plans, &ARTCRAFT_PRO_PRODUCTION); add_plan(&mut plans, &ARTCRAFT_MAX_PRODUCTION); + // One-off payments + add_credits(&mut plans, &ARTCRAFT_1000_PRODUCTION); + add_credits(&mut plans, &ARTCRAFT_2500_PRODUCTION); + plans }); -static SANDBOX_PRODUCTS_BY_STRIPE_ID : Lazy> = Lazy::new(|| { +static SANDBOX_PRODUCTS_BY_STRIPE_ID : Lazy> = Lazy::new(|| { let mut plans = HashMap::new(); - fn add_plan(plans: &mut HashMap, product: &StripeArtcraftProductInfo) { - plans.insert(product.product_id.to_string(), product.clone()); - } - + // Subscriptions add_plan(&mut plans, &ARTCRAFT_BASIC_SANDBOX); add_plan(&mut plans, &ARTCRAFT_PRO_SANDBOX); add_plan(&mut plans, &ARTCRAFT_MAX_SANDBOX); + // One-off payments + add_credits(&mut plans, &ARTCRAFT_1000_SANDBOX); + add_credits(&mut plans, &ARTCRAFT_2500_SANDBOX); + plans }); -pub fn get_artcraft_product_by_stripe_id_and_env(stripe_id: &str, env: ServerEnvironment) -> Option<&StripeArtcraftProductInfo> { +pub fn get_artcraft_product_by_stripe_id_and_env(stripe_id: &str, env: ServerEnvironment) -> Option<&StripeArtcraftGenericProductInfo> { match env { ServerEnvironment::Development => SANDBOX_PRODUCTS_BY_STRIPE_ID.get(stripe_id), ServerEnvironment::Production => PRODUCTION_PRODUCTS_BY_STRIPE_ID.get(stripe_id), } } + +fn add_plan(plans: &mut HashMap, product: &StripeArtcraftSubscriptionInfo) { + plans.insert(product.product_id.to_string(), StripeArtcraftGenericProductInfo::Subscription(product.clone())); +} + +fn add_credits(plans: &mut HashMap, product: &StripeArtcraftCreditsPackInfo) { + plans.insert(product.product_id.to_string(), StripeArtcraftGenericProductInfo::CreditsPack(product.clone())); +} + diff --git a/crates/service/plugins/billing_artcraft/src/configs/mod.rs b/crates/service/plugins/billing_artcraft/src/configs/mod.rs index 6b2ef562b3..35ae4496ff 100644 --- a/crates/service/plugins/billing_artcraft/src/configs/mod.rs +++ b/crates/service/plugins/billing_artcraft/src/configs/mod.rs @@ -1,5 +1,5 @@ -pub mod get_artcraft_product_by_slug_and_env; +pub mod credits_packs; pub mod get_artcraft_product_by_stripe_id_and_env; +pub mod stripe_artcraft_generic_product_info; pub mod stripe_artcraft_metadata_keys; -pub mod stripe_artcraft_product_info; -pub mod stripe_artcraft_product_info_list; +pub mod subscriptions; diff --git a/crates/service/plugins/billing_artcraft/src/configs/stripe_artcraft_generic_product_info.rs b/crates/service/plugins/billing_artcraft/src/configs/stripe_artcraft_generic_product_info.rs new file mode 100644 index 0000000000..9800cc0a6c --- /dev/null +++ b/crates/service/plugins/billing_artcraft/src/configs/stripe_artcraft_generic_product_info.rs @@ -0,0 +1,8 @@ +use crate::configs::credits_packs::stripe_artcraft_credits_pack_info::StripeArtcraftCreditsPackInfo; +use crate::configs::subscriptions::stripe_artcraft_subscription_info::StripeArtcraftSubscriptionInfo; + +#[derive(Debug, Clone)] +pub enum StripeArtcraftGenericProductInfo { + CreditsPack(StripeArtcraftCreditsPackInfo), + Subscription(StripeArtcraftSubscriptionInfo), +} diff --git a/crates/service/plugins/billing_artcraft/src/configs/get_artcraft_product_by_slug_and_env.rs b/crates/service/plugins/billing_artcraft/src/configs/subscriptions/get_artcraft_subscription_by_slug_and_env.rs similarity index 63% rename from crates/service/plugins/billing_artcraft/src/configs/get_artcraft_product_by_slug_and_env.rs rename to crates/service/plugins/billing_artcraft/src/configs/subscriptions/get_artcraft_subscription_by_slug_and_env.rs index 99ac2cd318..a681d434e4 100644 --- a/crates/service/plugins/billing_artcraft/src/configs/get_artcraft_product_by_slug_and_env.rs +++ b/crates/service/plugins/billing_artcraft/src/configs/subscriptions/get_artcraft_subscription_by_slug_and_env.rs @@ -1,9 +1,9 @@ -use crate::configs::stripe_artcraft_product_info::StripeArtcraftProductInfo; -use crate::configs::stripe_artcraft_product_info_list::{ARTCRAFT_BASIC_PRODUCTION, ARTCRAFT_BASIC_SANDBOX, ARTCRAFT_MAX_PRODUCTION, ARTCRAFT_MAX_SANDBOX, ARTCRAFT_PRO_PRODUCTION, ARTCRAFT_PRO_SANDBOX}; +use crate::configs::subscriptions::stripe_artcraft_subscription_info::StripeArtcraftSubscriptionInfo; +use crate::configs::subscriptions::stripe_artcraft_subscription_info_list::{ARTCRAFT_BASIC_PRODUCTION, ARTCRAFT_BASIC_SANDBOX, ARTCRAFT_MAX_PRODUCTION, ARTCRAFT_MAX_SANDBOX, ARTCRAFT_PRO_PRODUCTION, ARTCRAFT_PRO_SANDBOX}; use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug; use reusable_types::server_environment::ServerEnvironment; -pub fn get_artcraft_product_by_slug_and_env(slug: ArtcraftSubscriptionSlug, env: ServerEnvironment) -> StripeArtcraftProductInfo { +pub fn get_artcraft_subscription_by_slug_and_env(slug: ArtcraftSubscriptionSlug, env: ServerEnvironment) -> StripeArtcraftSubscriptionInfo { match (env, slug) { // Development sandbox (ServerEnvironment::Development, ArtcraftSubscriptionSlug::ArtcraftBasic) => ARTCRAFT_BASIC_SANDBOX, diff --git a/crates/service/plugins/billing_artcraft/src/configs/subscriptions/mod.rs b/crates/service/plugins/billing_artcraft/src/configs/subscriptions/mod.rs new file mode 100644 index 0000000000..5f8b57e5a0 --- /dev/null +++ b/crates/service/plugins/billing_artcraft/src/configs/subscriptions/mod.rs @@ -0,0 +1,3 @@ +pub mod get_artcraft_subscription_by_slug_and_env; +pub mod stripe_artcraft_subscription_info; +pub mod stripe_artcraft_subscription_info_list; \ No newline at end of file diff --git a/crates/service/plugins/billing_artcraft/src/configs/stripe_artcraft_product_info.rs b/crates/service/plugins/billing_artcraft/src/configs/subscriptions/stripe_artcraft_subscription_info.rs similarity index 84% rename from crates/service/plugins/billing_artcraft/src/configs/stripe_artcraft_product_info.rs rename to crates/service/plugins/billing_artcraft/src/configs/subscriptions/stripe_artcraft_subscription_info.rs index 4687e39e33..cbb30f066a 100644 --- a/crates/service/plugins/billing_artcraft/src/configs/stripe_artcraft_product_info.rs +++ b/crates/service/plugins/billing_artcraft/src/configs/subscriptions/stripe_artcraft_subscription_info.rs @@ -1,7 +1,7 @@ use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug; #[derive(Debug, Clone)] -pub struct StripeArtcraftProductInfo { +pub struct StripeArtcraftSubscriptionInfo { pub slug: ArtcraftSubscriptionSlug, pub product_id: &'static str, pub monthly_price_id: &'static str, diff --git a/crates/service/plugins/billing_artcraft/src/configs/stripe_artcraft_product_info_list.rs b/crates/service/plugins/billing_artcraft/src/configs/subscriptions/stripe_artcraft_subscription_info_list.rs similarity index 64% rename from crates/service/plugins/billing_artcraft/src/configs/stripe_artcraft_product_info_list.rs rename to crates/service/plugins/billing_artcraft/src/configs/subscriptions/stripe_artcraft_subscription_info_list.rs index 5d10283e61..b09d815f21 100644 --- a/crates/service/plugins/billing_artcraft/src/configs/stripe_artcraft_product_info_list.rs +++ b/crates/service/plugins/billing_artcraft/src/configs/subscriptions/stripe_artcraft_subscription_info_list.rs @@ -1,25 +1,25 @@ -use crate::configs::stripe_artcraft_product_info::StripeArtcraftProductInfo; +use crate::configs::subscriptions::stripe_artcraft_subscription_info::StripeArtcraftSubscriptionInfo; use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug; // // SANDBOX // -pub const ARTCRAFT_BASIC_SANDBOX : StripeArtcraftProductInfo = StripeArtcraftProductInfo { +pub const ARTCRAFT_BASIC_SANDBOX : StripeArtcraftSubscriptionInfo = StripeArtcraftSubscriptionInfo { slug: ArtcraftSubscriptionSlug::ArtcraftBasic, product_id: "prod_SyJURfbu2ixP7M", monthly_price_id: "price_1S2MrSEobp4xy4Tlwit8aeNE", yearly_price_id: "price_1S2MrsEobp4xy4TlDG2skTkc", }; -pub const ARTCRAFT_PRO_SANDBOX : StripeArtcraftProductInfo = StripeArtcraftProductInfo { +pub const ARTCRAFT_PRO_SANDBOX : StripeArtcraftSubscriptionInfo = StripeArtcraftSubscriptionInfo { slug: ArtcraftSubscriptionSlug::ArtcraftPro, product_id: "prod_SyJTy5arqKmaxT", monthly_price_id: "price_1S2MqZEobp4xy4TlR9KLyitD", yearly_price_id: "price_1S2MqyEobp4xy4Tl0ZXOK8Z0", }; -pub const ARTCRAFT_MAX_SANDBOX : StripeArtcraftProductInfo = StripeArtcraftProductInfo { +pub const ARTCRAFT_MAX_SANDBOX : StripeArtcraftSubscriptionInfo = StripeArtcraftSubscriptionInfo { slug: ArtcraftSubscriptionSlug::ArtcraftMax, product_id: "prod_SyJSoHNUU81BjU", monthly_price_id: "price_1S2MpGEobp4xy4TldyO9lAwd", @@ -30,21 +30,21 @@ pub const ARTCRAFT_MAX_SANDBOX : StripeArtcraftProductInfo = StripeArtcraftProdu // PRODUCTION // -pub const ARTCRAFT_BASIC_PRODUCTION : StripeArtcraftProductInfo = StripeArtcraftProductInfo { +pub const ARTCRAFT_BASIC_PRODUCTION : StripeArtcraftSubscriptionInfo = StripeArtcraftSubscriptionInfo { slug: ArtcraftSubscriptionSlug::ArtcraftBasic, product_id: "prod_SyIXACAGqPbsoG", monthly_price_id: "price_1S2LwbIaZEzwFveeYqYxgrV7", yearly_price_id: "price_1S2Lz8IaZEzwFvee40D7PFoa", }; -pub const ARTCRAFT_PRO_PRODUCTION : StripeArtcraftProductInfo = StripeArtcraftProductInfo { +pub const ARTCRAFT_PRO_PRODUCTION : StripeArtcraftSubscriptionInfo = StripeArtcraftSubscriptionInfo { slug: ArtcraftSubscriptionSlug::ArtcraftPro, product_id: "prod_SyIfqs9Rmv3Fdu", monthly_price_id: "price_1S2M4FIaZEzwFveeRoqYDbDw", yearly_price_id: "price_1S2M5VIaZEzwFveeLAXk3hL8", }; -pub const ARTCRAFT_MAX_PRODUCTION : StripeArtcraftProductInfo = StripeArtcraftProductInfo { +pub const ARTCRAFT_MAX_PRODUCTION : StripeArtcraftSubscriptionInfo = StripeArtcraftSubscriptionInfo { slug: ArtcraftSubscriptionSlug::ArtcraftMax, product_id: "prod_SyJ6VIFnc3riPp", monthly_price_id: "price_1S2MUSIaZEzwFveesCPf2XjP", diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/checkout/mod.rs b/crates/service/plugins/billing_artcraft/src/endpoints/checkout/mod.rs index 814bc35488..da33228df2 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/checkout/mod.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/checkout/mod.rs @@ -1 +1,2 @@ -pub mod stripe_artcraft_create_subscription_checkout_session_handler; \ No newline at end of file +pub mod stripe_artcraft_create_credits_pack_checkout_handler; +pub mod stripe_artcraft_create_subscription_checkout_handler; \ No newline at end of file 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 new file mode 100644 index 0000000000..283dcd5f9f --- /dev/null +++ b/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_credits_pack_checkout_handler.rs @@ -0,0 +1,165 @@ +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::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_credits_pack_checkout::{StripeArtcraftCreateCreditsPackCheckoutRequest, StripeArtcraftCreateCreditsPackCheckoutResponse}; +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; +use enums::common::subscription_namespace::SubscriptionNamespace; +use log::{error, info, warn}; +use reusable_types::server_environment::ServerEnvironment; +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; +use stripe_checkout::checkout_session::{CreateCheckoutSession, CreateCheckoutSessionAutomaticTax, CreateCheckoutSessionLineItems, CreateCheckoutSessionPaymentIntentData, CreateCheckoutSessionSubscriptionData}; +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. +// #[utoipa::path( +// get, +// tag = "Stripe (Artcraft)", +// path = "/v1/stripe_artcraft/checkout/credits_pack", +// params( +// ("request" = CreateCheckoutSessionRequest, description = "Payload for Request"), +// ), +// responses( +// (status = 200, description = "Success Delete", body = CreateCheckoutSessionSuccessResponse), +// ), +// )] +pub async fn stripe_artcraft_create_credits_pack_checkout_handler( + http_request: HttpRequest, + request: Json, + stripe_config: Data, + server_environment: Data, + internal_user_lookup: Data, + internal_session_cache_purge: Data, +) -> Result, CommonWebError> +{ + let slug = match request.credits_pack { + None => return Err(CommonWebError::BadInputWithSimpleMessage("no credits pack supplied".to_string())), + Some(slug) => slug, + }; + + let credits_pack = get_artcraft_credits_pack_by_slug_and_env(slug, **server_environment); + + 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 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::Payment) + .line_items(vec![ + CreateCheckoutSessionLineItems { + price: Some(credits_pack.price_id.to_string()), + quantity: None, + ..Default::default() + } + ]) + .allow_promotion_codes(true) + .automatic_tax(CreateCheckoutSessionAutomaticTax { + enabled: true, + liability: None, + }) + .metadata(metadata.clone()) + .payment_intent_data(CreateCheckoutSessionPaymentIntentData { + metadata: Some(metadata), + ..Default::default() + }); + + if let Some(existing_stripe_customer_id) = user_metadata.maybe_existing_stripe_customer_id.as_deref() { + match CustomerId::from_str(existing_stripe_customer_id) { + Ok(customer_id) => { + checkout_builder = checkout_builder.customer(customer_id); + } + Err(err) => { + // NB: Don't block checkout. + warn!("Error parsing user's ({}) supposed existing stripe customer id: {:?}", + &user_metadata.user_token, + err); + } + } + } + + let checkout_session = checkout_builder + .send(&stripe_config.client) + .await + .map_err(|err| { + error!("Stripe Error: {:?}", err); + CommonWebError::ServerError + })?; + + checkout_session + }; + + 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(StripeArtcraftCreateCreditsPackCheckoutResponse{ + success: true, + stripe_checkout_redirect_url: url, + })) +} diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_subscription_checkout_session_handler.rs b/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_subscription_checkout_handler.rs similarity index 92% rename from crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_subscription_checkout_session_handler.rs rename to crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_subscription_checkout_handler.rs index 47e093f290..a3e3a05a09 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_subscription_checkout_session_handler.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_subscription_checkout_handler.rs @@ -1,10 +1,10 @@ -use crate::configs::get_artcraft_product_by_slug_and_env::get_artcraft_product_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::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, StripeArtcraftCreateCheckoutSessionRequest, StripeArtcraftCreateCheckoutSessionResponse}; +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; use enums::common::subscription_namespace::SubscriptionNamespace; @@ -32,14 +32,14 @@ use user_traits_component::traits::internal_session_cache_purge::InternalSession // (status = 200, description = "Success Delete", body = CreateCheckoutSessionSuccessResponse), // ), // )] -pub async fn stripe_artcraft_create_checkout_session_handler( +pub async fn stripe_artcraft_create_subscription_session_handler( http_request: HttpRequest, - request: Json, + request: Json, stripe_config: Data, server_environment: Data, internal_user_lookup: Data, internal_session_cache_purge: Data, -) -> Result, CommonWebError> +) -> Result, CommonWebError> { let slug = match request.plan { None => return Err(CommonWebError::BadInputWithSimpleMessage("no plan supplied".to_string())), @@ -51,7 +51,7 @@ pub async fn stripe_artcraft_create_checkout_session_handler( Some(cadence) => cadence, }; - let plan = get_artcraft_product_by_slug_and_env(slug, **server_environment); + let plan = get_artcraft_subscription_by_slug_and_env(slug, **server_environment); let price_id = match cadence { PlanBillingCadence::Monthly => plan.monthly_price_id.clone(), @@ -180,7 +180,7 @@ pub async fn stripe_artcraft_create_checkout_session_handler( // Best effort to delete Redis session cache internal_session_cache_purge.best_effort_purge_session_cache(&http_request); - Ok(Json(StripeArtcraftCreateCheckoutSessionResponse { + Ok(Json(StripeArtcraftCreateSubscriptionCheckoutResponse { success: true, stripe_checkout_redirect_url: url, })) diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_created_handler.rs b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_created_handler.rs index 54ac96e821..eb4be282b3 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_created_handler.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_created_handler.rs @@ -1,5 +1,5 @@ -use crate::configs::get_artcraft_product_by_slug_and_env::get_artcraft_product_by_slug_and_env; use crate::configs::get_artcraft_product_by_stripe_id_and_env::get_artcraft_product_by_stripe_id_and_env; +use crate::configs::stripe_artcraft_generic_product_info::StripeArtcraftGenericProductInfo; use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::calculate_subscription_end_date::calculate_subscription_end_date; use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::subscription_event_extractor::subscription_summary_extractor; use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_error::StripeArtcraftWebhookError; @@ -59,12 +59,17 @@ pub async fn customer_subscription_created_handler( &summary.stripe_product_id, server_environment); let product = match maybe_product { + Some(StripeArtcraftGenericProductInfo::Subscription(subscription)) => subscription, + Some(StripeArtcraftGenericProductInfo::CreditsPack(credits_pack)) => { + error!("Received 'customer.subscription.updated' for a credits pack product ({}). This should not happen.", &credits_pack.slug.to_str()); + result.should_ignore_retry = true; + return Ok(result); + } None => { - return Err(StripeArtcraftWebhookError::BadRequest( - format!("No matching product for stripe product ID: {}", &summary.stripe_product_id) - )); + error!("No matching product for stripe product ID: {}", &summary.stripe_product_id); + result.should_ignore_retry = true; + return Ok(result); } - Some(product) => product, }; if let Some(user_token) = summary.user_token.as_deref() { diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_deleted_handler.rs b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_deleted_handler.rs index 907218f31c..308caa1ee2 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_deleted_handler.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_deleted_handler.rs @@ -1,4 +1,5 @@ use crate::configs::get_artcraft_product_by_stripe_id_and_env::get_artcraft_product_by_stripe_id_and_env; +use crate::configs::stripe_artcraft_generic_product_info::StripeArtcraftGenericProductInfo; use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::calculate_subscription_end_date::calculate_subscription_end_date; use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::subscription_event_extractor::subscription_summary_extractor; use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_error::StripeArtcraftWebhookError; @@ -9,8 +10,8 @@ use mysql_queries::queries::users::user_subscriptions::get_user_subscription_by_ use mysql_queries::queries::users::user_subscriptions::upsert_user_subscription_by_stripe_id::UpsertUserSubscription; use reusable_types::server_environment::ServerEnvironment; use reusable_types::stripe::stripe_subscription_status::StripeSubscriptionStatus; -use sqlx::{MySql, MySqlPool}; use sqlx::pool::PoolConnection; +use sqlx::{MySql, MySqlPool}; use stripe_shared::Subscription; /// Handle event type: 'customer.subscription.deleted' @@ -20,6 +21,7 @@ pub async fn customer_subscription_deleted_handler( server_environment: ServerEnvironment, mysql_connection: &mut PoolConnection, ) -> Result { + let summary = subscription_summary_extractor(subscription) .map_err(|err| { let reason = format!("Error extracting subscription from 'customer.subscription.deleted' payload: {:?}", err); @@ -41,12 +43,17 @@ pub async fn customer_subscription_deleted_handler( &summary.stripe_product_id, server_environment); let product = match maybe_product { + Some(StripeArtcraftGenericProductInfo::Subscription(subscription)) => subscription, + Some(StripeArtcraftGenericProductInfo::CreditsPack(credits_pack)) => { + error!("Received 'customer.subscription.updated' for a credits pack product ({}). This should not happen.", &credits_pack.slug.to_str()); + result.should_ignore_retry = true; + return Ok(result); + } None => { error!("No matching product for stripe product ID: {}", &summary.stripe_product_id); result.should_ignore_retry = true; return Ok(result); } - Some(product) => product, }; // NB: It's possible to receive events out of order. diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_updated_handler.rs b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_updated_handler.rs index d18f6906d1..6168bd6763 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_updated_handler.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_updated_handler.rs @@ -13,6 +13,8 @@ use reusable_types::stripe::stripe_subscription_status::StripeSubscriptionStatus use sqlx::pool::PoolConnection; use sqlx::{MySql, MySqlPool}; use stripe_shared::Subscription; +use crate::configs::stripe_artcraft_generic_product_info::StripeArtcraftGenericProductInfo; +use crate::configs::subscriptions::get_artcraft_subscription_by_slug_and_env::get_artcraft_subscription_by_slug_and_env; /// Handle event type: 'customer.subscription.updated' pub async fn customer_subscription_updated_handler( @@ -42,12 +44,17 @@ pub async fn customer_subscription_updated_handler( &summary.stripe_product_id, server_environment); let product = match maybe_product { + Some(StripeArtcraftGenericProductInfo::Subscription(subscription)) => subscription, + Some(StripeArtcraftGenericProductInfo::CreditsPack(credits_pack)) => { + error!("Received 'customer.subscription.updated' for a credits pack product ({}). This should not happen.", &credits_pack.slug.to_str()); + result.should_ignore_retry = true; + return Ok(result); + } None => { error!("No matching product for stripe product ID: {}", &summary.stripe_product_id); result.should_ignore_retry = true; return Ok(result); } - Some(product) => product, }; // NB: It's possible to receive events out of order. 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 c7ca0406ff..eb0801c065 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 @@ -2,7 +2,8 @@ use actix_http::body::MessageBody; use actix_service::ServiceFactory; use actix_web::dev::{ServiceRequest, ServiceResponse}; use actix_web::{web, App, Error, HttpResponse}; -use billing_artcraft_component::endpoints::checkout::stripe_artcraft_create_subscription_checkout_session_handler::stripe_artcraft_create_checkout_session_handler; +use billing_artcraft_component::endpoints::checkout::stripe_artcraft_create_credits_pack_checkout_handler::stripe_artcraft_create_credits_pack_checkout_handler; +use billing_artcraft_component::endpoints::checkout::stripe_artcraft_create_subscription_checkout_handler::stripe_artcraft_create_subscription_session_handler; use billing_artcraft_component::endpoints::webhook::stripe_artcraft_webhook_handler::stripe_artcraft_webhook_handler; pub fn add_stripe_artcraft_routes (app: App) -> App @@ -21,8 +22,12 @@ where .route(web::post().to(stripe_artcraft_webhook_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) - .service(web::resource("/checkout/create_subscription") - .route(web::post().to(stripe_artcraft_create_checkout_session_handler)) + .service(web::resource("/checkout/credits_pack") + .route(web::post().to(stripe_artcraft_create_credits_pack_checkout_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/checkout/subscription") + .route(web::post().to(stripe_artcraft_create_subscription_session_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) )