This commit is contained in:
Brandon Thomas committed 2025-09-12 01:00:04 -04:00
1 parent b5bc532b80
commit d10cb54c62
24 files changed
+40 -374

No files matched your search

@@ -1,3 +1,4 @@
pub mod common;
pub mod stripe_artcraft_webhook_handler;
pub mod webhook_event_handlers;
pub mod stripe_artcraft_webhook_error;
pub (crate) mod webhook_event_enrichment;
@@ -2,16 +2,8 @@ use actix_web::web::{Bytes, Data, Json};
use actix_web::{web, HttpRequest, HttpResponse};
use crate::endpoints::webhook::common::enriched_webhook_event::EnrichedWebhookEvent;
use crate::endpoints::webhook::webhook_event_handlers::checkout_session::checkout_session_completed_handler::checkout_session_completed_handler;
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::customer_subscription_created_handler::customer_subscription_created_handler;
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::customer_subscription_updated_handler::customer_subscription_updated_handler;
use crate::endpoints::webhook::webhook_event_handlers::handle_webhook_event_enrichment::handle_webhook_event_enrichment;
use crate::endpoints::webhook::webhook_event_handlers::handle_webhook_payload::handle_webhook_payload;
use crate::endpoints::webhook::webhook_event_handlers::invoice::invoice_paid_handler::invoice_paid_handler;
use crate::endpoints::webhook::webhook_event_handlers::invoice::invoice_payment_failed::invoice_payment_failed_handler;
use crate::endpoints::webhook::webhook_event_handlers::payment_intent::payment_intent_succeeded_handler::payment_intent_succeeded_handler;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::endpoints::webhook::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_enrichment::handle_webhook_event_enrichment::handle_webhook_event_enrichment;
use crate::fulfillment::transactionally_fulfill_artcraft_billing_event::transactionally_fulfill_artcraft_billing_action;
use crate::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient;
use crate::utils::verify_stripe_webhook_ip_address::verify_stripe_webhook_ip_address;
@@ -26,7 +18,6 @@ use sqlx::pool::PoolConnection;
use sqlx::{Acquire, MySql, MySqlConnection, MySqlPool, Transaction};
use std::sync::Arc;
use stripe_webhook::{Event, EventObject, Webhook};
/*
For one-off payments (eg. credits packs), we see this typical sequence of events:
@@ -1,6 +1,6 @@
use crate::configs::stripe_artcraft_metadata_keys::STRIPE_ARTCRAFT_METADATA_USER_TOKEN;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::endpoints::webhook::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_enrichment::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::utils::expand_ids::expand_customer_id::expand_customer_id;
use stripe_checkout::CheckoutSession;
@@ -3,7 +3,7 @@ use std::ops::{Add, Sub};
use chrono::{Duration, NaiveDateTime};
use once_cell::sync::Lazy;
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::subscription_event_extractor::SubscriptionSummary;
use crate::endpoints::webhook::webhook_event_enrichment::customer_subscription::subscription_event_extractor::SubscriptionSummary;
use reusable_types::stripe::stripe_subscription_status::StripeSubscriptionStatus;
// Push back the expiration date.
@@ -1,9 +1,9 @@
use crate::configs::get_artcraft_product_by_stripe_id_and_env::get_artcraft_product_by_stripe_id_and_env;
use crate::configs::stripe_artcraft_generic_product_info::StripeArtcraftGenericProductInfo;
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::calculate_subscription_end_date::calculate_subscription_end_date;
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::subscription_event_extractor::subscription_summary_extractor;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::endpoints::webhook::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_enrichment::customer_subscription::calculate_subscription_end_date::calculate_subscription_end_date;
use crate::endpoints::webhook::webhook_event_enrichment::customer_subscription::subscription_event_extractor::subscription_summary_extractor;
use crate::endpoints::webhook::webhook_event_enrichment::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use enums::common::subscription_namespace::SubscriptionNamespace;
use log::error;
use mysql_queries::queries::users::user::update::update_user_record_with_new_stripe_customer_id::{update_user_record_with_new_stripe_customer_id, update_user_record_with_new_stripe_customer_id_with_connection};
@@ -1,9 +1,9 @@
use crate::configs::get_artcraft_product_by_stripe_id_and_env::get_artcraft_product_by_stripe_id_and_env;
use crate::configs::stripe_artcraft_generic_product_info::StripeArtcraftGenericProductInfo;
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::calculate_subscription_end_date::calculate_subscription_end_date;
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::subscription_event_extractor::subscription_summary_extractor;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::endpoints::webhook::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_enrichment::customer_subscription::calculate_subscription_end_date::calculate_subscription_end_date;
use crate::endpoints::webhook::webhook_event_enrichment::customer_subscription::subscription_event_extractor::subscription_summary_extractor;
use crate::endpoints::webhook::webhook_event_enrichment::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use enums::common::subscription_namespace::SubscriptionNamespace;
use log::error;
use mysql_queries::queries::users::user_subscriptions::get_user_subscription_by_stripe_subscription_id::{get_user_subscription_by_stripe_subscription_id, get_user_subscription_by_stripe_subscription_id_with_connection};
@@ -1,10 +1,10 @@
use crate::configs::get_artcraft_product_by_stripe_id_and_env::get_artcraft_product_by_stripe_id_and_env;
use crate::configs::stripe_artcraft_generic_product_info::StripeArtcraftGenericProductInfo;
use crate::configs::subscriptions::get_artcraft_subscription_by_slug_and_env::get_artcraft_subscription_by_slug_and_env;
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::calculate_subscription_end_date::calculate_subscription_end_date;
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::subscription_event_extractor::subscription_summary_extractor;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::endpoints::webhook::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_enrichment::customer_subscription::calculate_subscription_end_date::calculate_subscription_end_date;
use crate::endpoints::webhook::webhook_event_enrichment::customer_subscription::subscription_event_extractor::subscription_summary_extractor;
use crate::endpoints::webhook::webhook_event_enrichment::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use enums::common::subscription_namespace::SubscriptionNamespace;
use log::{error, info};
use mysql_queries::queries::users::user::update::update_user_record_with_new_stripe_customer_id::{update_user_record_with_new_stripe_customer_id, update_user_record_with_new_stripe_customer_id_with_connection};
@@ -106,7 +106,7 @@ pub fn subscription_summary_extractor(subscription: &Subscription) -> AnyhowResu
#[cfg(test)]
mod tests {
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::subscription_event_extractor::subscription_summary_extractor;
use crate::endpoints::webhook::webhook_event_enrichment::customer_subscription::subscription_event_extractor::subscription_summary_extractor;
use reusable_types::stripe::stripe_recurring_interval::StripeRecurringInterval;
use reusable_types::stripe::stripe_subscription_status::StripeSubscriptionStatus;
use stripe_shared::Subscription;
@@ -2,17 +2,16 @@ use crate::endpoints::webhook::common::artcraft_billing_action::BillingAction::I
use crate::endpoints::webhook::common::artcraft_billing_action::{BillingAction, IgnoreableEventType};
use crate::endpoints::webhook::common::enriched_webhook_event::EnrichedWebhookEvent;
use crate::endpoints::webhook::common::webhook_event_log_summary::WebhookEventLogSummary;
use crate::endpoints::webhook::webhook_event_handlers::checkout_session::checkout_session_completed_handler::checkout_session_completed_handler;
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::customer_subscription_created_handler::customer_subscription_created_handler;
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::customer_subscription_deleted_handler::customer_subscription_deleted_handler;
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::customer_subscription_updated_handler::customer_subscription_updated_handler;
use crate::endpoints::webhook::webhook_event_handlers::ignore_known_unwanted_events::ignore_known_unwanted_events;
use crate::endpoints::webhook::webhook_event_handlers::invoice::invoice_paid_handler::invoice_paid_handler;
use crate::endpoints::webhook::webhook_event_handlers::invoice::invoice_payment_failed::invoice_payment_failed_handler;
use crate::endpoints::webhook::webhook_event_handlers::payment_intent::payment_intent_succeeded_handler::payment_intent_succeeded_handler;
use crate::endpoints::webhook::webhook_event_handlers::payment_intent::payment_intent_succeeded_handler_2::payment_intent_succeeded_handler_2;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::endpoints::webhook::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
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_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;
use crate::endpoints::webhook::webhook_event_enrichment::ignore_known_unwanted_events::ignore_known_unwanted_events;
use crate::endpoints::webhook::webhook_event_enrichment::invoice::invoice_paid_handler::invoice_paid_handler;
use crate::endpoints::webhook::webhook_event_enrichment::invoice::invoice_payment_failed::invoice_payment_failed_handler;
use crate::endpoints::webhook::webhook_event_enrichment::payment_intent::payment_intent_succeeded_extractor::payment_intent_succeeded_extractor;
use crate::endpoints::webhook::webhook_event_enrichment::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use log::{info, warn};
use reusable_types::server_environment::ServerEnvironment;
use sqlx::pool::PoolConnection;
@@ -189,7 +188,7 @@ pub async fn handle_webhook_event_enrichment(
info!("Event: {}, data: {:?}", webhook_payload.type_, payment_intent);
// NB: This is responsible for enabling one-off payments (eg. credits packs).
return payment_intent_succeeded_handler_2(
return payment_intent_succeeded_extractor(
&payment_intent,
server_environment,
stripe_client,
@@ -1,4 +1,4 @@
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::endpoints::webhook::webhook_event_enrichment::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use log::info;
use stripe_webhook::{Event, EventObject};
@@ -1,5 +1,5 @@
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::endpoints::webhook::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_enrichment::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::utils::expand_ids::expand_customer_id::expand_customer_id;
use crate::utils::expand_ids::expand_subscription_id::expand_subscription_id;
use stripe_shared::{Invoice, InvoiceStatus};
@@ -1,6 +1,6 @@
use crate::configs::stripe_artcraft_metadata_keys::STRIPE_ARTCRAFT_METADATA_USER_TOKEN;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::endpoints::webhook::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_enrichment::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::utils::expand_ids::expand_customer_id::expand_customer_id;
use stripe_shared::Invoice;
@@ -1,8 +1,6 @@
mod ignore_known_unwanted_events;
pub (crate) mod handle_webhook_event_enrichment;
pub mod handle_webhook_payload;
pub mod stripe_artcraft_webhook_error;
pub(crate) mod checkout_session;
pub(crate) mod customer_subscription;
pub(crate) mod invoice;
@@ -0,0 +1 @@
pub(crate) mod payment_intent_succeeded_extractor;
@@ -4,8 +4,8 @@ use crate::configs::subscriptions::stripe_artcraft_subscription_info::StripeArtc
use crate::endpoints::webhook::common::artcraft_billing_action::{BillingAction, IgnoreableEventType, WalletCreditsPurchaseEvent};
use crate::endpoints::webhook::common::enriched_webhook_event::EnrichedWebhookEvent;
use crate::endpoints::webhook::common::webhook_event_log_summary::WebhookEventLogSummary;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::endpoints::webhook::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_enrichment::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::fulfillment::credits_pack::complete_credits_pack_purchase::complete_credits_pack_purchase;
use crate::requests::lookup_purchase_from_payment_intent_success::lookup_purchase_from_payment_intent_success;
use crate::utils::expand_ids::expand_customer_id::expand_customer_id;
@@ -21,7 +21,7 @@ use tokens::tokens::users::UserToken;
use tokens::tokens::wallets::WalletToken;
// Handle event type: 'payment_intent.succeeded'
pub async fn payment_intent_succeeded_handler_2(
pub async fn payment_intent_succeeded_extractor(
payment_intent: &PaymentIntent,
server_environment: ServerEnvironment,
stripe_client: &Client,
@@ -1,194 +0,0 @@
use crate::endpoints::webhook::webhook_event_handlers::checkout_session::checkout_session_completed_handler::checkout_session_completed_handler;
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::customer_subscription_created_handler::customer_subscription_created_handler;
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::customer_subscription_deleted_handler::customer_subscription_deleted_handler;
use crate::endpoints::webhook::webhook_event_handlers::customer_subscription::customer_subscription_updated_handler::customer_subscription_updated_handler;
use crate::endpoints::webhook::webhook_event_handlers::ignore_known_unwanted_events::ignore_known_unwanted_events;
use crate::endpoints::webhook::webhook_event_handlers::invoice::invoice_paid_handler::invoice_paid_handler;
use crate::endpoints::webhook::webhook_event_handlers::invoice::invoice_payment_failed::invoice_payment_failed_handler;
use crate::endpoints::webhook::webhook_event_handlers::payment_intent::payment_intent_succeeded_handler::payment_intent_succeeded_handler;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use log::{info, warn};
use reusable_types::server_environment::ServerEnvironment;
use sqlx::pool::PoolConnection;
use sqlx::{MySql, Transaction};
use stripe::Client;
use stripe_webhook::{Event, EventObject};
pub async fn handle_webhook_payload(
transaction: &mut Transaction<'_, MySql>,
stripe_client: &Client,
server_environment: ServerEnvironment,
webhook_payload: Event,
stripe_event_type: &str,
) -> Result<StripeArtcraftWebhookSummary, StripeArtcraftWebhookError> {
if let Some(summary) = ignore_known_unwanted_events(&webhook_payload) {
return Ok(summary);
}
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,
};
/*
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
*/
match webhook_payload.data.object {
// =============== 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?
webhook_summary = checkout_session_completed_handler(checkout_session)?;
}
// =============== CUSTOMER SUBSCRIPTIONS ===============
EventObject::CustomerSubscriptionCreated(subscription) => {
info!("Event: {}, data: {:?}", webhook_payload.type_, subscription);
// DO NOT USE TO PROVISION SERVICE.
//
// This can be used to upsert the subscription record, but may be `incomplete` and unpaid.
// This is good for subscription state, renewal dates, etc.
//
// - `id` of the subscription object
// - `customer` id of the stripe customer object
// - `status` has a rich list of subscription states (active, etc.)
// - `items.data[0].price`
// - `items.data[0].plan`
// - billing_cycle_anchor
// - `metadata` - user token, etc. that were attached to the subscription
//
// Sent when the subscription is created. The subscription status may be incomplete if customer
// authentication is required to complete the payment or if you set payment_behavior to
// default_incomplete. For more details, read about subscription payment behavior.
//
// TODO: add `maybe_billing_cycle_anchor`
//
webhook_summary = customer_subscription_created_handler(
&subscription,
server_environment,
transaction,
).await?;
}
EventObject::CustomerSubscriptionUpdated(subscription) => {
info!("Event: {}, data: {:?}", webhook_payload.type_, subscription);
webhook_summary = customer_subscription_updated_handler(
&subscription,
server_environment,
transaction,
).await?;
}
EventObject::CustomerSubscriptionDeleted(subscription) => {
info!("Event: {}, data: {:?}", webhook_payload.type_, subscription);
webhook_summary = customer_subscription_deleted_handler(
&subscription,
server_environment,
transaction,
).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) => {
// TODO: How do we handle multiple subscriptions?
// TODO: How do we handle inactive subscriptions vs. payment failed ones?
// 'invoice.paid'
// - 'customer' - the customer id
// - 'status = paid'
// - (optional) 'subscription_details.subscription' - the stripe subscription id (if subscription)
// - (optional) 'subscription_details.metadata' - Artcraft user token, etc.
info!("Event: {}, data: {:?}", webhook_payload.type_, invoice);
webhook_summary = invoice_paid_handler(&invoice)?;
}
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: {:?}", webhook_payload.type_, invoice);
webhook_summary = invoice_payment_failed_handler(&invoice)?;
}
// =============== PAYMENT INTENTS ===============
EventObject::PaymentIntentSucceeded(payment_intent) => {
info!("Event: {}, data: {:?}", webhook_payload.type_, payment_intent);
webhook_summary = payment_intent_succeeded_handler(
&payment_intent,
server_environment,
stripe_client,
transaction,
).await?;
}
// =============== Ignored ===============
_ => {
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)
}
@@ -1,2 +0,0 @@
pub(crate) mod payment_intent_succeeded_handler;
pub(crate) mod payment_intent_succeeded_handler_2;
@@ -1,127 +0,0 @@
use crate::configs::get_artcraft_product_by_stripe_id_and_env::get_artcraft_product_by_stripe_id_and_env;
use crate::configs::stripe_artcraft_generic_product_info::StripeArtcraftGenericProductInfo;
use crate::configs::stripe_artcraft_metadata_keys::STRIPE_ARTCRAFT_METADATA_USER_TOKEN;
use crate::configs::subscriptions::stripe_artcraft_subscription_info::StripeArtcraftSubscriptionInfo;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
use crate::fulfillment::credits_pack::complete_credits_pack_purchase::complete_credits_pack_purchase;
use crate::requests::lookup_purchase_from_payment_intent_success::lookup_purchase_from_payment_intent_success;
use crate::utils::expand_ids::expand_customer_id::expand_customer_id;
use log::{error, info, warn};
use reusable_types::server_environment::ServerEnvironment;
use sqlx::{MySql, Transaction};
use stripe::Client;
use stripe_checkout::checkout_session::ListCheckoutSession;
use stripe_shared::PaymentIntent;
use tokens::tokens::users::UserToken;
// Handle event type: 'payment_intent.succeeded'
pub async fn payment_intent_succeeded_handler(
payment_intent: &PaymentIntent,
server_environment: ServerEnvironment,
stripe_client: &Client,
transaction: &mut Transaction<'_, MySql>,
) -> Result<StripeArtcraftWebhookSummary, StripeArtcraftWebhookError> {
let payment_intent_id = payment_intent.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 = payment_intent.customer
.as_ref()
.map(|c| expand_customer_id(c));
// NB: Our internal user token.
let maybe_user_token = payment_intent.metadata.get(STRIPE_ARTCRAFT_METADATA_USER_TOKEN)
.map(|t| t.to_string());
// TODO: Multiple ways to get this; better ways to get this
let user_token = match &maybe_user_token {
Some(token) => UserToken::new_from_str(token),
None => {
warn!("No user token found in payment intent metadata. Cannot proceed.");
return Err(StripeArtcraftWebhookError::BadRequest("no user token in payment intent".to_string()));
}
};
let mut should_ignore_retry = false;
let mut action_was_taken = false;
// Payment intent events are bare. They have absolutely no context about what they were for.
// No products, no checkout sessions, etc. We'll have to look them up on success.
let payment_succeeded = match payment_intent.status {
stripe_shared::PaymentIntentStatus::Succeeded => true,
_ => false,
};
if payment_succeeded {
info!("Payment intent succeeded. Looking up payment...");
// TODO TODO TODO
// TODO TODO TODO - DO NOT DO A TRANSACTION WITH HTTP REQUESTS!!! Move this logic out!
// TODO TODO TODO
// TODO: Summarize the webhook into an internal event, then process that in an atomic transaction.
warn!("DO NOT KEEP THIS REQUEST WITHIN A TRANSACTION - MOVE IT OUT!!!");
warn!("DO NOT KEEP THIS REQUEST WITHIN A TRANSACTION - MOVE IT OUT!!!");
warn!("DO NOT KEEP THIS REQUEST WITHIN A TRANSACTION - MOVE IT OUT!!!");
warn!("DO NOT KEEP THIS REQUEST WITHIN A TRANSACTION - MOVE IT OUT!!!");
warn!("DO NOT KEEP THIS REQUEST WITHIN A TRANSACTION - MOVE IT OUT!!!");
warn!("DO NOT KEEP THIS REQUEST WITHIN A TRANSACTION - MOVE IT OUT!!!");
warn!("DO NOT KEEP THIS REQUEST WITHIN A TRANSACTION - MOVE IT OUT!!!");
warn!("DO NOT KEEP THIS REQUEST WITHIN A TRANSACTION - MOVE IT OUT!!!");
warn!("DO NOT KEEP THIS REQUEST WITHIN A TRANSACTION - MOVE IT OUT!!!");
warn!("DO NOT KEEP THIS REQUEST WITHIN A TRANSACTION - MOVE IT OUT!!!");
warn!("DO NOT KEEP THIS REQUEST WITHIN A TRANSACTION - MOVE IT OUT!!!");
warn!("DO NOT KEEP THIS REQUEST WITHIN A TRANSACTION - MOVE IT OUT!!!");
warn!("DO NOT KEEP THIS REQUEST WITHIN A TRANSACTION - MOVE IT OUT!!!");
warn!("DO NOT KEEP THIS REQUEST WITHIN A TRANSACTION - MOVE IT OUT!!!");
let purchase = lookup_purchase_from_payment_intent_success(&payment_intent_id, stripe_client)
.await
.map_err(|err| {
error!("Error looking up purchase from payment intent {}: {:?}", &payment_intent_id, err);
StripeArtcraftWebhookError::ServerError("error looking up purchase".to_string())
})?;
if purchase.has_invoice {
// Subscription purchase. Let `invoice.paid` event handle this instead.
should_ignore_retry = true;
action_was_taken = false;
} else {
match get_artcraft_product_by_stripe_id_and_env(&purchase.product_id, server_environment) {
None => {
error!("Could not find product for ID: {}", &purchase.product_id);
return Err(StripeArtcraftWebhookError::ServerError("unknown product".to_string()));
}
Some(StripeArtcraftGenericProductInfo::Subscription(subscription)) => {
info!("Do not handle subscriptions as one-off payments: {}", purchase.product_id);
should_ignore_retry = true;
action_was_taken = false;
}
Some(StripeArtcraftGenericProductInfo::CreditsPack(credits_pack)) => {
info!("Fulfilling one-time payment...");
complete_credits_pack_purchase(
&user_token,
credits_pack,
purchase.quantity,
transaction,
).await?;
should_ignore_retry = true;
action_was_taken = true;
}
}
}
}
Ok(StripeArtcraftWebhookSummary {
maybe_user_token,
maybe_event_entity_id: Some(payment_intent_id),
maybe_stripe_customer_id,
action_was_taken,
should_ignore_retry,
})
}
@@ -1,4 +1,3 @@
use crate::endpoints::webhook::webhook_event_handlers::stripe_artcraft_webhook_error::StripeArtcraftWebhookError;
use crate::utils::expand_ids::expand_product_id::expand_product_id;
use log::{error, warn};
use stripe::{Client, RequestStrategy, StripeRequest};