From d4ec659568f00eb5d026c91c42ca33f0c09c5f9a Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 16 Sep 2025 22:56:38 -0400 Subject: [PATCH] cleanup --- ...ft_create_credits_pack_checkout_handler.rs | 31 +---- .../checkout_session_completed_handler.rs | 34 ----- .../checkout_session/mod.rs | 1 - .../handle_webhook_event_enrichment.rs | 130 ++++-------------- .../ignore_known_unwanted_events.rs | 16 ++- .../webhook/webhook_event_enrichment/mod.rs | 1 - 6 files changed, 48 insertions(+), 165 deletions(-) delete mode 100644 crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/checkout_session/checkout_session_completed_handler.rs delete mode 100644 crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/checkout_session/mod.rs diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_credits_pack_checkout_handler.rs b/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_credits_pack_checkout_handler.rs index 2d0f5db368..dd5ef07ea0 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_credits_pack_checkout_handler.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/checkout/stripe_artcraft_create_credits_pack_checkout_handler.rs @@ -94,7 +94,7 @@ pub async fn stripe_artcraft_create_credits_pack_checkout_handler( .map(|sub| sub.stripe_customer_id.as_str()) .map(|customer_id| CustomerId::from_str(customer_id).ok()) // NB: Infallible parse .flatten(); - + if maybe_existing_stripe_customer_id.is_none() { let result = find_user_stripe_customer_link_using_connection( &user_metadata.user_token_typed, @@ -114,32 +114,6 @@ pub async fn stripe_artcraft_create_credits_pack_checkout_handler( let cancel_url = stripe_config.checkout_cancel_url.clone(); let checkout_session = { - // `client_reference_id` - // Stripe Docs: - // A unique string to reference the Checkout Session. - // This can be a customer ID, a cart ID, or similar, and can be used to reconcile the session - // with your internal systems. - // - // Our Notes: - // This gets reported back in the Checkout Session (and related webhooks) as - // `client_reference_id`. Passing the same ID on multiple checkouts does not unify or - // cross-correlate customers and only seems to be metadata for the checkout session itself. - // This is probably only useful for tracking checkout session engagement. - //params.client_reference_id = Some("SOME_INTERNAL_ID"); - - // `customer_email` - // Stripe Docs: - // If provided, this value will be used when the Customer object is created. If not provided, - // customers will be asked to enter their email address. Use this parameter to prefill - // customer data if you already have an email on file. To access information about the - // customer once a session is complete, use the customer field. - // - // Our Notes: - // This does not look up previous customers with the same email and will not unify or - // cross-correlate customers. By default the field will be un-editable in the checkout flow - // if this is specified. - //params.customer_email = Some("email@example.com"); - let mut metadata = HashMap::new(); metadata.insert(STRIPE_ARTCRAFT_METADATA_USER_TOKEN.to_string(), user_metadata.user_token.to_string()); @@ -208,6 +182,9 @@ pub async fn stripe_artcraft_create_credits_pack_checkout_handler( } else { // If we don't have an existing customer_id, we want to create a new customer. // If we don't do that, the "customer" winds up as a "Guest" customer. + // The only danger in forcefully creating a new customer is that we might create + // duplicates (which Stripe won't merge) if we can't catch and consolidate across + // all events. checkout_builder = checkout_builder .customer_creation(CreateCheckoutSessionCustomerCreation::Always); } diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/checkout_session/checkout_session_completed_handler.rs b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/checkout_session/checkout_session_completed_handler.rs deleted file mode 100644 index 09466d7e32..0000000000 --- a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/checkout_session/checkout_session_completed_handler.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::endpoints::webhook::common::stripe_artcraft_webhook_error::StripeArtcraftWebhookError; -use crate::endpoints::webhook::common::webhook_event_log_summary::WebhookEventLogSummary; -use crate::utils::expand_ids::expand_customer_id::expand_customer_id; -use crate::utils::metadata::get_metadata_user_token::get_metadata_user_token; -use stripe_checkout::CheckoutSession; - -// After the subscription signup succeeds, the customer returns to your website at the success_url, -// which initiates a checkout.session.completed webhooks. When you receive a checkout.session.completed -// event, you can provision the subscription. Continue to provision each month (if billing monthly) as -// you receive invoice.paid events. If you receive an invoice.payment_failed event, notify your customer -// and send them to the customer portal to update their payment method. -pub fn checkout_session_completed_handler(checkout_session: CheckoutSession) -> Result { - 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 - // their subscriptions. - let maybe_stripe_customer_id = checkout_session.customer - .as_ref() - .map(|c| expand_customer_id(c)); - - // NB: Our internal user token. - let maybe_user_token = checkout_session.metadata - .as_ref() - .map(|metadata| get_metadata_user_token(metadata)) - .flatten(); - - Ok(WebhookEventLogSummary { - maybe_user_token, - maybe_event_entity_id: Some(stripe_checkout_id), - maybe_stripe_customer_id, - action_was_taken: false, - should_ignore_retry: false, - }) -} diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/checkout_session/mod.rs b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/checkout_session/mod.rs deleted file mode 100644 index 524729c738..0000000000 --- a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/checkout_session/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod checkout_session_completed_handler; \ No newline at end of file diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/handle_webhook_event_enrichment.rs b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/handle_webhook_event_enrichment.rs index e4911ade09..eb3eaf7d00 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/handle_webhook_event_enrichment.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/handle_webhook_event_enrichment.rs @@ -1,7 +1,8 @@ use crate::endpoints::webhook::common::enriched_webhook_event::EnrichedWebhookEvent; use crate::endpoints::webhook::common::stripe_artcraft_webhook_error::StripeArtcraftWebhookError; use crate::endpoints::webhook::common::webhook_event_log_summary::WebhookEventLogSummary; -use crate::endpoints::webhook::webhook_event_enrichment::checkout_session::checkout_session_completed_handler::checkout_session_completed_handler; +use crate::endpoints::webhook::webhook_event_enrichment::customer::customer_created_handler::customer_created_handler; +use crate::endpoints::webhook::webhook_event_enrichment::customer::customer_updated_handler::customer_updated_handler; use crate::endpoints::webhook::webhook_event_enrichment::customer_subscription::customer_subscription_created_handler::customer_subscription_created_handler; use crate::endpoints::webhook::webhook_event_enrichment::customer_subscription::customer_subscription_deleted_handler::customer_subscription_deleted_handler; use crate::endpoints::webhook::webhook_event_enrichment::customer_subscription::customer_subscription_updated_handler::customer_subscription_updated_handler; @@ -16,20 +17,6 @@ use sqlx::pool::PoolConnection; use sqlx::{MySql, Transaction}; use stripe::Client; use stripe_webhook::{Event, EventObject}; -use crate::endpoints::webhook::webhook_event_enrichment::customer::customer_created_handler::customer_created_handler; -use crate::endpoints::webhook::webhook_event_enrichment::customer::customer_updated_handler::customer_updated_handler; -/* - You usually need these: - 1. Subscription events: to track contract state (active, canceled, past_due, trialing, etc.). - 2. Invoice events: to track actual money flow and handle edge cases like failed payments, dunning, or refunds. - - Minimal Set to Cover Payment Lifecycle - - invoice.paid → confirm credits or entitlements - - invoice.payment_failed → trigger dunning or downgrade access - - customer.subscription.deleted → stop service when canceled - - customer.subscription.updated → react to upgrades, downgrades, trial end - - (Optionally) checkout.session.completed → provision on signup -*/ pub async fn handle_webhook_event_enrichment( stripe_event_descriptor: &StripeEventDescriptor, @@ -64,6 +51,7 @@ pub async fn handle_webhook_event_enrichment( // We'll ignore any payment intents from subscription invoices and let other event handlers // fulfill subscription states. info!("Event {}, data: {:?}", stripe_event_descriptor, payment_intent); + return payment_intent_succeeded_extractor( &payment_intent, server_environment, @@ -71,6 +59,33 @@ pub async fn handle_webhook_event_enrichment( ).await; } + // =============== INVOICES =============== + + EventObject::InvoicePaid(invoice) => { + // Invoices are for subscriptions, not one-off charges and purchases. + // This is the *required* event that enables the subscription! + // These are fired on an interval - whatever the billing cadence is. + info!("Event: {}, data: {:?}", stripe_event_descriptor, invoice); + + return invoice_paid_handler( + &stripe_event_descriptor, + &invoice, + server_environment, + stripe_client + ).await; + } + + EventObject::InvoicePaymentFailed(_invoice) => { + // TODO: Halt service. + // When we detect invoice payment failures, we need to disable subscription services. + // info!("Event: {:?}, data: {:?}", stripe_event_descriptor, invoice); + } + + EventObject::InvoiceCreated(_invoice) => { + // 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 + } + // =============== CUSTOMER SUBSCRIPTIONS =============== EventObject::CustomerSubscriptionCreated(subscription) => { @@ -106,91 +121,6 @@ pub async fn handle_webhook_event_enrichment( ).await; } - // =============== INVOICES =============== - - EventObject::InvoiceCreated(_invoice) => { - // 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 - } - - EventObject::InvoicePaid(invoice) => { - // Invoices are for subscriptions, not one-off charges and purchases. - // This is the *required* event that enables the subscription! - // These are fired on an interval - whatever the billing cadence is. - info!("Event: {}, data: {:?}", stripe_event_descriptor, invoice); - return invoice_paid_handler( - &stripe_event_descriptor, - &invoice, - server_environment, - stripe_client - ).await; - } - - /*EventObject::InvoicePaymentFailed(invoice) => { - // TODO: Halt service. - // TODO: in wallet schema, hold a 'lifetime_total_credits_used' - no, that wastes space. `credits_used` then SUM() for reports - // When we detect invoice payment failures, we need to disable - // subscription services. - info!("Event: {:?}, data: {:?}", stripe_event_descriptor, invoice); - webhook_summary = invoice_payment_failed_handler(&invoice)?; - } - */ - - // =============== CUSTOMERS =============== - - // EventObject::CustomerCreated(customer) => { - // // We use this simply to create a (totally optional) User <-> Customer link. - // // We already get that link from subscriptions, but this will help us associate - // // one-off payments and payment cards with a user. - // info!("Event: {}, data: {:?}", stripe_event_descriptor, customer); - // return customer_created_handler(customer); - // } - - // EventObject::CustomerUpdated(customer) => { - // // We use this simply to create a (totally optional) User <-> Customer link. - // // We already get that link from subscriptions, but this will help us associate - // // one-off payments and payment cards with a user. - // info!("Event: {}, data: {:?}", stripe_event_descriptor, customer); - // return customer_updated_handler(customer); - // } - - // =============== CHECKOUT SESSIONS =============== - - // TODO: Provision the subscription here. - EventObject::CheckoutSessionCompleted(checkout_session) => { - info!("Event: {}, data: {:?}", webhook_payload.type_, checkout_session); - - // TODO: DO NOT USE TO PROVISION SERVICE - USE `invoice.paid`. - // - // Checkout session completion is ideal for provisioning the service after checkout, - // but will not be used for monthly subscription renewals, which are handled by - // invoice.paid events. - // - // - `payment_status = {paid, unpaid, no_payment_required}` will let us know - // if the funds are in our account. NOTE (2): this will not be `paid` for free - // trials, but `invoice.paid` events will fire for free trials. (2): Async - // methods like ACH will have the status `unpaid` until the payment - // clears. - // - // - `subscription` will contain the ID of the subscription in subscription mode. - // - // - `metadata` - user token, etc. - // - // After the subscription signup succeeds, the customer returns to your website at the success_url, - // which initiates a checkout.session.completed webhooks. When you receive a checkout.session.completed - // event, you can provision the subscription. Continue to provision each month (if billing monthly) as - // you receive invoice.paid events. If you receive an invoice.payment_failed event, notify your customer - // and send them to the customer portal to update their payment method. - // - // Still need to ask about this. - does stripe say we can provision service here? - // Do we know if this happens for upgrades/downgrades/renewals? - // What about one-off payments? - - /* TODO: Commented out for now - webhook_summary = checkout_session_completed_handler(checkout_session)?; - */ - } - // =============== Ignored =============== _ => { diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/ignore_known_unwanted_events.rs b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/ignore_known_unwanted_events.rs index ee954e4306..16d1a47d66 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/ignore_known_unwanted_events.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/ignore_known_unwanted_events.rs @@ -7,6 +7,18 @@ use stripe_webhook::{Event, EventObject}; pub fn ignore_known_unwanted_events(webhook_payload: &Event) -> Option { match webhook_payload.data.object { + EventObject::CheckoutSessionCompleted(_) => { + // We don't use this to provision service - we use `invoice.paid` instead. + // + // Checkout Session Completed `payment_status = {paid, unpaid, no_payment_required}` + // will let us know if the funds are in our account under SOME circumstances. + // + // NOTE however that: + // (1) this will not be `paid` for free trials, whereas `invoice.paid` events will fire for free trials. + // (2) async methods like ACH will have the status `unpaid` until the payment clears. + // + // Thus, we really don't want to use this event to provision service. + } EventObject::ChargeSucceeded(_) => { // charge.succeeded - building block event and also used for older legacy integrations. // Use `invoice.paid` as billing success signal for subscriptions. @@ -20,8 +32,8 @@ pub fn ignore_known_unwanted_events(webhook_payload: &Event) -> Option { // We don't need to know about Stripe customer object metadata changes, - // eg. stripe email change. Unfortunately, we also can't directly associate - // metadata with customers in a simple API call to create a checkout or portal + // eg. stripe email change. Unfortunately, we also can't directly associate + // metadata with customers in a simple API call to create a checkout or portal // session, so these objects are kind of useless. } EventObject::PaymentIntentCreated(_) => { diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/mod.rs b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/mod.rs index f45ecabd9a..af78af1397 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/mod.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/mod.rs @@ -1,7 +1,6 @@ mod ignore_known_unwanted_events; pub (crate) mod handle_webhook_event_enrichment; -pub(crate) mod checkout_session; pub(crate) mod customer_subscription; pub(crate) mod invoice; pub(crate) mod payment_intent;