mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 17:58:55 +00:00
cleanup
This commit is contained in:
1 parent
780ee74b95
commit
d4ec659568
6 files changed
+48
-165
No files matched your search
+4
-27
@@ -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);
|
||||
}
|
||||
|
||||
-34
@@ -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<WebhookEventLogSummary, StripeArtcraftWebhookError> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
pub(crate) mod checkout_session_completed_handler;
|
||||
+30
-100
@@ -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 ===============
|
||||
|
||||
_ => {
|
||||
|
||||
+14
-2
@@ -7,6 +7,18 @@ use stripe_webhook::{Event, EventObject};
|
||||
pub fn ignore_known_unwanted_events(webhook_payload: &Event) -> Option<WebhookEventLogSummary> {
|
||||
|
||||
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<WebhookEv
|
||||
EventObject::CustomerDeleted(_) |
|
||||
EventObject::CustomerUpdated(_) => {
|
||||
// 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(_) => {
|
||||
|
||||
-1
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user