compiles with endpoint logic

This commit is contained in:
Brandon Thomas committed 2025-09-12 01:00:02 -04:00
1 parent fe46c72d06
commit 35554c6dde
14 files changed
+615 -20

No files matched your search

Generated
+21
View File
@@ -1028,6 +1028,23 @@ dependencies = [
"smol_str 0.3.2",
]
[[package]]
name = "async-stripe-webhook"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75f8382af92463fa8e0b86e825d7d08eecbcd8715c197f1a66173fcc18e35774"
dependencies = [
"async-stripe-shared",
"async-stripe-types",
"chrono",
"hex",
"hmac 0.12.1",
"miniserde",
"serde",
"sha2 0.10.8",
"thiserror 1.0.63",
]
[[package]]
name = "async-task"
version = "4.7.1"
@@ -1307,14 +1324,18 @@ dependencies = [
"anyhow",
"async-stripe 1.0.0-alpha.2",
"async-stripe-core",
"async-stripe-webhook",
"chrono",
"errors",
"http_server_common",
"log",
"mockall",
"mysql_queries",
"once_cell",
"serde",
"serde_derive",
"serde_json",
"sqlx",
"tokio",
"workspace-build-acceleration",
]
@@ -1,7 +1,7 @@
[package]
publish = false
name = "billing_artcraft_component"
edition = "2018"
edition = "2024"
version = "0.0.1"
authors = [
"Brandon Thomas <bt@brand.io>",
@@ -22,9 +22,8 @@ workspace-build-acceleration.workspace = true
# Internal
errors.workspace = true
#mysql_queries.workspace = true
#http_server_common = { path = "../../../lib/deprecated/http_server_common" }
#mysql_queries = { path = "../../../schema/database/mysql_queries" }
http_server_common = { path = "../../../lib/deprecated/http_server_common" }
mysql_queries = { path = "../../../schema/database/mysql_queries" }
#reusable_types = { path = "../../../schema/public/reusable_types" }
#url_config = { path = "../../../lib/url_config" }
#user_traits_component = { path = "../user_traits_component" }
@@ -39,11 +38,13 @@ once_cell.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
# Money
#async-stripe = { version = "1.0.0-alpha.2", features = ["runtime-tokio-hyper"] }
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 = [] }
#sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
#utoipa = { version = "4", features = ["chrono","actix_extras"] }
@@ -0,0 +1 @@
pub mod webhook;
@@ -0,0 +1,2 @@
pub mod stripe_artcraft_webhook_handler;
pub mod stripe_artcraft_webhook_summary;
@@ -0,0 +1,363 @@
use actix_web::web::{Bytes, Data, Json};
use actix_web::{web, HttpRequest, HttpResponse};
use crate::endpoints::webhook::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient;
use crate::utils::common_web_error::CommonWebError;
use crate::utils::verify_stripe_webhook_ip_address::verify_stripe_webhook_ip_address;
use chrono::NaiveDateTime;
use http_server_common::request::get_request_header_optional::get_request_header_optional;
use log::{error, info, warn};
use mysql_queries::queries::billing::stripe::get_stripe_webhook_event_log_by_id::{get_stripe_webhook_event_log_by_id, get_stripe_webhook_event_log_by_id_with_connection};
use mysql_queries::queries::billing::stripe::insert_stripe_webhook_event_log::InsertStripeWebhookEventLog;
use serde_derive::Serialize;
use sqlx::pool::PoolConnection;
use sqlx::{MySql, MySqlConnection, MySqlPool};
use std::sync::Arc;
use stripe_webhook::{Event, Webhook};
#[derive(Serialize)]
pub struct StripeArtcraftWebhookSuccessResponse {
pub success: bool,
}
// /// Stripe (Artcraft) Webhook Handler
// #[utoipa::path(
// post,
// tag = "Stripe (Artcraft)",
// path = "/v1/stripe_artcraft/webhook",
// responses(
// (status = 200, description = "Success", body = StripeArtcraftWebhookSuccessResponse),
// ),
// params()
// )]
pub async fn stripe_artcraft_webhook_handler(
http_request: HttpRequest,
//server_state: Data<Arc<ServerState>>,
stripe_config: Data<ArtcraftStripeConfigWithClient>,
request_body_bytes: Bytes,
mysql_pool: Data<MySqlPool>,
) -> Result<Json<StripeArtcraftWebhookSuccessResponse>, CommonWebError>
{
verify_stripe_webhook_ip_address(&http_request)
.map_err(|e| {
let reason = format!("Improper client IP address. Error: {:?}", e);
error!("{}", &reason);
CommonWebError::BadInputWithSimpleMessage("bad request".to_string())
})?;
let secret_signing_key = &stripe_config.secret_webhook_signing_key;
let stripe_signature = get_request_header_optional(&http_request, "Stripe-Signature")
.unwrap_or_default();
// NB: Treat the request payload as unstructured and defer to Stripe libraries.
let webhook_payload = String::from_utf8(request_body_bytes.to_vec())
.map_err(|err| {
let reason = format!("Could not decode request body to UTF-8: {:?}", err);
error!("{}", &reason);
CommonWebError::BadInputWithSimpleMessage(reason)
})?;
let webhook_payload = Webhook::construct_event(&webhook_payload, &stripe_signature, secret_signing_key)
.map_err(|e| {
let reason = format!("Could not construct Stripe webhook event: {:?}", e);
error!("{}", &reason);
println!("{:?}", webhook_payload);
CommonWebError::BadInputWithSimpleMessage(reason)
})?;
// Events can be re-sent, so we need to make handling them idempotent.
let stripe_event_id = webhook_payload.id.to_string();
let stripe_event_created_at = NaiveDateTime::from_timestamp(webhook_payload.created, 0);
let stripe_event_type = webhook_payload.type_.as_str().to_string();
//let stripe_event_type = serde_json::to_string(&webhook_payload.type_)
// .map(|s| s.replace("\"", ""))
// .map_err(|err| {
// let reason = format!("Could not serialize webhook type: {:?}", err);
// error!("{}", &reason);
// CommonWebError::BadInputWithSimpleMessage(reason)
// })?;
// NB: Whether this was test data or live data
let stripe_is_production = webhook_payload.livemode;
info!("Stripe webhook event type: {} ({:?}); is production: {}, created at: {}, pending events to be handled: {}",
&stripe_event_type,
&webhook_payload.type_,
stripe_is_production,
&stripe_event_created_at,
webhook_payload.pending_webhooks);
let mut mysql_connection = mysql_pool
.acquire()
.await?;
let maybe_previously_played_event = get_stripe_webhook_event_log_by_id_with_connection(&stripe_event_id, &mut mysql_connection)
.await
.map_err(|err| {
let reason = format!("Could not query previous event by ID ({}): {:?}", &stripe_event_id, err);
error!("{}", &reason);
CommonWebError::ServerError
})?;
if let Some(event) = maybe_previously_played_event {
// The event is being replayed by Stripe, and we've already handled it.
// We'll ignore it so that we remain idempotent.
if event.should_ignore_retry {
warn!("Stripe is replying event with ID={}. Ignoring it since we have previously handled it.",
&stripe_event_id);
return Ok(Json(StripeArtcraftWebhookSuccessResponse {
success: true,
}));
}
}
let webhook_summary = handle_webhook_payload(&mut mysql_connection, webhook_payload, &stripe_event_type).await?;
let query = InsertStripeWebhookEventLog {
stripe_event_id,
stripe_event_type,
maybe_stripe_event_entity_id: webhook_summary.maybe_event_entity_id,
maybe_stripe_customer_id: webhook_summary.maybe_stripe_customer_id,
stripe_event_created_at,
stripe_is_production,
maybe_user_token: webhook_summary.maybe_user_token,
action_was_taken: webhook_summary.action_was_taken,
should_ignore_retry: webhook_summary.should_ignore_retry,
};
query.insert(&mysql_pool)
.await
.map_err(|err| {
let reason = format!("Could not insert Stripe webhook event log: {:?}", err);
error!("{}", &reason);
CommonWebError::ServerError
})?;
Ok(Json(StripeArtcraftWebhookSuccessResponse {
success: true,
}))
}
async fn handle_webhook_payload(
mysql_connection: &mut PoolConnection<MySql>,
webhook_payload: Event,
stripe_event_type: &String
) -> Result<StripeArtcraftWebhookSummary, CommonWebError> {
let mut unhandled_event_type = false;
let mut webhook_summary = StripeArtcraftWebhookSummary {
maybe_user_token: None,
maybe_event_entity_id: None,
maybe_stripe_customer_id: None,
action_was_taken: false,
should_ignore_retry: false,
};
// NB:
// - "Webhook endpoints might occasionally receive the same event more than once."
// - "Stripe does not guarantee delivery of events in the order in which they are generated."
/*
match webhook_payload.type_ {
// =============== CHECKOUT SESSIONS ===============
EventType::CheckoutSessionCompleted => {
if let EventObject::CheckoutSession(checkout_session) = webhook_payload.data.object {
webhook_summary = checkout_session_completed_handler(checkout_session)?;
}
}
// EventType::CheckoutSessionExpired => {}
// EventType::CheckoutSessionAsyncPaymentFailed => {}
// EventType::CheckoutSessionAsyncPaymentSucceeded => {}
// =============== CUSTOMERS ===============
EventType::CustomerCreated => {
if let EventObject::Customer(customer) = webhook_payload.data.object {
webhook_summary = customer_created_handler(&customer)?;
}
}
EventType::CustomerUpdated => {
if let EventObject::Customer(customer) = webhook_payload.data.object {
webhook_summary = customer_updated_handler(&customer)?;
}
}
EventType::CustomerDeleted => {
if let EventObject::Customer(customer) = webhook_payload.data.object {
webhook_summary = customer_deleted_handler(&customer)?;
}
}
// =============== CUSTOMER SUBSCRIPTIONS ===============
EventType::CustomerSubscriptionCreated => {
if let EventObject::Subscription(subscription) = webhook_payload.data.object {
webhook_summary = customer_subscription_created_handler(
&subscription,
internal_subscription_product_lookup.get_ref(),
&mysql_pool).await?;
}
}
EventType::CustomerSubscriptionUpdated => {
if let EventObject::Subscription(subscription) = webhook_payload.data.object {
webhook_summary = customer_subscription_updated_handler(
&subscription,
internal_subscription_product_lookup.get_ref(),
&mysql_pool).await?;
}
}
EventType::CustomerSubscriptionDeleted => {
if let EventObject::Subscription(subscription) = webhook_payload.data.object {
webhook_summary = customer_subscription_deleted_handler(
&subscription,
internal_subscription_product_lookup.get_ref(),
&mysql_pool).await?;
}
}
// EventType::CustomerSubscriptionPendingUpdateApplied => {}
// EventType::CustomerSubscriptionPendingUpdateExpired => {}
// EventType::CustomerSubscriptionTrialWillEnd => {}
// =============== INVOICES ===============
EventType::InvoiceCreated => {
// TODO: We need to respond to this so we don't hold payments up by 72 hours!
// See: https://stripe.com/docs/billing/subscriptions/webhooks
}
EventType::InvoiceUpdated => {
if let EventObject::Invoice(invoice) = webhook_payload.data.object {
webhook_summary = invoice_updated_handler(&invoice)?;
}
}
EventType::InvoicePaid => {
if let EventObject::Invoice(invoice) = webhook_payload.data.object {
webhook_summary = invoice_paid_handler(&invoice)?;
}
}
EventType::InvoicePaymentSucceeded => {
if let EventObject::Invoice(invoice) = webhook_payload.data.object {
webhook_summary = invoice_payment_succeeded_handler(&invoice)?;
}
}
EventType::InvoicePaymentFailed => {
if let EventObject::Invoice(invoice) = webhook_payload.data.object {
webhook_summary = invoice_payment_failed_handler(&invoice)?;
}
}
// EventType::InvoiceDeleted => {}
// EventType::InvoiceFinalizationFailed => {}
// EventType::InvoiceFinalized => {}
// EventType::InvoiceItemCreated => {}
// EventType::InvoiceItemDeleted => {}
// EventType::InvoiceItemUpdated => {}
// EventType::InvoiceMarkedUncollectible => {}
// EventType::InvoicePaymentActionRequired => {}
// EventType::InvoiceSent => {}
// EventType::InvoiceUpcoming => {}
// EventType::InvoiceUpdated => {}
// EventType::InvoiceVoided => {}
// =============== PAYMENT INTENTS ===============
EventType::PaymentIntentSucceeded => {
if let EventObject::PaymentIntent(payment_intent) = webhook_payload.data.object {
webhook_summary = payment_intent_succeeded_handler(&payment_intent)?;
}
}
// EventType::PaymentIntentAmountCapturableUpdated => {}
// EventType::PaymentIntentCanceled => {}
// EventType::PaymentIntentCreated => {}
// EventType::PaymentIntentPartiallyFunded => {}
// EventType::PaymentIntentPaymentFailed => {}
// EventType::PaymentIntentProcessing => {}
// EventType::PaymentIntentRequiresAction => {}
// EventType::PaymentIntentRequiresCapture => {}
// =============== CHARGES ===============
EventType::ChargeSucceeded => {
if let EventObject::Charge(charge) = webhook_payload.data.object {
webhook_summary = charge_succeeded_handler(&charge)?;
}
}
// =============== Ignored ===============
// Ignoring these types:
// Account* (6),
// ApplicationFee* (2),
// BalanceAvailable (1),
// BillingPortal* (2),
// CapabilityCreated (1),
// Charge* (13),
// [CheckoutSession* handled above],
// Coupon* (3),
// CreditNote* (3),
// CustomerDiscount* (3),
// CustomerSource* (4),
// [CustomerSubscription* handled above],
// CreditTax* (3),
// FileCreated (1),
// IdentityVerification* (6),
// [Invoice* handled above],
// IssuingAuthorization* (3),
// IssuingCard* (2),
// IssuingCardholder* (2),
// IssuingDispute* (2),
// IssuingTransaction* (2),
// MandateUpdated (1),
// Order* (6),
// [PaymentIntent* handled above],
// PaymentLink* (2),
// PaymentMethod* (4),
// Payout* (5),
// Person* (3),
// Plan* (3),
// Price* (3),
// Product* (3),
// PromotionCode* (2),
// Quote* (4),
// RadarEarlyFraudWarning* (2),
// Recipient* (3),
// ReportingReport* (3),
// Review* (2),
// SetupIntent* (5), -- N/A; collecting payments in the future we don't yet have information for (eg. crowdfunding, rental car, utility bill)
// SubscriptionSchedule* (7), -- N/A; backdate subscriptions or schedule one to start later (eg. for physical goods, like a newspaper)
// SigmaScheduledQueryRunCreated (1),
// Sku (3),
// Source* (7),
// TaxRate* (2),
// TerminalReader* (2),
// TestHelpersTestClock* (5),
// Topup* (5),
// Transfer* (5),
_ => {
unhandled_event_type = true;
},
}
*/
// To play with the payload contents as JSON:
// let json = serde_json::ser::to_string(&event_payload).unwrap();
// info!("event payload as json: {}", json);
if unhandled_event_type {
warn!("Unhandled Stripe webhook event type: {} ({:?})",
&stripe_event_type,
&webhook_payload.type_);
}
Ok(webhook_summary)
}
@@ -0,0 +1,21 @@
/// Reports common information from the webhook handlers
pub struct StripeArtcraftWebhookSummary {
/// If we recorded our own internal user ID as Stripe metadata, this passes it upstream.
pub maybe_user_token: Option<String>,
/// This is the core entity type associated with the webhook event.
/// We pass this upstream so we can record it and look it up later for debugging.
pub maybe_event_entity_id: Option<String>,
/// This is the stripe customer ID, if it was associated with the event.
pub maybe_stripe_customer_id: Option<String>,
/// Whether we took any sort of action in response to the webhook.
/// Not all event types have handlers yet, and even so, sometimes we may choose to do nothing.
pub action_was_taken: bool,
/// Whether any retries should be dropped.
/// This helps us handle event with idempotency in the event Stripe replays them (and it can!)
pub should_ignore_retry: bool,
}
@@ -0,0 +1,2 @@
pub mod endpoints;
pub mod utils;
@@ -0,0 +1,30 @@
use stripe::Client;
#[derive(Clone)]
pub struct ArtcraftStripeConfig {
pub secret_key: String,
pub secret_webhook_signing_key: String,
pub checkout_success_url: String,
pub checkout_cancel_url: String,
}
#[derive(Clone)]
pub struct ArtcraftStripeConfigWithClient {
pub secret_key: String,
pub secret_webhook_signing_key: String,
pub checkout_success_url: String,
pub checkout_cancel_url: String,
pub client: Client,
}
impl ArtcraftStripeConfig {
pub fn to_config_with_client(&self) -> ArtcraftStripeConfigWithClient {
ArtcraftStripeConfigWithClient {
secret_key: self.secret_key.clone(),
secret_webhook_signing_key: self.secret_webhook_signing_key.clone(),
checkout_success_url: self.checkout_success_url.clone(),
checkout_cancel_url: self.checkout_cancel_url.clone(),
client: Client::new(self.secret_key.clone()),
}
}
}
@@ -0,0 +1,101 @@
use actix_http::StatusCode;
use actix_web::{HttpResponse, HttpResponseBuilder, ResponseError};
use std::error::Error;
use std::fmt::{Display, Formatter};
use log::error;
use serde_derive::Serialize;
#[derive(Debug)]
pub enum CommonWebError {
// TODO: Bad input with error fields vector.
/// Bad input with a user-facing error message.
BadInputWithSimpleMessage(String),
/// Entity not found.
NotFound,
/// Not authorized to perform the action.
NotAuthorized,
/// Server error with no reason.
ServerError,
/// Payment required with no extra info
PaymentRequired,
}
impl Error for CommonWebError {}
impl Display for CommonWebError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Debug, Serialize)]
struct SerializeSimpleErrorWithoutMessage<'a> {
success: bool,
error_code: u16,
error_code_str: Option<&'a str>,
}
#[derive(Debug, Serialize)]
struct SerializeSimpleErrorWithMessage<'a> {
success: bool,
error_code: u16,
error_code_str: Option<&'a str>,
message: &'a str,
}
impl ResponseError for CommonWebError {
fn status_code(&self) -> StatusCode {
match self {
CommonWebError::BadInputWithSimpleMessage(_) => StatusCode::BAD_REQUEST,
CommonWebError::NotFound => StatusCode::NOT_FOUND,
CommonWebError::NotAuthorized => StatusCode::UNAUTHORIZED,
CommonWebError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
CommonWebError::PaymentRequired => StatusCode::PAYMENT_REQUIRED,
}
}
fn error_response(&self) -> HttpResponse {
match self {
CommonWebError::BadInputWithSimpleMessage(msg) => {
HttpResponse::BadRequest()
.json(SerializeSimpleErrorWithMessage {
success: false,
error_code: self.status_code().as_u16(),
error_code_str: self.status_code().canonical_reason(),
message: msg,
})
},
_ => {
HttpResponseBuilder::new(self.status_code())
.json(SerializeSimpleErrorWithoutMessage {
success: false,
error_code: self.status_code().as_u16(),
error_code_str: self.status_code().canonical_reason(),
})
}
}
}
}
impl From<sqlx::Error> for CommonWebError {
fn from(value: sqlx::Error) -> Self {
error!("SQLx error: {:?}", value);
CommonWebError::ServerError
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_http::body::MessageBody;
#[test]
fn test_bad_input_json() {
let error = CommonWebError::BadInputWithSimpleMessage("foo bar baz".to_string());
let response = error.error_response();
let bytes = response.into_body().try_into_bytes().unwrap();
let body = String::from_utf8(bytes.to_vec()).unwrap();
assert_eq!(body, "{\"success\":false,\"error_code\":400,\"error_code_str\":\"Bad Request\",\"message\":\"foo bar baz\"}");
}
}
@@ -0,0 +1,3 @@
pub mod artcraft_stripe_config;
pub mod common_web_error;
pub mod verify_stripe_webhook_ip_address;
@@ -0,0 +1,48 @@
use std::collections::HashSet;
use actix_web::HttpRequest;
use anyhow::anyhow;
use errors::AnyhowResult;
use once_cell::sync::Lazy;
use http_server_common::request::get_request_ip::get_request_ip;
/// List of IP addresses that send webhook requests
/// From: https://stripe.com/docs/ips
static STRIPE_WEBHOOK_IP_ADDRESSES : Lazy<HashSet<String>> = Lazy::new(|| {
let mut ip_addresses = HashSet::new();
ip_addresses.extend([
"3.18.12.63",
"3.130.192.231",
"13.235.14.237",
"13.235.122.149",
"18.211.135.69",
"35.154.171.200",
"52.15.183.38",
"54.88.130.119",
"54.88.130.237",
"54.187.174.169",
"54.187.205.235",
"54.187.216.72",
].map(|s| s.to_string()));
ip_addresses
});
/// Verify that the request comes from a Stripe webhook client IP
/// Recommendation from: https://stripe.com/docs/webhooks/best-practices
pub fn verify_stripe_webhook_ip_address(http_request: &HttpRequest) -> AnyhowResult<()> {
let ip_address = get_request_ip(http_request);
if STRIPE_WEBHOOK_IP_ADDRESSES.contains(&ip_address) {
return Ok(());
}
// TODO: Only allow this in development
if ip_address == "127.0.0.1" {
return Ok(());
}
Err(anyhow!("Not a valid Stripe webhook IP address: {:?}", &ip_address))
}
@@ -180,15 +180,19 @@ pub async fn stripe_artcraft_create_checkout_session_handler(
}
}
CheckoutSession::create(&server_state.stripe_artcraft.client, params)
.await
.map_err(|e| {
error!("Stripe Error: {:?}", e);
CommonWebError::ServerError
})?
// TODO: THis is now broken
//CheckoutSession::create(&server_state.stripe_artcraft.client, params)
// .await
// .map_err(|e| {
// error!("Stripe Error: {:?}", e);
// CommonWebError::ServerError
// })?
()
};
let url = checkout_session.url.ok_or(CommonWebError::ServerError)?;
//let url = checkout_session.url.ok_or(CommonWebError::ServerError)?;
let url = "todo".to_string();
// Best effort to delete Redis session cache
internal_session_cache_purge.best_effort_purge_session_cache(&http_request);
@@ -45,6 +45,7 @@ use actix_helpers::middleware::disabled_endpoint_filter::disabled_endpoint_filte
use actix_helpers::middleware::disabled_endpoint_filter::disabled_endpoints::disabled_endpoints::DisabledEndpoints;
use actix_helpers::middleware::disabled_endpoint_filter::disabled_endpoints::exact_match_disabled_endpoints::ExactMatchDisabledEndpoints;
use actix_helpers::middleware::disabled_endpoint_filter::disabled_endpoints::prefix_disabled_endpoints::PrefixDisabledEndpoints;
use billing_artcraft_component::utils::artcraft_stripe_config::ArtcraftStripeConfig;
use billing_component::stripe::stripe_config::{FullUrlOrPath, StripeCheckoutConfigs, StripeConfig, StripeCustomerPortalConfigs, StripeSecrets};
use billing_component::stripe::traits::internal_product_to_stripe_lookup::InternalProductToStripeLookup;
use billing_component::stripe::traits::internal_subscription_product_lookup::InternalSubscriptionProductLookup;
@@ -413,17 +414,12 @@ async fn main() -> AnyhowResult<()> {
client: stripe_client,
},
stripe_artcraft: {
let secret_key = easyenv::get_env_string_required("STRIPE_ARTCRAFT_SECRET_KEY")?;
StripeArtcraftSettings {
secret_key: secret_key.clone(),
ArtcraftStripeConfig {
secret_key: easyenv::get_env_string_required("STRIPE_ARTCRAFT_SECRET_KEY")?,
secret_webhook_signing_key: easyenv::get_env_string_required("STRIPE_ARTCRAFT_SECRET_WEBHOOK_KEY")?,
checkout_success_url: easyenv::get_env_string_required("STRIPE_ARTCRAFT_CHECKOUT_SUCCESS_URL")?,
checkout_cancel_url: easyenv::get_env_string_required("STRIPE_ARTCRAFT_CHECKOUT_CANCEL_URL")?,
client: {
let api_secret = secret_key.clone();
async_stripe_artcraft::Client::new(api_secret)
},
}
}.to_config_with_client()
},
hostname: server_hostname,
startup_time,
@@ -623,6 +619,7 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()>
// NB: app_data being clone()'d below should all be safe (dependencies included)
let app = App::new()
.app_data(web::Data::new(server_state_arc.stripe_artcraft.clone()))
.app_data(web::Data::new(server_state_arc.firehose_publisher.clone()))
.app_data(web::Data::new(server_state_arc.mysql_pool.clone()))
.app_data(web::Data::new(server_state_arc.redis_pool.clone()))
@@ -5,6 +5,7 @@ use sqlx::MySqlPool;
use actix_helpers::middleware::banned_cidr_filter::banned_cidr_set::BannedCidrSet;
use actix_helpers::middleware::banned_ip_filter::ip_ban_list::ip_ban_list::IpBanList;
use billing_artcraft_component::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient;
use billing_component::stripe::stripe_config::StripeConfig;
use cloud_storage::bucket_client::BucketClient;
use email_sender::smtp_email_sender::SmtpEmailSender;
@@ -49,7 +50,7 @@ pub struct ServerState {
pub server_info: ServerInfo,
pub stripe: StripeSettings,
pub stripe_artcraft: StripeArtcraftSettings,
pub stripe_artcraft: ArtcraftStripeConfigWithClient,
pub hostname: String,