Continued work on product integration

This commit is contained in:
Brandon Thomas committed 2022-10-25 05:02:52 -04:00
1 parent 560448f1ad
commit 5ee51ed8e2
14 files changed
+382 -148

No files matched your search

Generated
+107
View File
@@ -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;
@@ -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)
}
}
@@ -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,
@@ -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))
@@ -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));
}
}
@@ -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;
@@ -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);
+27 -5
View File
@@ -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,
}