mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 09:28:11 +00:00
big refactor of this to remove http during db transactions
This commit is contained in:
1 parent
036dbeaeb4
commit
da2e7ae4b6
19 files changed
+532
-26
No files matched your search
+3
@@ -5,6 +5,9 @@ pub struct StripeArtcraftCreditsPackInfo {
|
||||
pub slug: ArtcraftCreditsPackSlug,
|
||||
pub product_id: &'static str,
|
||||
pub price_id: &'static str,
|
||||
|
||||
/// When a new credits pack purchase is made, this is how many credits
|
||||
/// are added for "quantity=1"
|
||||
pub purchase_credits_amount: u64,
|
||||
}
|
||||
|
||||
@@ -4,3 +4,6 @@ pub const STRIPE_ARTCRAFT_METADATA_USER_TOKEN : &str = "user_token";
|
||||
pub const STRIPE_ARTCRAFT_METADATA_USERNAME : &str = "username";
|
||||
|
||||
pub const STRIPE_ARTCRAFT_METADATA_EMAIL : &str = "email";
|
||||
|
||||
pub const STRIPE_ARTCRAFT_METADATA_WALLET_TOKEN : &str = "wallet_token";
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
use crate::endpoints::webhook::common::billing_action::BillingAction;
|
||||
use crate::endpoints::webhook::common::event_log_summary::WebhookEventLogSummary;
|
||||
|
||||
pub struct ArtcraftBillingEvent {
|
||||
/// Higher-level events that we care about handling.
|
||||
/// Not all webhook requests have them.
|
||||
pub action: BillingAction,
|
||||
|
||||
/// Details of what we'll write to the webhook event logs.
|
||||
/// In addition to important events, this includes some no-op events,
|
||||
/// various irrelevant events, errors, etc.
|
||||
pub webhook_event_log_summary: WebhookEventLogSummary,
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
use crate::configs::credits_packs::stripe_artcraft_credits_pack_info::StripeArtcraftCreditsPackInfo;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use tokens::tokens::wallets::WalletToken;
|
||||
|
||||
pub enum BillingAction {
|
||||
/// An ignorable event.
|
||||
IgnorableEvent{
|
||||
event_type: IgnoreableEventType,
|
||||
description: String
|
||||
},
|
||||
|
||||
/// A user purchased wallet credits.
|
||||
WalletCreditsPurchase(WalletCreditsPurchaseEvent),
|
||||
|
||||
// TODO:
|
||||
SubscriptionCreated,
|
||||
SubscriptionRenewalBillingFailed,
|
||||
SubscriptionRenewalBillingSucceeded,
|
||||
SubscriptionCanceled,
|
||||
}
|
||||
|
||||
pub struct WalletCreditsPurchaseEvent {
|
||||
pub owner_user_token: UserToken,
|
||||
|
||||
// We might have sent the wallet to stripe.
|
||||
// If not, we'll need to look it up or create one.
|
||||
pub maybe_wallet_token: Option<WalletToken>,
|
||||
|
||||
pub pack: StripeArtcraftCreditsPackInfo,
|
||||
|
||||
// NB: This is a multiplier on the pack's base value.
|
||||
pub quantity: u64,
|
||||
}
|
||||
|
||||
#[derive(Copy,Clone,Debug)]
|
||||
pub enum IgnoreableEventType {
|
||||
Other,
|
||||
PaymentIntentFailed,
|
||||
PaymentIntentForSubscription,
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct WebhookEventLogSummary {
|
||||
/// This is the stripe customer ID, if it was associated with the event.
|
||||
pub maybe_stripe_customer_id: Option<String>,
|
||||
|
||||
/// If we recorded our own internal user ID as Stripe metadata, this passes it upstream.
|
||||
pub maybe_user_token: Option<UserToken>,
|
||||
|
||||
/// 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>,
|
||||
|
||||
/// 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,3 @@
|
||||
pub mod event_log_summary;
|
||||
pub mod artcraft_billing_event;
|
||||
pub mod billing_action;
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod common;
|
||||
pub mod stripe_artcraft_webhook_handler;
|
||||
pub mod webhook_event_handlers;
|
||||
+27
-19
@@ -1,6 +1,7 @@
|
||||
use actix_web::web::{Bytes, Data, Json};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
|
||||
use crate::endpoints::webhook::common::artcraft_billing_event::ArtcraftBillingEvent;
|
||||
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;
|
||||
@@ -10,6 +11,8 @@ use crate::endpoints::webhook::webhook_event_handlers::invoice::invoice_payment_
|
||||
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::webhook_event_handlers::webhook_event_to_artcraft_event::webhook_event_to_artcraft_event;
|
||||
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;
|
||||
use chrono::NaiveDateTime;
|
||||
@@ -25,7 +28,6 @@ use std::sync::Arc;
|
||||
use stripe_webhook::{Event, EventObject, Webhook};
|
||||
|
||||
/*
|
||||
|
||||
For one-off payments (eg. credits packs), we see this typical sequence of events:
|
||||
|
||||
- charge.succeeded (EventId("evt_3S3gutEobp4xy4Tl15tGHk88")) -- IGNORE
|
||||
@@ -33,7 +35,6 @@ For one-off payments (eg. credits packs), we see this typical sequence of events
|
||||
- payment_intent.created (EventId("evt_3S3gutEobp4xy4Tl1mg80Zgu")) -- IGNORE
|
||||
- checkout.session.completed (EventId("evt_1S3guvEobp4xy4TlxY7TVlUs")) -- Can use, but `payment_intent.succeeded` is better
|
||||
- charge.updated (EventId("evt_3S3gutEobp4xy4Tl1RgfG4NL")) -- IGNORE
|
||||
|
||||
*/
|
||||
|
||||
|
||||
@@ -134,19 +135,26 @@ pub async fn stripe_artcraft_webhook_handler(
|
||||
}
|
||||
}
|
||||
|
||||
let artcraft_event = webhook_event_to_artcraft_event(
|
||||
&stripe_config.client,
|
||||
**server_environment,
|
||||
webhook_payload,
|
||||
&stripe_event_type,
|
||||
).await?;
|
||||
|
||||
let mut transaction = mysql_connection.begin().await?;
|
||||
|
||||
let result = do_with_transaction(
|
||||
let result = process_event(
|
||||
**server_environment,
|
||||
&stripe_config,
|
||||
webhook_payload,
|
||||
artcraft_event,
|
||||
stripe_event_id.clone(),
|
||||
stripe_event_created_at,
|
||||
stripe_event_type.clone(),
|
||||
stripe_is_production,
|
||||
&mut transaction
|
||||
).await;
|
||||
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
transaction.commit().await?;
|
||||
@@ -154,8 +162,10 @@ pub async fn stripe_artcraft_webhook_handler(
|
||||
Err(err) => {
|
||||
error!("Error handling Stripe webhook event `{}` ({}): {:?}",
|
||||
stripe_event_type,stripe_event_id, err);
|
||||
|
||||
transaction.rollback().await?;
|
||||
return Err(err);
|
||||
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,36 +174,34 @@ pub async fn stripe_artcraft_webhook_handler(
|
||||
}))
|
||||
}
|
||||
|
||||
async fn do_with_transaction(
|
||||
async fn process_event(
|
||||
server_environment: ServerEnvironment,
|
||||
stripe_config: &ArtcraftStripeConfigWithClient,
|
||||
webhook_payload: Event,
|
||||
artcraft_event: ArtcraftBillingEvent,
|
||||
stripe_event_id: String,
|
||||
stripe_event_created_at: NaiveDateTime,
|
||||
stripe_event_type: String,
|
||||
stripe_is_production: bool,
|
||||
transaction: &mut Transaction<'_, MySql>
|
||||
) -> Result<(), StripeArtcraftWebhookError> {
|
||||
|
||||
let webhook_summary = handle_webhook_payload(
|
||||
|
||||
// TODO: Maybe grab the primary key of the impacted entity?
|
||||
transactionally_fulfill_artcraft_billing_action(
|
||||
&artcraft_event.action,
|
||||
transaction,
|
||||
&stripe_config.client,
|
||||
server_environment,
|
||||
webhook_payload,
|
||||
&stripe_event_type,
|
||||
).await?;
|
||||
|
||||
// NB: These records are uniquely keyed by ID, so this only happens once.
|
||||
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,
|
||||
maybe_stripe_event_entity_id: artcraft_event.webhook_event_log_summary.maybe_event_entity_id,
|
||||
maybe_stripe_customer_id: artcraft_event.webhook_event_log_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,
|
||||
maybe_user_token: artcraft_event.webhook_event_log_summary.maybe_user_token.map(|t| t.to_string()),
|
||||
action_was_taken: artcraft_event.webhook_event_log_summary.action_was_taken,
|
||||
should_ignore_retry: artcraft_event.webhook_event_log_summary.should_ignore_retry,
|
||||
};
|
||||
|
||||
query.insert_transactional(transaction)
|
||||
|
||||
+4
-3
@@ -1,9 +1,10 @@
|
||||
pub mod stripe_artcraft_webhook_error;
|
||||
|
||||
mod ignore_known_unwanted_events;
|
||||
pub (crate) mod webhook_event_to_artcraft_event;
|
||||
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;
|
||||
pub(crate) mod payment_intent;
|
||||
pub(crate) mod stripe_artcraft_webhook_summary;
|
||||
pub mod handle_webhook_payload;
|
||||
mod ignore_known_unwanted_events;
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
pub(crate) mod payment_intent_succeeded_handler;
|
||||
pub(crate) mod payment_intent_succeeded_handler;
|
||||
pub(crate) mod payment_intent_succeeded_handler_2;
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
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::stripe_artcraft_subscription_info::StripeArtcraftSubscriptionInfo;
|
||||
use crate::endpoints::webhook::common::artcraft_billing_event::ArtcraftBillingEvent;
|
||||
use crate::endpoints::webhook::common::billing_action::{BillingAction, IgnoreableEventType, WalletCreditsPurchaseEvent};
|
||||
use crate::endpoints::webhook::common::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::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 crate::utils::metadata::get_metadata_user_token::get_metadata_user_token;
|
||||
use crate::utils::metadata::get_metadata_wallet_token::get_metadata_wallet_token;
|
||||
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;
|
||||
use tokens::tokens::wallets::WalletToken;
|
||||
|
||||
// Handle event type: 'payment_intent.succeeded'
|
||||
pub async fn payment_intent_succeeded_handler_2(
|
||||
payment_intent: &PaymentIntent,
|
||||
server_environment: ServerEnvironment,
|
||||
stripe_client: &Client,
|
||||
) -> Result<ArtcraftBillingEvent, 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 tokens
|
||||
let maybe_user_token = get_metadata_user_token(&payment_intent.metadata);
|
||||
let maybe_wallet_token = get_metadata_wallet_token(&payment_intent.metadata);
|
||||
|
||||
// TODO: Multiple ways to get this; better ways to get this
|
||||
let user_token = match &maybe_user_token {
|
||||
Some(token) => token.clone(),
|
||||
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 event_log_summary = WebhookEventLogSummary {
|
||||
maybe_stripe_customer_id,
|
||||
maybe_user_token,
|
||||
maybe_event_entity_id: Some(payment_intent_id.clone()),
|
||||
action_was_taken: false,
|
||||
should_ignore_retry: true,
|
||||
};
|
||||
|
||||
// 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 {
|
||||
event_log_summary.should_ignore_retry = true;
|
||||
|
||||
return Ok(ArtcraftBillingEvent {
|
||||
action: BillingAction::IgnorableEvent {
|
||||
description: "payment_intent not succeeded".to_string(),
|
||||
event_type: IgnoreableEventType::PaymentIntentFailed,
|
||||
},
|
||||
webhook_event_log_summary: event_log_summary,
|
||||
});
|
||||
}
|
||||
|
||||
info!("Payment intent succeeded. Looking up payment...");
|
||||
|
||||
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.
|
||||
event_log_summary.should_ignore_retry = true;
|
||||
|
||||
return Ok(ArtcraftBillingEvent {
|
||||
action: BillingAction::IgnorableEvent {
|
||||
event_type: IgnoreableEventType::PaymentIntentForSubscription,
|
||||
description: "payment_intent for subscription; will handle via invoice.paid".to_string(),
|
||||
},
|
||||
webhook_event_log_summary: event_log_summary,
|
||||
});
|
||||
}
|
||||
|
||||
let credits_pack = 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);
|
||||
event_log_summary.should_ignore_retry = true;
|
||||
|
||||
return Ok(ArtcraftBillingEvent {
|
||||
action: BillingAction::IgnorableEvent {
|
||||
event_type: IgnoreableEventType::PaymentIntentForSubscription,
|
||||
description: "payment_intent for subscription; will handle via invoice.paid".to_string(),
|
||||
},
|
||||
webhook_event_log_summary: event_log_summary,
|
||||
});
|
||||
}
|
||||
Some(StripeArtcraftGenericProductInfo::CreditsPack(credits_pack)) => credits_pack,
|
||||
};
|
||||
|
||||
Ok(ArtcraftBillingEvent {
|
||||
action: BillingAction::WalletCreditsPurchase(WalletCreditsPurchaseEvent {
|
||||
owner_user_token: user_token,
|
||||
maybe_wallet_token: None, // TODO: Use the checkout session to set this !
|
||||
pack: credits_pack.clone(),
|
||||
quantity: purchase.quantity,
|
||||
}),
|
||||
webhook_event_log_summary: event_log_summary,
|
||||
})
|
||||
}
|
||||
+1
-1
@@ -18,4 +18,4 @@ pub struct StripeArtcraftWebhookSummary {
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
use crate::endpoints::webhook::common::artcraft_billing_event::ArtcraftBillingEvent;
|
||||
use crate::endpoints::webhook::common::billing_action::BillingAction::IgnorableEvent;
|
||||
use crate::endpoints::webhook::common::billing_action::{BillingAction, IgnoreableEventType};
|
||||
use crate::endpoints::webhook::common::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 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 webhook_event_to_artcraft_event(
|
||||
stripe_client: &Client,
|
||||
server_environment: ServerEnvironment,
|
||||
webhook_payload: Event,
|
||||
stripe_event_type: &str,
|
||||
) -> Result<ArtcraftBillingEvent, StripeArtcraftWebhookError> {
|
||||
|
||||
// TODO: Cleanup -
|
||||
if let Some(_summary) = ignore_known_unwanted_events(&webhook_payload) {
|
||||
return Ok(ArtcraftBillingEvent {
|
||||
action: BillingAction::IgnorableEvent {
|
||||
event_type: IgnoreableEventType::Other,
|
||||
description: "TODO".to_string(),
|
||||
|
||||
},
|
||||
webhook_event_log_summary: WebhookEventLogSummary {
|
||||
maybe_user_token: None,
|
||||
maybe_event_entity_id: None,
|
||||
maybe_stripe_customer_id: None,
|
||||
action_was_taken: false,
|
||||
should_ignore_retry: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let mut unhandled_event_type = false;
|
||||
|
||||
let mut webhook_summary = WebhookEventLogSummary {
|
||||
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?
|
||||
/* TODO: Commented out for now
|
||||
webhook_summary = checkout_session_completed_handler(checkout_session)?;
|
||||
*/
|
||||
}
|
||||
|
||||
// =============== CUSTOMER SUBSCRIPTIONS ===============
|
||||
|
||||
/* TODO TEMP -
|
||||
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
|
||||
}
|
||||
|
||||
/* TODO: Commented out for now.
|
||||
|
||||
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);
|
||||
|
||||
// NB: This is responsible for enabling one-off payments (eg. credits packs).
|
||||
return payment_intent_succeeded_handler_2(
|
||||
&payment_intent,
|
||||
server_environment,
|
||||
stripe_client,
|
||||
).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(ArtcraftBillingEvent {
|
||||
action: IgnorableEvent {
|
||||
event_type: IgnoreableEventType::Other,
|
||||
description: "todo".to_string(),
|
||||
},
|
||||
webhook_event_log_summary: webhook_summary,
|
||||
})
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod subscriptions;
|
||||
pub mod credits_pack;
|
||||
pub (crate) mod subscriptions;
|
||||
pub (crate) mod credits_pack;
|
||||
pub (crate) mod transactionally_fulfill_artcraft_billing_event;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
use crate::endpoints::webhook::common::billing_action::BillingAction;
|
||||
use crate::fulfillment::credits_pack::complete_credits_pack_purchase::complete_credits_pack_purchase;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use log::{info, warn};
|
||||
use sqlx::Transaction;
|
||||
|
||||
pub async fn transactionally_fulfill_artcraft_billing_action(
|
||||
event: &BillingAction,
|
||||
transaction: &mut Transaction<'_, sqlx::MySql>,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
match event {
|
||||
BillingAction::IgnorableEvent { .. } => {
|
||||
warn!("Received ignorable billing action; nothing to fulfill.");
|
||||
return Ok(())
|
||||
}
|
||||
BillingAction::WalletCreditsPurchase(purchase) => {
|
||||
info!("Completing credits pack purchase for user: {} ... ", purchase.owner_user_token.as_str());
|
||||
complete_credits_pack_purchase(
|
||||
&purchase.owner_user_token,
|
||||
&purchase.pack,
|
||||
purchase.quantity,
|
||||
transaction,
|
||||
).await?;
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!("Unhandled billing action in fulfillment"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
use crate::configs::stripe_artcraft_metadata_keys::STRIPE_ARTCRAFT_METADATA_USER_TOKEN;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub (crate) fn get_metadata_user_token(
|
||||
metadata: &std::collections::HashMap<String, String>,
|
||||
) -> Option<UserToken> {
|
||||
metadata.get(STRIPE_ARTCRAFT_METADATA_USER_TOKEN)
|
||||
.map(|t| UserToken::new_from_str(&t))
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
use crate::configs::stripe_artcraft_metadata_keys::STRIPE_ARTCRAFT_METADATA_WALLET_TOKEN;
|
||||
use tokens::tokens::wallets::WalletToken;
|
||||
|
||||
pub (crate) fn get_metadata_wallet_token(
|
||||
metadata: &std::collections::HashMap<String, String>,
|
||||
) -> Option<WalletToken> {
|
||||
metadata.get(STRIPE_ARTCRAFT_METADATA_WALLET_TOKEN)
|
||||
.map(|t| WalletToken::new_from_str(&t))
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub (crate) mod get_metadata_user_token;
|
||||
pub (crate) mod get_metadata_wallet_token;
|
||||
@@ -1,3 +1,4 @@
|
||||
pub (crate) mod metadata;
|
||||
pub mod artcraft_stripe_config;
|
||||
pub mod common_web_error;
|
||||
pub mod enum_conversion;
|
||||
|
||||
Reference in new issue
Block a user