mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-09 11:31:27 +00:00
omg, this yak shave worked.
This commit is contained in:
1 parent
35554c6dde
commit
2b7cc30bbe
12 files changed
+345
-5
No files matched your search
Generated
+19
@@ -973,6 +973,20 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stripe-checkout"
|
||||
version = "1.0.0-alpha.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b115740c799730d4caac099d48be4908d2d99611d56a874d2d3555b5d4306a97"
|
||||
dependencies = [
|
||||
"async-stripe-client-core",
|
||||
"async-stripe-shared",
|
||||
"async-stripe-types",
|
||||
"miniserde",
|
||||
"serde",
|
||||
"smol_str 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stripe-client-core"
|
||||
version = "1.0.0-alpha.2"
|
||||
@@ -1322,21 +1336,26 @@ dependencies = [
|
||||
"actix-http",
|
||||
"actix-web",
|
||||
"anyhow",
|
||||
"artcraft_api_defs",
|
||||
"async-stripe 1.0.0-alpha.2",
|
||||
"async-stripe-checkout",
|
||||
"async-stripe-core",
|
||||
"async-stripe-webhook",
|
||||
"chrono",
|
||||
"enums",
|
||||
"errors",
|
||||
"http_server_common",
|
||||
"log",
|
||||
"mockall",
|
||||
"mysql_queries",
|
||||
"once_cell",
|
||||
"reusable_types",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"user_traits_component",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ normal = ["workspace-build-acceleration"]
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
artcraft_api_defs.workspace = true
|
||||
enums.workspace = true
|
||||
errors.workspace = true
|
||||
http_server_common = { path = "../../../lib/deprecated/http_server_common" }
|
||||
mysql_queries = { path = "../../../schema/database/mysql_queries" }
|
||||
#reusable_types = { path = "../../../schema/public/reusable_types" }
|
||||
reusable_types = { path = "../../../schema/public/reusable_types" }
|
||||
user_traits_component = { path = "../user_traits_component" }
|
||||
#url_config = { path = "../../../lib/url_config" }
|
||||
#user_traits_component = { path = "../user_traits_component" }
|
||||
|
||||
# External
|
||||
actix-http.workspace = true
|
||||
@@ -45,6 +47,7 @@ sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls",
|
||||
async-stripe = { version = "1.0.0-alpha.2", features = [] }
|
||||
async-stripe-core = { version = "1.0.0-alpha.2", features = ["customer"] }
|
||||
async-stripe-webhook = { version = "1.0.0-alpha.2", features = [] }
|
||||
async-stripe-checkout = { version = "1.0.0-alpha.2", features = ["checkout_session"] }
|
||||
|
||||
#sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
|
||||
#utoipa = { version = "4", features = ["chrono","actix_extras"] }
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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 enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug;
|
||||
use reusable_types::server_environment::ServerEnvironment;
|
||||
|
||||
pub fn get_artcraft_product_by_slug(slug: ArtcraftSubscriptionSlug, env: ServerEnvironment) -> StripeArtcraftProductInfo {
|
||||
match (env, slug) {
|
||||
// Development sandbox
|
||||
(ServerEnvironment::Development, ArtcraftSubscriptionSlug::ArtcraftBasic) => ARTCRAFT_BASIC_SANDBOX,
|
||||
(ServerEnvironment::Development, ArtcraftSubscriptionSlug::ArtcraftPro) => ARTCRAFT_PRO_SANDBOX,
|
||||
(ServerEnvironment::Development, ArtcraftSubscriptionSlug::ArtcraftMax) => ARTCRAFT_MAX_SANDBOX,
|
||||
// Production
|
||||
(ServerEnvironment::Production, ArtcraftSubscriptionSlug::ArtcraftBasic) => ARTCRAFT_BASIC_PRODUCTION,
|
||||
(ServerEnvironment::Production, ArtcraftSubscriptionSlug::ArtcraftPro) => ARTCRAFT_PRO_PRODUCTION,
|
||||
(ServerEnvironment::Production, ArtcraftSubscriptionSlug::ArtcraftMax) => ARTCRAFT_MAX_PRODUCTION,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod get_artcraft_product_by_slug;
|
||||
pub mod stripe_artcraft_metadata_keys;
|
||||
pub mod stripe_artcraft_product_info;
|
||||
pub mod stripe_artcraft_product_info_list;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
pub const STRIPE_ARTCRAFT_METADATA_USER_TOKEN : &str = "user_token";
|
||||
|
||||
pub const STRIPE_ARTCRAFT_METADATA_USERNAME : &str = "username";
|
||||
|
||||
pub const STRIPE_ARTCRAFT_METADATA_EMAIL : &str = "email";
|
||||
@@ -0,0 +1,9 @@
|
||||
use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug;
|
||||
|
||||
pub struct StripeArtcraftProductInfo {
|
||||
pub slug: ArtcraftSubscriptionSlug,
|
||||
pub product_id: &'static str,
|
||||
pub monthly_price_id: &'static str,
|
||||
pub yearly_price_id: &'static str,
|
||||
}
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
use crate::configs::stripe_artcraft_product_info::StripeArtcraftProductInfo;
|
||||
use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug;
|
||||
|
||||
//
|
||||
// SANDBOX
|
||||
//
|
||||
|
||||
pub const ARTCRAFT_BASIC_SANDBOX : StripeArtcraftProductInfo = StripeArtcraftProductInfo {
|
||||
slug: ArtcraftSubscriptionSlug::ArtcraftBasic,
|
||||
product_id: "prod_SyJURfbu2ixP7M",
|
||||
monthly_price_id: "price_1S2MrSEobp4xy4Tlwit8aeNE",
|
||||
yearly_price_id: "price_1S2MrsEobp4xy4TlDG2skTkc",
|
||||
};
|
||||
|
||||
pub const ARTCRAFT_PRO_SANDBOX : StripeArtcraftProductInfo = StripeArtcraftProductInfo {
|
||||
slug: ArtcraftSubscriptionSlug::ArtcraftPro,
|
||||
product_id: "prod_SyJTy5arqKmaxT",
|
||||
monthly_price_id: "price_1S2MqZEobp4xy4TlR9KLyitD",
|
||||
yearly_price_id: "price_1S2MqyEobp4xy4Tl0ZXOK8Z0",
|
||||
};
|
||||
|
||||
pub const ARTCRAFT_MAX_SANDBOX : StripeArtcraftProductInfo = StripeArtcraftProductInfo {
|
||||
slug: ArtcraftSubscriptionSlug::ArtcraftMax,
|
||||
product_id: "prod_SyJSoHNUU81BjU",
|
||||
monthly_price_id: "price_1S2MpGEobp4xy4TldyO9lAwd",
|
||||
yearly_price_id: "price_1S2MppEobp4xy4Tln6xjTKKD",
|
||||
};
|
||||
|
||||
//
|
||||
// PRODUCTION
|
||||
//
|
||||
|
||||
pub const ARTCRAFT_BASIC_PRODUCTION : StripeArtcraftProductInfo = StripeArtcraftProductInfo {
|
||||
slug: ArtcraftSubscriptionSlug::ArtcraftBasic,
|
||||
product_id: "prod_SyIXACAGqPbsoG",
|
||||
monthly_price_id: "price_1S2LwbIaZEzwFveeYqYxgrV7",
|
||||
yearly_price_id: "price_1S2Lz8IaZEzwFvee40D7PFoa",
|
||||
};
|
||||
|
||||
pub const ARTCRAFT_PRO_PRODUCTION : StripeArtcraftProductInfo = StripeArtcraftProductInfo {
|
||||
slug: ArtcraftSubscriptionSlug::ArtcraftPro,
|
||||
product_id: "prod_SyIfqs9Rmv3Fdu",
|
||||
monthly_price_id: "price_1S2M4FIaZEzwFveeRoqYDbDw",
|
||||
yearly_price_id: "price_1S2M5VIaZEzwFveeLAXk3hL8",
|
||||
};
|
||||
|
||||
pub const ARTCRAFT_MAX_PRODUCTION : StripeArtcraftProductInfo = StripeArtcraftProductInfo {
|
||||
slug: ArtcraftSubscriptionSlug::ArtcraftMax,
|
||||
product_id: "prod_SyJ6VIFnc3riPp",
|
||||
monthly_price_id: "price_1S2MUSIaZEzwFveesCPf2XjP",
|
||||
yearly_price_id: "price_1S2MVlIaZEzwFveeEtPV27kH",
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod stripe_artcraft_create_subscription_checkout_session_handler;
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
use crate::configs::get_artcraft_product_by_slug::get_artcraft_product_by_slug;
|
||||
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, Json};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use artcraft_api_defs::stripe_artcraft::create_subscription_checkout::{PlanBillingCadence, StripeArtcraftCreateCheckoutSessionRequest, StripeArtcraftCreateCheckoutSessionResponse};
|
||||
//use billing_component::stripe::traits::internal_user_lookup::InternalUserLookup;
|
||||
use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug;
|
||||
use log::{error, 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, 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/create_subscription",
|
||||
// params(
|
||||
// ("request" = CreateCheckoutSessionRequest, description = "Payload for Request"),
|
||||
// ),
|
||||
// responses(
|
||||
// (status = 200, description = "Success Delete", body = CreateCheckoutSessionSuccessResponse),
|
||||
// ),
|
||||
// )]
|
||||
pub async fn stripe_artcraft_create_checkout_session_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<StripeArtcraftCreateCheckoutSessionRequest>,
|
||||
//server_state: Data<Arc<ServerState>>,
|
||||
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>
|
||||
{
|
||||
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_product_by_slug(slug, **server_environment);
|
||||
|
||||
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(&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,
|
||||
//};
|
||||
|
||||
//error!("Subscriptions: {:?}", &user_metadata.existing_subscription_keys);
|
||||
|
||||
//// TODO: This will not handle a future where we have multiple "namespaces" or can offer users more than one subscription.
|
||||
//// It will actively block users from subscribing to two or more websites.
|
||||
//if !user_metadata.existing_subscription_keys.is_empty() {
|
||||
// return Err(CommonWebError::BadInputWithSimpleMessage("user already has plan".to_string()))
|
||||
//}
|
||||
|
||||
let success_url = stripe_config.checkout_success_url.clone();
|
||||
let cancel_url = stripe_config.checkout_cancel_url.clone();
|
||||
|
||||
let checkout_session = {
|
||||
let mut params = CreateCheckoutSession::new();
|
||||
|
||||
let mut params = params.success_url(&success_url);
|
||||
let mut params = params.cancel_url(&cancel_url);
|
||||
|
||||
// `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());
|
||||
//}
|
||||
|
||||
// NB: This metadata attaches to Stripe's Checkout Session object.
|
||||
// This does not attach to the subscription or payment intent, which have their own metadata
|
||||
// objects. (TODO: Confirm this.)
|
||||
//let mut params = params.metadata(metadata.clone());
|
||||
|
||||
// Subscription mode: Use Stripe Billing to set up fixed-price subscriptions.
|
||||
// TODO: params.mode = Some(CheckoutSessionMode::Subscription);
|
||||
|
||||
// NB: This metadata attaches to the subscription entity itself.
|
||||
// This cannot be used for non-subscription, one-off payments.
|
||||
// https://support.stripe.com/questions/using-metadata-with-checkout-sessions
|
||||
let mut params = params.subscription_data(CreateCheckoutSessionSubscriptionData {
|
||||
metadata: Some(metadata),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// TODO: Buy extra credits.
|
||||
// // Payment mode: Accept one-time payments for cards, iDEAL, and more.
|
||||
// params.mode = Some(CheckoutSessionMode::Payment);
|
||||
// // NB: This metadata attaches to the payment_intent entity itself.
|
||||
// // This cannot be used for subscriptions.
|
||||
// // https://support.stripe.com/questions/using-metadata-with-checkout-sessions
|
||||
// params.payment_intent_data = Some(CreateCheckoutSessionPaymentIntentData {
|
||||
// metadata: Some(metadata.clone()),
|
||||
// ..Default::default()
|
||||
// });
|
||||
|
||||
let mut params = params.automatic_tax(CreateCheckoutSessionAutomaticTax {
|
||||
enabled: true,
|
||||
liability: None,
|
||||
});
|
||||
|
||||
let mut params = params.line_items(vec![
|
||||
CreateCheckoutSessionLineItems {
|
||||
price: Some(price_id.to_string()),
|
||||
quantity: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
]);
|
||||
|
||||
// If we already have a Stripe customer associated with the user account, we'll reuse it.
|
||||
//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) => {
|
||||
// params = params.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);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// TODO: THis is now broken
|
||||
//CheckoutSession::create(&server_state.stripe_artcraft.client, params)
|
||||
// .await
|
||||
// .map_err(|e| {
|
||||
// error!("Stripe Error: {:?}", e);
|
||||
// CommonWebError::ServerError
|
||||
// })?
|
||||
|
||||
let checkout_session = CreateCheckoutSession::new()
|
||||
.success_url(&success_url)
|
||||
.cancel_url(&cancel_url)
|
||||
//.customer(customer.id.as_str())
|
||||
.mode(CheckoutSessionMode::Subscription)
|
||||
.line_items(vec![
|
||||
CreateCheckoutSessionLineItems {
|
||||
price: Some(price_id.to_string()),
|
||||
quantity: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
])
|
||||
//.expand([String::from("line_items"), String::from("line_items.data.price.product")])
|
||||
.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(StripeArtcraftCreateCheckoutSessionResponse {
|
||||
success: true,
|
||||
stripe_checkout_redirect_url: url,
|
||||
}))
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod webhook;
|
||||
pub mod webhook;
|
||||
pub mod checkout;
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod configs;
|
||||
pub mod endpoints;
|
||||
pub mod utils;
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
use crate::http_server::endpoints::stripe_artcraft::checkout::stripe_artcraft_create_subscription_checkout_session_handler::stripe_artcraft_create_checkout_session_handler;
|
||||
use crate::http_server::endpoints::stripe_artcraft::webhook::stripe_artcraft_webhook_handler::stripe_artcraft_webhook_handler;
|
||||
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::webhook::stripe_artcraft_webhook_handler::stripe_artcraft_webhook_handler;
|
||||
|
||||
pub fn add_stripe_artcraft_routes<T, B> (app: App<T>) -> App<T>
|
||||
where
|
||||
|
||||
Reference in new issue
Block a user