big refactor of this to remove http during db transactions

This commit is contained in:
Brandon Thomas committed 2025-09-12 01:00:04 -04:00
1 parent 036dbeaeb4
commit da2e7ae4b6
19 files changed
+532 -26

No files matched your search

@@ -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";
@@ -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,
}
@@ -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,
}
@@ -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;
@@ -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)
@@ -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;
@@ -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;
@@ -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,
})
}
@@ -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,
}
}
@@ -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;
@@ -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))
}
@@ -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;