mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-08 02:52:12 +00:00
Continued work on product integration
This commit is contained in:
1 parent
560448f1ad
commit
5ee51ed8e2
14 files changed
+382
-148
No files matched your search
Generated
+107
@@ -869,6 +869,7 @@ dependencies = [
|
||||
"dotenv",
|
||||
"http_server_common",
|
||||
"log",
|
||||
"mockall",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"reusable_types",
|
||||
@@ -876,6 +877,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tokio 1.13.0",
|
||||
"toml",
|
||||
]
|
||||
|
||||
@@ -1467,6 +1469,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "difflib"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.9.0"
|
||||
@@ -1536,6 +1544,12 @@ version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||
|
||||
[[package]]
|
||||
name = "downcast"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "0.4.8"
|
||||
@@ -1629,6 +1643,15 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "float-cmp"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -1660,6 +1683,12 @@ dependencies = [
|
||||
"percent-encoding 2.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fragile"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa"
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-cprng"
|
||||
version = "0.1.1"
|
||||
@@ -2362,6 +2391,15 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.7"
|
||||
@@ -2776,6 +2814,33 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mockall"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50e4a1c770583dac7ab5e2f6c139153b783a53a1bbee9729613f193e59828326"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"downcast",
|
||||
"fragile",
|
||||
"lazy_static",
|
||||
"mockall_derive",
|
||||
"predicates",
|
||||
"predicates-tree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mockall_derive"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "832663583d5fa284ca8810bf7015e46c9fff9622d3cf34bd1eea5003fec06dd0"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mysqlclient-sys"
|
||||
version = "0.2.4"
|
||||
@@ -2846,6 +2911,12 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "normalize-line-endings"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.3.6"
|
||||
@@ -3216,6 +3287,36 @@ version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c"
|
||||
dependencies = [
|
||||
"difflib",
|
||||
"float-cmp",
|
||||
"itertools",
|
||||
"normalize-line-endings",
|
||||
"predicates-core",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "predicates-core"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb"
|
||||
|
||||
[[package]]
|
||||
name = "predicates-tree"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032"
|
||||
dependencies = [
|
||||
"predicates-core",
|
||||
"termtree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-hack"
|
||||
version = "0.5.19"
|
||||
@@ -4428,6 +4529,12 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termtree"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b"
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.11.0"
|
||||
|
||||
@@ -54,3 +54,9 @@ async-trait = "0.1.58"
|
||||
# "rustls" doesn't appear to work with IP addresses, per:
|
||||
# https://github.com/launchbadge/sqlx/issues/1095#issuecomment-798050828
|
||||
sqlx = { version = "0.5", features = [ "mysql", "runtime-actix-native-tls", "offline", "chrono" ] }
|
||||
|
||||
[dev-dependencies]
|
||||
mockall = "0.11.3"
|
||||
|
||||
# NB: Tokio version matches `async-stripe` transitive dep; features are for testing
|
||||
tokio = { version = "1.13.0", features = ["rt", "macros"] }
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod stripe_checkout_success_handler;
|
||||
pub mod stripe_create_checkout_session_error;
|
||||
pub mod stripe_create_checkout_session_json_handler;
|
||||
pub mod stripe_create_checkout_session_redirect_handler;
|
||||
pub mod stripe_create_checkout_session_shared;
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::{HttpResponse, ResponseError};
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Serialize, Eq, PartialEq)]
|
||||
pub enum CreateCheckoutSessionError {
|
||||
BadRequest,
|
||||
PlanNotFound,
|
||||
InvalidSession,
|
||||
ServerError,
|
||||
StripeError,
|
||||
}
|
||||
|
||||
impl ResponseError for CreateCheckoutSessionError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
CreateCheckoutSessionError::BadRequest => StatusCode::BAD_REQUEST,
|
||||
CreateCheckoutSessionError::PlanNotFound => StatusCode::NOT_FOUND,
|
||||
CreateCheckoutSessionError::InvalidSession => StatusCode::UNAUTHORIZED,
|
||||
CreateCheckoutSessionError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
CreateCheckoutSessionError::StripeError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for CreateCheckoutSessionError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
+17
-40
@@ -4,8 +4,11 @@ use actix_web::http::{header, StatusCode};
|
||||
use actix_web::web::{Path, Query};
|
||||
use actix_web::{web, HttpResponse, HttpRequest};
|
||||
use chrono::{DateTime, Utc};
|
||||
use crate::stripe::http_endpoints::checkout::stripe_create_checkout_session_error::CreateCheckoutSessionError;
|
||||
use crate::stripe::http_endpoints::checkout::stripe_create_checkout_session_shared::stripe_create_checkout_session_shared;
|
||||
use crate::stripe::stripe_config::StripeConfig;
|
||||
use crate::stripe::traits::internal_product_to_stripe_lookup::InternalProductToStripeLookup;
|
||||
use crate::stripe::traits::internal_user_lookup::{InternalUserLookup, UserMetadata};
|
||||
use http_server_common::request::get_request_header_optional::get_request_header_optional;
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use log::{error, warn};
|
||||
@@ -13,13 +16,14 @@ use sqlx::MySqlPool;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use stripe::{CheckoutSession, CheckoutSessionMode, CreateCheckoutSession, CreateCheckoutSessionLineItems};
|
||||
use crate::stripe::traits::internal_user_lookup::UserMetadata;
|
||||
|
||||
// =============== Request ===============
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateCheckoutSessionRequest {
|
||||
price_key: Option<String>,
|
||||
/// The (non-Stripe) internal identifier for the product or subscription.
|
||||
/// This will be translated into a Stripe identifier.
|
||||
internal_plan_key: Option<String>,
|
||||
}
|
||||
|
||||
// =============== Success Response ===============
|
||||
@@ -30,52 +34,25 @@ pub struct CreateCheckoutSessionSuccessResponse {
|
||||
pub stripe_checkout_redirect_url: String,
|
||||
}
|
||||
|
||||
// =============== Error Response ===============
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub enum CreateCheckoutSessionError {
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for CreateCheckoutSessionError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
CreateCheckoutSessionError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for CreateCheckoutSessionError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stripe_create_checkout_session_json_handler(
|
||||
_http_request: HttpRequest,
|
||||
_mysql_pool: web::Data<MySqlPool>,
|
||||
http_request: HttpRequest,
|
||||
request: Query<CreateCheckoutSessionRequest>,
|
||||
stripe_config: web::Data<StripeConfig>,
|
||||
stripe_client: web::Data<stripe::Client>,
|
||||
internal_product_to_stripe_lookup: web::Data<dyn InternalProductToStripeLookup>,
|
||||
internal_user_lookup: web::Data<dyn InternalUserLookup>,
|
||||
) -> Result<HttpResponse, CreateCheckoutSessionError>
|
||||
{
|
||||
let price_key = request.price_key.as_deref().unwrap_or("unknown");
|
||||
|
||||
let user_metadata = UserMetadata::default();
|
||||
let maybe_internal_product_key = request.internal_plan_key.as_deref();
|
||||
|
||||
let url = stripe_create_checkout_session_shared(
|
||||
maybe_internal_product_key,
|
||||
&http_request,
|
||||
&stripe_config,
|
||||
price_key,
|
||||
Some(&user_metadata))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error creating Stripe checkout session: {:?}", err);
|
||||
CreateCheckoutSessionError::ServerError
|
||||
})?;
|
||||
&stripe_client,
|
||||
internal_product_to_stripe_lookup.get_ref(),
|
||||
internal_user_lookup.get_ref(),
|
||||
).await?;
|
||||
|
||||
let response = CreateCheckoutSessionSuccessResponse {
|
||||
success: true,
|
||||
|
||||
+13
-57
@@ -4,88 +4,44 @@ use actix_web::http::{header, StatusCode};
|
||||
use actix_web::web::{Path, Query};
|
||||
use actix_web::{web, HttpResponse, HttpRequest};
|
||||
use chrono::{DateTime, Utc};
|
||||
use crate::stripe::http_endpoints::checkout::stripe_create_checkout_session_error::CreateCheckoutSessionError;
|
||||
use crate::stripe::http_endpoints::checkout::stripe_create_checkout_session_shared::stripe_create_checkout_session_shared;
|
||||
use crate::stripe::stripe_config::StripeConfig;
|
||||
use crate::stripe::traits::internal_product_to_stripe_lookup::InternalProductToStripeLookup;
|
||||
use crate::stripe::traits::internal_user_lookup::InternalUserLookup;
|
||||
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
|
||||
use log::{error, warn};
|
||||
use sqlx::MySqlPool;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use stripe::{CheckoutSession, CheckoutSessionMode, CreateCheckoutSession, CreateCheckoutSessionLineItems};
|
||||
use crate::stripe::traits::internal_user_lookup::InternalUserLookup;
|
||||
|
||||
// =============== Request ===============
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateCheckoutSessionRequest {
|
||||
price_key: Option<String>,
|
||||
}
|
||||
|
||||
// =============== Error Response ===============
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub enum CreateCheckoutSessionError {
|
||||
NotAuthorizedError,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for CreateCheckoutSessionError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
CreateCheckoutSessionError::NotAuthorizedError => StatusCode::UNAUTHORIZED,
|
||||
CreateCheckoutSessionError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for CreateCheckoutSessionError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
product: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn stripe_create_checkout_session_redirect_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Query<CreateCheckoutSessionRequest>,
|
||||
stripe_config: web::Data<StripeConfig>,
|
||||
mysql_pool: web::Data<MySqlPool>,
|
||||
stripe_client: web::Data<stripe::Client>,
|
||||
internal_product_to_stripe_lookup: web::Data<dyn InternalProductToStripeLookup>,
|
||||
internal_user_lookup: web::Data<dyn InternalUserLookup>,
|
||||
) -> Result<HttpResponse, CreateCheckoutSessionError>
|
||||
{
|
||||
let price_key = request.price_key.as_deref().unwrap_or("unknown");
|
||||
|
||||
let maybe_user_metadata = internal_user_lookup
|
||||
.lookup_user_from_http_request(&http_request, &mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error looking up user: {:?}", err);
|
||||
CreateCheckoutSessionError::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(CreateCheckoutSessionError::NotAuthorizedError),
|
||||
Some(user_metadata) => user_metadata,
|
||||
};
|
||||
|
||||
if user_metadata.user_token.is_none() {
|
||||
return Err(CreateCheckoutSessionError::NotAuthorizedError);
|
||||
}
|
||||
let maybe_internal_product_key = request.product.as_deref();
|
||||
|
||||
let url = stripe_create_checkout_session_shared(
|
||||
maybe_internal_product_key,
|
||||
&http_request,
|
||||
&stripe_config,
|
||||
price_key,
|
||||
Some(&user_metadata))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error creating Stripe checkout session: {:?}", err);
|
||||
CreateCheckoutSessionError::ServerError
|
||||
})?;
|
||||
&stripe_client,
|
||||
internal_product_to_stripe_lookup.get_ref(),
|
||||
internal_user_lookup.get_ref(),
|
||||
).await?;
|
||||
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header((header::LOCATION, url))
|
||||
|
||||
+123
-40
@@ -1,49 +1,65 @@
|
||||
use actix_web::{HttpRequest, web};
|
||||
use anyhow::anyhow;
|
||||
use container_common::anyhow_result::AnyhowResult;
|
||||
use crate::stripe::helpers::common_metadata_keys::{METADATA_EMAIL, METADATA_USER_TOKEN, METADATA_USERNAME};
|
||||
use crate::stripe::http_endpoints::checkout::stripe_create_checkout_session_error::CreateCheckoutSessionError;
|
||||
use crate::stripe::stripe_config::StripeConfig;
|
||||
use crate::stripe::traits::internal_user_lookup::UserMetadata;
|
||||
use crate::stripe::traits::internal_product_to_stripe_lookup::{InternalProductToStripeLookup, StripeProduct};
|
||||
use crate::stripe::traits::internal_user_lookup::{InternalUserLookup, UserMetadata};
|
||||
use log::error;
|
||||
use sqlx::MySqlPool;
|
||||
use std::collections::HashMap;
|
||||
use stripe::{CheckoutSession, CheckoutSessionMode, CreateCheckoutSession, CreateCheckoutSessionAutomaticTax, CreateCheckoutSessionLineItems, CreateCheckoutSessionPaymentIntentData, CreateCheckoutSessionSubscriptionData};
|
||||
|
||||
// NB: These are "test" product IDs.
|
||||
// TODO: Pass these in via request; validate via a dynamically dispatched trait callable that can do
|
||||
// whatever the calling codebase needs
|
||||
pub const PRODUCT_FAKEYOU_BASIC_ID : &'static str = "prod_MMxi2J5y69VPbO";
|
||||
pub const PRODUCT_FAKEYOU_BASIC_PRICE_ID : &'static str = "price_1LeDnKEU5se17MekVr1iYYNf";
|
||||
|
||||
pub const PRODUCT_ONE_TIME_PURCHASE_ID : &'static str = "prod_MPQ6nWJ4k6lJmw";
|
||||
pub const PRODUCT_ONE_TIME_PURCHASE_PRICE_ID : &'static str = "price_1LgbG9EU5se17MekZIw95gEO";
|
||||
|
||||
/// Create a checkout session and return the URL
|
||||
/// If anything fails, treat it as a 500 server error.
|
||||
pub async fn stripe_create_checkout_session_shared(
|
||||
maybe_internal_product_key: Option<&str>,
|
||||
http_request: &HttpRequest,
|
||||
stripe_config: &StripeConfig,
|
||||
price_key: &str,
|
||||
maybe_user_metadata: Option<&UserMetadata>,
|
||||
) -> AnyhowResult<String> {
|
||||
stripe_client: &stripe::Client,
|
||||
internal_product_to_stripe_lookup: &dyn InternalProductToStripeLookup,
|
||||
internal_user_lookup: &dyn InternalUserLookup,
|
||||
|
||||
let stripe_client = {
|
||||
let api_secret = stripe_config.secrets.secret_key
|
||||
.as_deref()
|
||||
.ok_or(anyhow!("API key not configured"))?;
|
||||
stripe::Client::new(api_secret)
|
||||
) -> Result<String, CreateCheckoutSessionError> {
|
||||
let internal_product_key = match maybe_internal_product_key {
|
||||
None => return Err(CreateCheckoutSessionError::BadRequest),
|
||||
Some(internal_product_key) => internal_product_key,
|
||||
};
|
||||
|
||||
let stripe_product = internal_product_to_stripe_lookup
|
||||
.lookup_stripe_product_from_internal_product_key(internal_product_key)
|
||||
.map_err(|err| {
|
||||
error!("Error looking up product: {:?}", err);
|
||||
CreateCheckoutSessionError::ServerError // NB: This was probably *our* fault.
|
||||
})?
|
||||
.ok_or(CreateCheckoutSessionError::PlanNotFound)?; // Non-existing product
|
||||
|
||||
let maybe_user_metadata = internal_user_lookup
|
||||
.lookup_user_from_http_request(http_request)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error looking up user: {:?}", err);
|
||||
CreateCheckoutSessionError::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(CreateCheckoutSessionError::InvalidSession),
|
||||
Some(user_metadata) => user_metadata,
|
||||
};
|
||||
|
||||
if user_metadata.user_token.is_none() {
|
||||
return Err(CreateCheckoutSessionError::InvalidSession);
|
||||
}
|
||||
|
||||
let success_url = stripe_config.checkout.success_url
|
||||
.as_deref()
|
||||
.ok_or(anyhow!("Checkout Success URL not configured"))?;
|
||||
.ok_or(CreateCheckoutSessionError::ServerError)?;
|
||||
|
||||
let cancel_url = stripe_config.checkout.cancel_url
|
||||
.as_deref()
|
||||
.ok_or(anyhow!("Checkout Cancel URL not configured"))?;
|
||||
|
||||
let (price_id, is_subscription) = match price_key {
|
||||
"subscription" => (PRODUCT_FAKEYOU_BASIC_PRICE_ID, true),
|
||||
"one-time" => (PRODUCT_ONE_TIME_PURCHASE_PRICE_ID, false),
|
||||
_ => return Err(anyhow!("wrong price key!")),
|
||||
};
|
||||
.ok_or(CreateCheckoutSessionError::ServerError)?;
|
||||
|
||||
let checkout_session = {
|
||||
let mut params = CreateCheckoutSession::new(
|
||||
@@ -79,16 +95,14 @@ pub async fn stripe_create_checkout_session_shared(
|
||||
|
||||
let mut metadata = HashMap::new();
|
||||
|
||||
if let Some(user_metadata) = maybe_user_metadata {
|
||||
if let Some(user_token) = user_metadata.user_token.as_deref() {
|
||||
metadata.insert(METADATA_USER_TOKEN.to_string(), user_token.to_string());
|
||||
}
|
||||
if let Some(username) = user_metadata.username.as_deref() {
|
||||
metadata.insert(METADATA_USERNAME.to_string(), username.to_string());
|
||||
}
|
||||
if let Some(user_email) = user_metadata.user_email.as_deref() {
|
||||
metadata.insert(METADATA_EMAIL.to_string(), user_email.to_string());
|
||||
}
|
||||
if let Some(user_token) = user_metadata.user_token.as_deref() {
|
||||
metadata.insert(METADATA_USER_TOKEN.to_string(), user_token.to_string());
|
||||
}
|
||||
if let Some(username) = user_metadata.username.as_deref() {
|
||||
metadata.insert(METADATA_USERNAME.to_string(), username.to_string());
|
||||
}
|
||||
if let Some(user_email) = user_metadata.user_email.as_deref() {
|
||||
metadata.insert(METADATA_EMAIL.to_string(), user_email.to_string());
|
||||
}
|
||||
|
||||
// NB: This metadata attaches to Stripe's Checkout Session object.
|
||||
@@ -96,7 +110,7 @@ pub async fn stripe_create_checkout_session_shared(
|
||||
// objects. (TODO: Confirm this.)
|
||||
params.metadata = Some(metadata.clone());
|
||||
|
||||
if is_subscription {
|
||||
if stripe_product.is_subscription_product {
|
||||
// Subscription mode: Use Stripe Billing to set up fixed-price subscriptions.
|
||||
params.mode = Some(CheckoutSessionMode::Subscription);
|
||||
|
||||
@@ -125,7 +139,7 @@ pub async fn stripe_create_checkout_session_shared(
|
||||
|
||||
params.line_items = Some(vec![
|
||||
CreateCheckoutSessionLineItems {
|
||||
price: Some(price_id.to_string()),
|
||||
price: Some(stripe_product.stripe_price_id.to_string()),
|
||||
quantity: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -135,9 +149,78 @@ pub async fn stripe_create_checkout_session_shared(
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Error: {:?}", e);
|
||||
anyhow!("error creating checkout session against Stripe")
|
||||
CreateCheckoutSessionError::StripeError
|
||||
})?
|
||||
};
|
||||
|
||||
checkout_session.url.ok_or(anyhow!("checkout session does not contain a URL"))
|
||||
checkout_session.url.ok_or(CreateCheckoutSessionError::ServerError)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::error::UrlencodedError::ContentType;
|
||||
use crate::stripe::http_endpoints::checkout::stripe_create_checkout_session_shared::stripe_create_checkout_session_shared;
|
||||
use crate::stripe::stripe_config::{StripeCheckout, StripeConfig, StripeSecrets};
|
||||
use crate::stripe::traits::internal_product_to_stripe_lookup::{MockInternalProductToStripeLookup, StripeProduct};
|
||||
use crate::stripe::traits::internal_user_lookup::{MockInternalUserLookup, UserMetadata};
|
||||
use mockall::predicate::*;
|
||||
use tokio;
|
||||
use crate::stripe::http_endpoints::checkout::stripe_create_checkout_session_error::CreateCheckoutSessionError;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_success_case() {
|
||||
let http_request = actix_web::test::TestRequest::default()
|
||||
.insert_header(actix_web::http::header::ContentType::json())
|
||||
.to_http_request();
|
||||
|
||||
let maybe_internal_product_key = Some("TEST_FAKEYOU_PRODUCT");
|
||||
|
||||
let stripe_config = StripeConfig {
|
||||
checkout: StripeCheckout {
|
||||
success_url: Some("http://example.com/success".to_string()),
|
||||
cancel_url: Some("http://example.com/cancel".to_string()),
|
||||
},
|
||||
secrets: StripeSecrets {
|
||||
publishable_key: None,
|
||||
secret_key: Some("sk_test_12345".to_string()), // NB: Expected key format
|
||||
secret_webhook_signing_key: Some("fake_test_signing".to_string()),
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Mock this somehow? We can't really test this library unless we can get inside it.
|
||||
// Note that this might also fail in CI if the client tries to actually talk to Stripe.com.
|
||||
let mut stripe_client= stripe::Client::new("sk_test_12345");
|
||||
|
||||
let mut internal_product_to_stripe_lookup_mock = MockInternalProductToStripeLookup::new();
|
||||
|
||||
internal_product_to_stripe_lookup_mock.expect_lookup_stripe_product_from_internal_product_key()
|
||||
.with(eq("TEST_FAKEYOU_PRODUCT"))
|
||||
.returning(|_| Ok(Some(StripeProduct {
|
||||
stripe_product_id: "TEST_PRODUCT_ID".to_string(),
|
||||
stripe_price_id: "TEST_PRICE_ID".to_string(),
|
||||
is_subscription_product: true,
|
||||
})));
|
||||
|
||||
let mut internal_user_lookup_mock = MockInternalUserLookup::new();
|
||||
|
||||
internal_user_lookup_mock.expect_lookup_user_from_http_request()
|
||||
.returning(|_| Ok(Some(UserMetadata {
|
||||
user_token: Some("U:USER".to_string()),
|
||||
username: Some("vegito".to_string()),
|
||||
user_email: Some("vegito@fakeyou.com".to_string()),
|
||||
})));
|
||||
|
||||
let result = stripe_create_checkout_session_shared(
|
||||
maybe_internal_product_key,
|
||||
&http_request,
|
||||
&stripe_config,
|
||||
&stripe_client,
|
||||
&internal_product_to_stripe_lookup_mock,
|
||||
&internal_user_lookup_mock,
|
||||
).await;
|
||||
|
||||
// TODO: Sort of throwing my hands up over testing this.
|
||||
// There's no convenient way to test which arguments get sent.
|
||||
assert_eq!(result, Err(CreateCheckoutSessionError::StripeError));
|
||||
}
|
||||
}
|
||||
+4
@@ -1,3 +1,6 @@
|
||||
#[cfg(test)]
|
||||
use mockall::{automock, mock, predicate::*};
|
||||
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
@@ -31,6 +34,7 @@ pub struct StripeProduct {
|
||||
}
|
||||
|
||||
/// Allows external systems to map internal product keys to stripe information.
|
||||
#[cfg_attr(test, automock)]
|
||||
pub trait InternalProductToStripeLookup {
|
||||
/// Look up stripe product information from an internal system identifier.
|
||||
fn lookup_stripe_product_from_internal_product_key(&self, internal_product_key: &str)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#[cfg(test)]
|
||||
use mockall::{automock, mock, predicate::*};
|
||||
|
||||
use actix_web::HttpRequest;
|
||||
use async_trait::async_trait;
|
||||
use sqlx::MySqlPool;
|
||||
@@ -47,10 +50,11 @@ pub struct UserMetadata {
|
||||
|
||||
/// Allows us to inject a user lookup from the HTTP request's session info and database backend,
|
||||
/// then translate these into the pieces we need for the billing component.
|
||||
#[cfg_attr(test, automock)]
|
||||
#[async_trait(?Send)] // NB: Marking async_trait as not needing Sync/Send. Hopefully this doesn't blow up on us.
|
||||
pub trait InternalUserLookup {
|
||||
|
||||
/// Lookup a user's session details from an HTTP request, then return the
|
||||
/// relevant pieces for the Stripe integration.
|
||||
async fn lookup_user_from_http_request(&self, http_request: &HttpRequest, mysql_pool: &MySqlPool) -> Result<Option<UserMetadata>, InternalUserLookupError>;
|
||||
async fn lookup_user_from_http_request(&self, http_request: &HttpRequest) -> Result<Option<UserMetadata>, InternalUserLookupError>;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use billing_component::stripe::traits::internal_product_to_stripe_lookup::{InternalProductToStripeLookup, StripeProduct, StripeProductLookupError};
|
||||
use crate::configs::plans::plan_list::ALL_PLANS_BY_SLUG;
|
||||
|
||||
/// A simple Actix injectable action
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct InternalProductToStripeLookupImpl {}
|
||||
|
||||
impl InternalProductToStripeLookup for InternalProductToStripeLookupImpl {
|
||||
fn lookup_stripe_product_from_internal_product_key(&self, internal_product_key: &str) -> Result<Option<StripeProduct>, StripeProductLookupError> {
|
||||
Ok(ALL_PLANS_BY_SLUG.get(internal_product_key)
|
||||
.and_then(|plan| {
|
||||
let stripe_product_id = match plan.stripe_product_id() {
|
||||
None => return None,
|
||||
Some(stripe_product_id) => stripe_product_id.to_string(),
|
||||
};
|
||||
|
||||
let stripe_price_id = match plan.stripe_price_id() {
|
||||
None => return None,
|
||||
Some(stripe_price_id) => stripe_price_id.to_string(),
|
||||
};
|
||||
|
||||
Some(StripeProduct {
|
||||
stripe_product_id,
|
||||
stripe_price_id,
|
||||
is_subscription_product: true, // NB: For now all plans are subscription plans.
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod internal_product_to_stripe_lookup_impl;
|
||||
pub mod stripe_internal_subscription_product_lookup_impl;
|
||||
pub mod stripe_internal_user_lookup;
|
||||
pub mod stripe_internal_user_lookup_impl;
|
||||
+5
-3
@@ -10,12 +10,14 @@ use users_component::utils::session_checker::SessionChecker;
|
||||
#[derive(Clone)]
|
||||
pub struct StripeInternalUserLookupImpl {
|
||||
session_checker: SessionChecker,
|
||||
mysql_pool: MySqlPool,
|
||||
}
|
||||
|
||||
impl StripeInternalUserLookupImpl {
|
||||
pub fn new(session_checker: SessionChecker) -> Self {
|
||||
pub fn new(session_checker: SessionChecker, mysql_pool: MySqlPool) -> Self {
|
||||
Self {
|
||||
session_checker,
|
||||
mysql_pool,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,8 +25,8 @@ impl StripeInternalUserLookupImpl {
|
||||
// NB: Marking async_trait as not needing Sync/Send. Hopefully this doesn't blow up on us.
|
||||
#[async_trait(?Send)]
|
||||
impl InternalUserLookup for StripeInternalUserLookupImpl {
|
||||
async fn lookup_user_from_http_request(&self, http_request: &HttpRequest, mysql_pool: &MySqlPool) -> Result<Option<UserMetadata>, InternalUserLookupError> {
|
||||
let mut mysql_connection = mysql_pool.acquire()
|
||||
async fn lookup_user_from_http_request(&self, http_request: &HttpRequest) -> Result<Option<UserMetadata>, InternalUserLookupError> {
|
||||
let mut mysql_connection = self.mysql_pool.acquire()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Could not acquire DB pool: {:?}", e);
|
||||
@@ -52,7 +52,7 @@ use crate::configs::static_api_tokens::{StaticApiTokenConfig, StaticApiTokens, S
|
||||
use crate::http_server::middleware::ip_filter_middleware::IpFilter;
|
||||
use crate::http_server::web_utils::redis_rate_limiter::RedisRateLimiter;
|
||||
use crate::routes::add_routes;
|
||||
use crate::server_state::{ServerState, EnvConfig, TwitchOauthSecrets, TwitchOauth, RedisRateLimiters, InMemoryCaches};
|
||||
use crate::server_state::{ServerState, EnvConfig, TwitchOauthSecrets, TwitchOauth, RedisRateLimiters, InMemoryCaches, StripeSettings};
|
||||
use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus;
|
||||
use crate::threads::db_health_checker_thread::db_health_checker_thread::db_health_checker_thread;
|
||||
use crate::threads::ip_banlist_set::IpBanlistSet;
|
||||
@@ -72,14 +72,17 @@ use sqlx::MySqlPool;
|
||||
use sqlx::mysql::MySqlPoolOptions;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use anyhow::anyhow;
|
||||
use storage_buckets_common::bucket_client::BucketClient;
|
||||
use tokio::runtime::Runtime;
|
||||
use billing_component::stripe::traits::internal_product_to_stripe_lookup::InternalProductToStripeLookup;
|
||||
use billing_component::stripe::traits::internal_subscription_product_lookup::InternalSubscriptionProductLookup;
|
||||
use billing_component::stripe::traits::internal_user_lookup::InternalUserLookup;
|
||||
use twitch_common::twitch_secrets::TwitchSecrets;
|
||||
use users_component::utils::session_checker::SessionChecker;
|
||||
use users_component::utils::session_cookie_manager::SessionCookieManager;
|
||||
use crate::billing::stripe_internal_user_lookup::StripeInternalUserLookupImpl;
|
||||
use crate::billing::internal_product_to_stripe_lookup_impl::InternalProductToStripeLookupImpl;
|
||||
use crate::billing::stripe_internal_user_lookup_impl::StripeInternalUserLookupImpl;
|
||||
|
||||
// TODO TODO TODO TODO
|
||||
// TODO TODO TODO TODO
|
||||
@@ -328,6 +331,16 @@ async fn main() -> AnyhowResult<()> {
|
||||
},
|
||||
};
|
||||
|
||||
let stripe_client = {
|
||||
let api_secret = stripe_configs
|
||||
.secrets
|
||||
.secret_key
|
||||
.clone()
|
||||
.ok_or(anyhow!("stripe secret key not configured"))?;
|
||||
|
||||
stripe::Client::new(api_secret)
|
||||
};
|
||||
|
||||
let server_state = ServerState {
|
||||
env_config: EnvConfig {
|
||||
num_workers,
|
||||
@@ -337,7 +350,10 @@ async fn main() -> AnyhowResult<()> {
|
||||
cookie_http_only,
|
||||
website_homepage_redirect,
|
||||
},
|
||||
stripe_configs,
|
||||
stripe: StripeSettings {
|
||||
config: stripe_configs,
|
||||
client: stripe_client,
|
||||
},
|
||||
hostname: server_hostname,
|
||||
health_check_status,
|
||||
mysql_pool: pool,
|
||||
@@ -406,7 +422,11 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()>
|
||||
|
||||
// NB: Dynamic dispatch needs to be wrapped with Arc.
|
||||
let product_lookup : Arc<dyn InternalSubscriptionProductLookup> = Arc::new(StripeInternalSubscriptionProductLookupImpl {});
|
||||
let user_lookup : Arc<dyn InternalUserLookup> = Arc::new(StripeInternalUserLookupImpl::new(server_state_arc.session_checker.clone()));
|
||||
let stripe_lookup : Arc<dyn InternalProductToStripeLookup> = Arc::new(InternalProductToStripeLookupImpl{});
|
||||
let user_lookup : Arc<dyn InternalUserLookup> = Arc::new(StripeInternalUserLookupImpl::new(
|
||||
server_state_arc.session_checker.clone(),
|
||||
server_state_arc.mysql_pool.clone(),
|
||||
));
|
||||
|
||||
// NB: app_data being clone()'d below should all be safe (dependencies included)
|
||||
let app = App::new()
|
||||
@@ -414,8 +434,10 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()>
|
||||
.app_data(web::Data::new(server_state_arc.mysql_pool.clone()))
|
||||
.app_data(web::Data::new(server_state_arc.session_checker.clone()))
|
||||
.app_data(web::Data::new(server_state_arc.cookie_manager.clone()))
|
||||
.app_data(web::Data::new(server_state_arc.stripe_configs.clone()))
|
||||
.app_data(web::Data::new(server_state_arc.stripe.clone().config.clone()))
|
||||
.app_data(web::Data::new(server_state_arc.stripe.clone().client.clone()))
|
||||
.app_data(web::Data::from(product_lookup)) // NB: Data::from(Arc<T>) for dynamic dispatch
|
||||
.app_data(web::Data::from(stripe_lookup)) // NB: Data::from(Arc<T>) for dynamic dispatch
|
||||
.app_data(web::Data::from(user_lookup)) // NB: Data::from(Arc<T>) for dynamic dispatch
|
||||
.app_data(server_state_arc.clone())
|
||||
.wrap(build_common_cors_config())
|
||||
|
||||
@@ -22,7 +22,7 @@ pub struct ServerState {
|
||||
/// Configuration from ENV vars.
|
||||
pub env_config: EnvConfig,
|
||||
|
||||
pub stripe_configs: StripeConfig,
|
||||
pub stripe: StripeSettings,
|
||||
|
||||
pub hostname: String,
|
||||
|
||||
@@ -120,3 +120,9 @@ pub struct InMemoryCaches {
|
||||
/// vector clock.
|
||||
pub tts_queue_length: SingleItemTtlCache<TtsQueueLengthResult>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StripeSettings {
|
||||
pub config: StripeConfig,
|
||||
pub client: stripe::Client,
|
||||
}
|
||||
Reference in new issue
Block a user