diff --git a/_migrations/2022-09-17-053418_stripe_webhook_event_logs/up.sql b/_migrations/2022-09-17-053418_stripe_webhook_event_logs/up.sql index d5314035a5..903ec73044 100644 --- a/_migrations/2022-09-17-053418_stripe_webhook_event_logs/up.sql +++ b/_migrations/2022-09-17-053418_stripe_webhook_event_logs/up.sql @@ -33,8 +33,12 @@ CREATE TABLE stripe_webhook_event_logs ( -- ========== INTERNAL SYSTEM METADATA ========== - -- Whether we handled the event - event_was_handled BOOLEAN NOT NULL DEFAULT FALSE, + -- Whether we handled the event with any sort of action + action_was_taken BOOLEAN NOT NULL DEFAULT FALSE, + + -- Whether we should ignore any future retries by Stripe replaying the event. + -- This helps us treat events with idempotency. + should_ignore_retry BOOLEAN NOT NULL DEFAULT FALSE, -- Some event types may attach a user token maybe_user_token VARCHAR(32) DEFAULT NULL, 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 ec390a5257..8e82bb96eb 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 @@ -98,7 +98,8 @@ pub async fn stripe_webhook_handler( maybe_user_token: None, maybe_event_entity_id: None, maybe_stripe_customer_id: None, - event_was_handled: false + action_was_taken: false, + should_ignore_retry: false, }; // NB: @@ -298,7 +299,8 @@ pub async fn stripe_webhook_handler( stripe_event_created_at, stripe_is_production, maybe_user_token: webhook_summary.maybe_user_token, - event_was_handled: webhook_summary.event_was_handled, + action_was_taken: webhook_summary.action_was_taken, + should_ignore_retry: webhook_summary.should_ignore_retry, }; query.insert(&mysql_pool) diff --git a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/charge/charge_succeeded_handler.rs b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/charge/charge_succeeded_handler.rs index 635f8e5820..85a47bd311 100644 --- a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/charge/charge_succeeded_handler.rs +++ b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/charge/charge_succeeded_handler.rs @@ -26,6 +26,7 @@ pub fn charge_succeeded_handler(charge: &Charge) -> Result maybe_user_token: maybe_user_token, maybe_event_entity_id: Some(stripe_checkout_id), maybe_stripe_customer_id: maybe_stripe_customer_id, - event_was_handled: false, + action_was_taken: false, + should_ignore_retry: false, }) } diff --git a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer/customer_created_handler.rs b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer/customer_created_handler.rs index afb2187d9b..b7c045b1c1 100644 --- a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer/customer_created_handler.rs +++ b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/customer/customer_created_handler.rs @@ -20,6 +20,7 @@ pub fn customer_created_handler(customer: &Customer) -> Result Result Result { // NB: This is a terminal status and the subscription cannot be updated any further. - skip_update = true; + should_process_update = false; + should_ignore_retry = true; } _ => {} } @@ -44,7 +47,7 @@ pub async fn customer_subscription_updated_handler( // TODO: record cancel_at (future_cancel_at), canceled_at, ended_at (if subscription ended, when it ended), start_date - if !skip_update { + if should_process_update { let upsert = UpsertSubscriptionByStripeId { stripe_subscription_id: &summary.stripe_subscription_id, maybe_user_token: summary.user_token.as_deref(), @@ -69,12 +72,16 @@ pub async fn customer_subscription_updated_handler( error!("Mysql error: {:?}", err); StripeWebhookError::ServerError })?; + + action_was_taken = true; + should_ignore_retry = true; } Ok(StripeWebhookSummary { maybe_user_token: summary.user_token, maybe_event_entity_id: Some(summary.stripe_subscription_id), maybe_stripe_customer_id: Some(summary.stripe_customer_id), - event_was_handled: true, + action_was_taken, + should_ignore_retry, }) } diff --git a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/invoice/invoice_paid_handler.rs b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/invoice/invoice_paid_handler.rs index 29eaeeb6c1..a24037ddf6 100644 --- a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/invoice/invoice_paid_handler.rs +++ b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/invoice/invoice_paid_handler.rs @@ -78,7 +78,8 @@ pub fn invoice_paid_handler(invoice: &Invoice) -> Result Result Result Result Resul maybe_user_token: maybe_user_token, maybe_event_entity_id: Some(payment_intent_id), maybe_stripe_customer_id: maybe_stripe_customer_id, - event_was_handled: false, + action_was_taken: false, + should_ignore_retry: false, }) } diff --git a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/stripe_webhook_summary.rs b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/stripe_webhook_summary.rs index 89d93cc22e..6f1c0a0a9d 100644 --- a/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/stripe_webhook_summary.rs +++ b/crates/component_subsystem/billing/src/stripe/webhook_event_handlers/stripe_webhook_summary.rs @@ -11,7 +11,11 @@ pub struct StripeWebhookSummary { /// This is the stripe customer ID, if it was associated with the event. pub maybe_stripe_customer_id: Option, - /// Whether we took action in response to the webhook. - /// Not all event types have handlers yet. - pub event_was_handled: bool, + /// 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, } diff --git a/crates/lib/database_queries/src/queries/billing/stripe/insert_stripe_webhook_event_log.rs b/crates/lib/database_queries/src/queries/billing/stripe/insert_stripe_webhook_event_log.rs index 478735ec72..dc09ff4db1 100644 --- a/crates/lib/database_queries/src/queries/billing/stripe/insert_stripe_webhook_event_log.rs +++ b/crates/lib/database_queries/src/queries/billing/stripe/insert_stripe_webhook_event_log.rs @@ -13,7 +13,8 @@ pub struct InsertStripeWebhookEventLog { pub stripe_event_created_at: NaiveDateTime, pub stripe_is_production: bool, pub maybe_user_token: Option, - pub event_was_handled: bool, + pub action_was_taken: bool, + pub should_ignore_retry: bool, } impl InsertStripeWebhookEventLog { @@ -31,7 +32,8 @@ SET stripe_event_created_at = ?, stripe_is_production = ?, maybe_user_token = ?, - event_was_handled = ? + action_was_taken = ?, + should_ignore_retry = ? "#, &self.stripe_event_id, &self.stripe_event_type, @@ -40,7 +42,8 @@ SET &self.stripe_event_created_at, self.stripe_is_production, &self.maybe_user_token, - self.event_was_handled, + self.action_was_taken, + self.should_ignore_retry, ); let query_result = query.execute(mysql_pool).await;