rework handlers to add credits

This commit is contained in:
Brandon Thomas committed 2025-09-12 01:00:03 -04:00
1 parent 65f36704cf
commit c8651fa2bc
28 files changed
+558 -56

No files matched your search

@@ -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<StripeArtcraftCreateCreditsPackCheckoutResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
CREATE_CREDITS_PACK_CHECKOUT_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -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<StripeArtcraftCreateCheckoutSessionResponse, StorytellerError> {
request: StripeArtcraftCreateSubscriptionCheckoutRequest,
) -> Result<StripeArtcraftCreateSubscriptionCheckoutResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
CREATE_SUBSCRIPTION_CHECKOUT_URL_PATH,
@@ -1 +1,2 @@
pub mod create_subscription_checkout;
pub mod create_credits_pack_checkout;
pub mod create_subscription_checkout;
@@ -1 +1,2 @@
pub mod storyteller_open_billing_command;
pub mod storyteller_open_billing_command;
pub mod storyteller_open_credits_purchase_command;
@@ -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<ArtcraftCreditsPackSlug>,
}
#[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<String, String> {
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<ArtcraftCreditsPackSlug>,
) -> 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(())
}
@@ -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<ArtcraftCreditsPackSlug>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCreateCreditsPackCheckoutResponse {
pub success: bool,
pub stripe_checkout_redirect_url: String,
}
@@ -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<ArtcraftSubscriptionSlug>,
@@ -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,
}
@@ -1 +1,2 @@
pub mod create_credits_pack_checkout;
pub mod create_subscription_checkout;
@@ -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<Self, String> {
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<Self> {
// 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);
}
}
}
}
@@ -1,3 +1,4 @@
pub mod artcraft_credits_pack_slug;
pub mod artcraft_subscription_slug;
pub mod generation_provider;
pub mod job_status;
@@ -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,
}
}
@@ -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;
@@ -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,
}
@@ -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",
};
@@ -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<HashMap<String, StripeArtcraftProductInfo>> = Lazy::new(|| {
static PRODUCTION_PRODUCTS_BY_STRIPE_ID : Lazy<HashMap<String, StripeArtcraftGenericProductInfo>> = Lazy::new(|| {
let mut plans = HashMap::new();
fn add_plan(plans: &mut HashMap<String, StripeArtcraftProductInfo>, 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<HashMap<String, StripeArtcraftProductInfo>> = Lazy::new(|| {
static SANDBOX_PRODUCTS_BY_STRIPE_ID : Lazy<HashMap<String, StripeArtcraftGenericProductInfo>> = Lazy::new(|| {
let mut plans = HashMap::new();
fn add_plan(plans: &mut HashMap<String, StripeArtcraftProductInfo>, 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<String, StripeArtcraftGenericProductInfo>, product: &StripeArtcraftSubscriptionInfo) {
plans.insert(product.product_id.to_string(), StripeArtcraftGenericProductInfo::Subscription(product.clone()));
}
fn add_credits(plans: &mut HashMap<String, StripeArtcraftGenericProductInfo>, product: &StripeArtcraftCreditsPackInfo) {
plans.insert(product.product_id.to_string(), StripeArtcraftGenericProductInfo::CreditsPack(product.clone()));
}
@@ -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;
@@ -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),
}
@@ -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,
@@ -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;
@@ -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,
@@ -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",
@@ -1 +1,2 @@
pub mod stripe_artcraft_create_subscription_checkout_session_handler;
pub mod stripe_artcraft_create_credits_pack_checkout_handler;
pub mod stripe_artcraft_create_subscription_checkout_handler;
@@ -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<StripeArtcraftCreateCreditsPackCheckoutRequest>,
stripe_config: Data<ArtcraftStripeConfigWithClient>,
server_environment: Data<ServerEnvironment>,
internal_user_lookup: Data<dyn InternalUserLookup>,
internal_session_cache_purge: Data<dyn InternalSessionCachePurge>,
) -> Result<Json<StripeArtcraftCreateCreditsPackCheckoutResponse>, 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,
}))
}
@@ -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<StripeArtcraftCreateCheckoutSessionRequest>,
request: Json<StripeArtcraftCreateSubscriptionCheckoutRequest>,
stripe_config: Data<ArtcraftStripeConfigWithClient>,
server_environment: Data<ServerEnvironment>,
internal_user_lookup: Data<dyn InternalUserLookup>,
internal_session_cache_purge: Data<dyn InternalSessionCachePurge>,
) -> Result<Json<StripeArtcraftCreateCheckoutSessionResponse>, CommonWebError>
) -> Result<Json<StripeArtcraftCreateSubscriptionCheckoutResponse>, 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,
}))
@@ -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() {
@@ -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<MySql>,
) -> Result<StripeArtcraftWebhookSummary, StripeArtcraftWebhookError> {
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.
@@ -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.
@@ -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<T, B> (app: App<T>) -> App<T>
@@ -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()))
)
)