diff --git a/crates/component_subsystem/billing/src/stripe/helpers/common_metadata_keys.rs b/crates/component_subsystem/billing/src/stripe/helpers/common_metadata_keys.rs new file mode 100644 index 0000000000..fc2d863178 --- /dev/null +++ b/crates/component_subsystem/billing/src/stripe/helpers/common_metadata_keys.rs @@ -0,0 +1,5 @@ +//! Keys that are used as metadata frequently. + +pub const METADATA_USER_TOKEN : &'static str = "user_token"; +pub const METADATA_USERNAME : &'static str = "username"; +pub const METADATA_EMAIL : &'static str = "email"; diff --git a/crates/component_subsystem/billing/src/stripe/helpers/mod.rs b/crates/component_subsystem/billing/src/stripe/helpers/mod.rs index 3566a625b6..36f3c72539 100644 --- a/crates/component_subsystem/billing/src/stripe/helpers/mod.rs +++ b/crates/component_subsystem/billing/src/stripe/helpers/mod.rs @@ -1 +1,2 @@ +pub mod common_metadata_keys; pub mod expand_customer_id; \ No newline at end of file diff --git a/crates/component_subsystem/billing/src/stripe/http_endpoints/checkout/stripe_create_checkout_session_shared.rs b/crates/component_subsystem/billing/src/stripe/http_endpoints/checkout/stripe_create_checkout_session_shared.rs index bc47bc9c46..78219da3e2 100644 --- a/crates/component_subsystem/billing/src/stripe/http_endpoints/checkout/stripe_create_checkout_session_shared.rs +++ b/crates/component_subsystem/billing/src/stripe/http_endpoints/checkout/stripe_create_checkout_session_shared.rs @@ -3,9 +3,12 @@ use container_common::anyhow_result::AnyhowResult; use crate::stripe::stripe_config::StripeConfig; use log::error; use std::collections::HashMap; -use stripe::{CheckoutSession, CheckoutSessionMode, CreateCheckoutSession, CreateCheckoutSessionLineItems}; +use stripe::{CheckoutSession, CheckoutSessionMode, CreateCheckoutSession, CreateCheckoutSessionLineItems, CreateCheckoutSessionPaymentIntentData, CreateCheckoutSessionSubscriptionData}; +use crate::stripe::helpers::common_metadata_keys::{METADATA_EMAIL, METADATA_USER_TOKEN, METADATA_USERNAME}; // 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"; @@ -31,12 +34,6 @@ pub async fn stripe_create_checkout_session_shared( .as_deref() .ok_or(anyhow!("Checkout Cancel URL not configured"))?; - let mut metadata = HashMap::new(); - - if let Some(token) = user_token { - metadata.insert("user_token".to_string(), token.to_string()); - } - let checkout_session = { let mut params = CreateCheckoutSession::new( cancel_url, @@ -45,6 +42,27 @@ pub async fn stripe_create_checkout_session_shared( params.mode = Some(CheckoutSessionMode::Subscription); + let mut metadata = HashMap::new(); + + if let Some(token) = user_token { + metadata.insert(METADATA_USER_TOKEN.to_string(), token.to_string()); + metadata.insert(METADATA_USERNAME.to_string(), token.to_string()); + metadata.insert(METADATA_EMAIL.to_string(), token.to_string()); + } + + // NB: This metadata attaches to the subscription entity itself. + // https://support.stripe.com/questions/using-metadata-with-checkout-sessions + params.subscription_data = Some(CreateCheckoutSessionSubscriptionData { + metadata, + ..Default::default() + }); + + // If not a subscription - + //params.payment_intent_data = Some(CreateCheckoutSessionPaymentIntentData { + // metadata: metadata.clone(), + // ..Default::default() + //}); + params.line_items = Some(vec![ CreateCheckoutSessionLineItems { price: Some(PRODUCT_FAKEYOU_BASIC_PRICE_ID.to_string()), diff --git a/crates/component_subsystem/billing/src/stripe/http_endpoints/stripe_webhook_handler.rs b/crates/component_subsystem/billing/src/stripe/http_endpoints/stripe_webhook_handler.rs index 4adfb1d210..6679074330 100644 --- a/crates/component_subsystem/billing/src/stripe/http_endpoints/stripe_webhook_handler.rs +++ b/crates/component_subsystem/billing/src/stripe/http_endpoints/stripe_webhook_handler.rs @@ -6,6 +6,8 @@ use actix_web::{web, HttpResponse, HttpRequest}; use chrono::{DateTime, Utc}; use crate::stripe::stripe_config::StripeConfig; use crate::stripe::webhook_event_handlers::checkout_session_completed_handler::checkout_session_completed_handler; +use crate::stripe::webhook_event_handlers::customer_subscription::customer_subscription_created_handler::customer_subscription_created_handler; +use crate::stripe::webhook_event_handlers::customer_subscription::customer_subscription_updated_handler::customer_subscription_updated_handler; use crate::stripe::webhook_event_handlers::invoice_paid_handler::invoice_paid_handler; use crate::stripe::webhook_event_handlers::stripe_webhook_error::StripeWebhookError; use http_server_common::request::get_request_header_optional::get_request_header_optional; @@ -16,6 +18,7 @@ use sqlx::MySqlPool; use std::collections::HashMap; use std::fmt; use stripe::{EventObject, EventType, PaymentIntentStatus, Webhook}; +use crate::stripe::webhook_event_handlers::customer_subscription::customer_subscription_deleted_handler::customer_subscription_deleted_handler; #[derive(Serialize)] pub struct StripeWebhookSuccessResponse { @@ -51,6 +54,13 @@ pub async fn stripe_webhook_handler( warn!("Event type: {:?}", webhook_payload.event_type); + // Events seen (handled): + // + // CustomerSubscriptionCreated ------ + // CustomerSubscriptionUpdated ------- + // + // Events seen (not yet handled): + // // CheckoutSessionCompleted // ChargeSucceeded // PaymentMethodAttached @@ -58,9 +68,7 @@ pub async fn stripe_webhook_handler( // CustomerUpdated // InvoiceCreated // InvoiceFinalized - // CustomerSubscriptionCreated ------ // InvoiceUpdated - // CustomerSubscriptionUpdated ------- // InvoicePaymentSucceeded // PaymentIntentSucceeded // PaymentIntentCreated @@ -71,6 +79,9 @@ pub async fn stripe_webhook_handler( // NB: Whether this was test data or live data let is_production = webhook_payload.livemode; + // 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.event_type { EventType::SubscriptionScheduleAborted => {} EventType::SubscriptionScheduleCanceled => {} @@ -80,11 +91,38 @@ pub async fn stripe_webhook_handler( EventType::SubscriptionScheduleReleased => {} EventType::SubscriptionScheduleUpdated => {} + // =============== CHECKOUT SESSIONS =============== + EventType::CheckoutSessionCompleted => { if let EventObject::CheckoutSession(checkout_session) = webhook_payload.data.object { let _r = checkout_session_completed_handler(checkout_session)?; } } + + // =============== CUSTOMER SUBSCRIPTIONS =============== + + EventType::CustomerSubscriptionCreated => { + if let EventObject::Subscription(subscription) = webhook_payload.data.object { + let _r = customer_subscription_created_handler(&subscription)?; + } + } + EventType::CustomerSubscriptionUpdated => { + if let EventObject::Subscription(subscription) = webhook_payload.data.object { + let _r = customer_subscription_updated_handler(&subscription)?; + } + } + EventType::CustomerSubscriptionDeleted => { + if let EventObject::Subscription(subscription) = webhook_payload.data.object { + let _r = customer_subscription_deleted_handler(&subscription)?; + } + } + EventType::CustomerSubscriptionPendingUpdateApplied => { + } + EventType::CustomerSubscriptionPendingUpdateExpired => { + } + + // =============== 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 @@ -94,12 +132,12 @@ pub async fn stripe_webhook_handler( let _r = invoice_paid_handler(&invoice)?; } } - EventType::InvoicePaymentFailed => { - } EventType::PaymentIntentSucceeded => { if let EventObject::PaymentIntent(payment_intent) = webhook_payload.data.object { } } + EventType::InvoicePaymentFailed => { + } _ => {}, } diff --git a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/checkout_session_completed_handler.rs b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/checkout_session_completed_handler.rs index d7a5f85276..ab809c9a79 100644 --- a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/checkout_session_completed_handler.rs +++ b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/checkout_session_completed_handler.rs @@ -1,6 +1,8 @@ +use log::{error, warn}; use container_common::anyhow_result::AnyhowResult; use crate::stripe::helpers::expand_customer_id::expand_customer_id; use stripe::CheckoutSession; +use crate::stripe::helpers::common_metadata_keys::METADATA_USER_TOKEN; use crate::stripe::webhook_event_handlers::stripe_webhook_error::StripeWebhookError; // After the subscription signup succeeds, the customer returns to your website at the success_url, @@ -10,7 +12,6 @@ use crate::stripe::webhook_event_handlers::stripe_webhook_error::StripeWebhookEr // and send them to the customer portal to update their payment method. pub fn checkout_session_completed_handler(checkout_session: CheckoutSession) -> Result<(), StripeWebhookError> { - let stripe_checkout_id = checkout_session.id.to_string(); // NB: We'll need this to send them to the "customer portal", which is how they can modify or cancel @@ -20,8 +21,10 @@ pub fn checkout_session_completed_handler(checkout_session: CheckoutSession) -> .map(|c| expand_customer_id(c)); // NB: Our internal user token. - let maybe_user_token = checkout_session.metadata.get("user_token") + let maybe_user_token = checkout_session.metadata.get(METADATA_USER_TOKEN) .map(|t| t.to_string()); + error!(">>> checkout.session.completed: {:?}, {:?}", maybe_stripe_customer_id, maybe_user_token); + Ok(()) } diff --git a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer_subscription/customer_subscription_created_handler.rs b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer_subscription/customer_subscription_created_handler.rs new file mode 100644 index 0000000000..ce343f6cef --- /dev/null +++ b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer_subscription/customer_subscription_created_handler.rs @@ -0,0 +1,26 @@ +use crate::stripe::helpers::common_metadata_keys::METADATA_USER_TOKEN; +use crate::stripe::helpers::expand_customer_id::expand_customer_id; +use crate::stripe::webhook_event_handlers::stripe_webhook_error::StripeWebhookError; +use log::{error, warn}; +use stripe::Subscription; + +/// Handle event type: 'customer.subscription.created' +pub fn customer_subscription_created_handler(subscription: &Subscription) -> Result<(), StripeWebhookError> { + + let stripe_subscription_id = subscription.id.to_string(); + + // NB: We'll need this to send them to the "customer portal", which is how they can modify or + // cancel their subscriptions. + let maybe_stripe_customer_id = subscription.customer + .as_ref() + .map(|c| expand_customer_id(c)); + + // NB: Our internal user token. + let maybe_user_token = subscription.metadata.get(METADATA_USER_TOKEN) + .map(|t| t.to_string()); + + error!(">>> customer.subscription.created: {:?}, {:?}, {:?}", + stripe_subscription_id, maybe_stripe_customer_id, maybe_user_token); + + Ok(()) +} diff --git a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer_subscription/customer_subscription_deleted_handler.rs b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer_subscription/customer_subscription_deleted_handler.rs new file mode 100644 index 0000000000..b3070821b3 --- /dev/null +++ b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer_subscription/customer_subscription_deleted_handler.rs @@ -0,0 +1,26 @@ +use crate::stripe::helpers::common_metadata_keys::METADATA_USER_TOKEN; +use crate::stripe::helpers::expand_customer_id::expand_customer_id; +use crate::stripe::webhook_event_handlers::stripe_webhook_error::StripeWebhookError; +use log::{error, warn}; +use stripe::Subscription; + +/// Handle event type: 'customer.subscription.deleted' +pub fn customer_subscription_deleted_handler(subscription: &Subscription) -> Result<(), StripeWebhookError> { + + let stripe_subscription_id = subscription.id.to_string(); + + // NB: We'll need this to send them to the "customer portal", which is how they can modify or + // cancel their subscriptions. + let maybe_stripe_customer_id = subscription.customer + .as_ref() + .map(|c| expand_customer_id(c)); + + // NB: Our internal user token. + let maybe_user_token = subscription.metadata.get(METADATA_USER_TOKEN) + .map(|t| t.to_string()); + + error!(">>> customer.subscription.deleted: {:?}, {:?}, {:?}", + stripe_subscription_id, maybe_stripe_customer_id, maybe_user_token); + + Ok(()) +} diff --git a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer_subscription/customer_subscription_updated_handler.rs b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer_subscription/customer_subscription_updated_handler.rs new file mode 100644 index 0000000000..20faa9efb9 --- /dev/null +++ b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer_subscription/customer_subscription_updated_handler.rs @@ -0,0 +1,26 @@ +use crate::stripe::helpers::common_metadata_keys::METADATA_USER_TOKEN; +use crate::stripe::helpers::expand_customer_id::expand_customer_id; +use crate::stripe::webhook_event_handlers::stripe_webhook_error::StripeWebhookError; +use log::{error, warn}; +use stripe::Subscription; + +/// Handle event type: 'customer.subscription.updated' +pub fn customer_subscription_updated_handler(subscription: &Subscription) -> Result<(), StripeWebhookError> { + + let stripe_subscription_id = subscription.id.to_string(); + + // NB: We'll need this to send them to the "customer portal", which is how they can modify or + // cancel their subscriptions. + let maybe_stripe_customer_id = subscription.customer + .as_ref() + .map(|c| expand_customer_id(c)); + + // NB: Our internal user token. + let maybe_user_token = subscription.metadata.get(METADATA_USER_TOKEN) + .map(|t| t.to_string()); + + error!(">>> customer.subscription.updated: {:?}, {:?}, {:?}", + stripe_subscription_id, maybe_stripe_customer_id, maybe_user_token); + + Ok(()) +} diff --git a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer_subscription/mod.rs b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer_subscription/mod.rs new file mode 100644 index 0000000000..ba54fd38bb --- /dev/null +++ b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer_subscription/mod.rs @@ -0,0 +1,3 @@ +pub mod customer_subscription_created_handler; +pub mod customer_subscription_deleted_handler; +pub mod customer_subscription_updated_handler; \ No newline at end of file diff --git a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/invoice_paid_handler.rs b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/invoice_paid_handler.rs index 856a704186..d0306a3570 100644 --- a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/invoice_paid_handler.rs +++ b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/invoice_paid_handler.rs @@ -2,6 +2,7 @@ use container_common::anyhow_result::AnyhowResult; use crate::stripe::helpers::expand_customer_id::expand_customer_id; use log::warn; use stripe::Invoice; +use crate::stripe::helpers::common_metadata_keys::METADATA_USER_TOKEN; use crate::stripe::webhook_event_handlers::stripe_webhook_error::StripeWebhookError; // https://stripe.com/docs/billing/subscriptions/webhooks : @@ -29,11 +30,10 @@ pub fn invoice_paid_handler(invoice: &Invoice) -> Result<(), StripeWebhookError> .map(|c| expand_customer_id(c)); // NB: Our internal user token. - let maybe_user_token = invoice.metadata.get("user_token") + let maybe_user_token = invoice.metadata.get(METADATA_USER_TOKEN) .map(|t| t.to_string()); - - warn!("invoice_paid: {:?}, {:?}, {:?}", paid_status, maybe_stripe_customer_id, maybe_user_token); + warn!(">>> invoice.paid: {:?}, {:?}, {:?}", paid_status, maybe_stripe_customer_id, maybe_user_token); Ok(()) } \ No newline at end of file diff --git a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/mod.rs b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/mod.rs index 78fe7ed54b..072d6819cb 100644 --- a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/mod.rs +++ b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/mod.rs @@ -1,3 +1,5 @@ -pub (crate) mod checkout_session_completed_handler; -pub (crate) mod invoice_paid_handler; pub mod stripe_webhook_error; + +pub (crate) mod checkout_session_completed_handler; +pub (crate) mod customer_subscription; +pub (crate) mod invoice_paid_handler;