refactor subscriptions handlers

This commit is contained in:
Brandon Thomas committed 2025-09-12 01:00:04 -04:00
1 parent 72631c94be
commit 8248e53ded
11 files changed
+191 -369

No files matched your search

@@ -43,6 +43,11 @@ CREATE TABLE stripe_webhook_event_logs (
-- Some event types may attach a user token
maybe_user_token VARCHAR(32) DEFAULT NULL,
-- TODO: Add an internal optional entity token this resolves to.
-- eg. `wallets.token` or `user_subscriptions.token`.
-- NB: This field does not exist yet !
maybe_internal_entity_token VARCHAR(32) DEFAULT NULL,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -59,6 +59,8 @@ CREATE TABLE user_subscriptions (
maybe_stripe_is_production BOOLEAN DEFAULT NULL,
-- TODO: add `maybe_billing_cycle_anchor`
-- ========== VECTOR CLOCK ==========
-- Incremented with every update.
@@ -9,15 +9,13 @@ use tokens::tokens::wallets::WalletToken;
pub enum ArtcraftBillingAction {
/// An ignorable event.
IgnorableEvent,
/// A user purchased wallet credits.
WalletCreditsPurchase(WalletCreditsPurchaseEvent),
SubscriptionCreated(UpsertableSubscriptionDetails),
SubscriptionUpdated(UpsertableSubscriptionDetails),
// TODO
SubscriptionDeleted,
SubscriptionDeleted(UpsertableSubscriptionDetails),
// TODO:
SubscriptionRenewalBillingFailed,
@@ -27,32 +25,32 @@ pub enum ArtcraftBillingAction {
pub struct WalletCreditsPurchaseEvent {
pub owner_user_token: UserToken,
// We might have sent the wallet to stripe.
// 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,
}
pub struct UpsertableSubscriptionDetails {
/// Stripe's subscription_id is a unique foreign key in the
/// Stripe's subscription_id is a unique foreign key in the
/// `users_subscriptions` table!
pub stripe_subscription_id: String,
// Other stripe foreign keys ...
// Other stripe foreign keys ...
pub stripe_customer_id: String,
pub stripe_product_id: String,
pub stripe_price_id: String,
/// Artcraft subscription product
pub subscription: StripeArtcraftSubscriptionInfo,
/// Artcraft user
pub owner_user_token: UserToken,
@@ -60,7 +58,7 @@ pub struct UpsertableSubscriptionDetails {
pub stripe_subscription_status: StripeSubscriptionStatus,
pub stripe_recurring_interval : StripeRecurringInterval,
pub stripe_is_production: bool,
/// When the subscription was "created" in Stripe (including any backdating)
@@ -1,13 +1,50 @@
use crate::billing_action_fulfillment::artcraft_billing_action::UpsertableSubscriptionDetails;
use enums::common::subscription_namespace::SubscriptionNamespace;
use log::info;
use mysql_queries::queries::users::user_subscriptions::get_user_subscription_by_stripe_subscription_id_transactional::get_user_subscription_by_stripe_subscription_id_transactional;
use mysql_queries::queries::users::user_subscriptions::upsert_user_subscription_by_stripe_id::UpsertUserSubscription;
use reusable_types::stripe::stripe_subscription_status::StripeSubscriptionStatus;
#[derive(Copy, Clone)]
pub enum CrudType {
Create,
Update,
Delete,
}
/// Record the credits pack purchase
pub async fn upsert_subscription_details(
details: &UpsertableSubscriptionDetails,
crud_type: CrudType,
transaction: &mut sqlx::Transaction<'_, sqlx::MySql>,
) -> anyhow::Result<()> {
let maybe_existing_subscription = get_user_subscription_by_stripe_subscription_id_transactional(
&details.stripe_subscription_id,
transaction,
).await?;
// NB: It's possible to receive events out of order.
// We can get an 'update' after a 'create', etc.
if let Some(existing_sub) = maybe_existing_subscription {
match crud_type {
CrudType::Create => {
// We won't want to play a `create` event on top of something already existing.
info!("Subscription record already exists. Skipping insert.");
return Ok(()); // Turn this into a no-op. This is stale info.
}
CrudType::Update | CrudType::Delete => {
match existing_sub.maybe_stripe_subscription_status {
Some(StripeSubscriptionStatus::Canceled) => {
// NB: The stored subscription already had a terminal status and the subscription cannot be updated any further.
info!("Existing subscription record already in terminal state. Skipping updates.");
return Ok(()); // Turn this into a no-op. This is stale info.
}
_ => {} // Fall-through
}
}
}
}
let upsert = UpsertUserSubscription {
// This is the primary key
@@ -4,7 +4,7 @@ use anyhow::anyhow;
use errors::AnyhowResult;
use log::{info, warn};
use sqlx::Transaction;
use crate::billing_action_fulfillment::subscriptions::upsert_subscription_details::upsert_subscription_details;
use crate::billing_action_fulfillment::subscriptions::upsert_subscription_details::{upsert_subscription_details, CrudType};
pub async fn transactionally_fulfill_artcraft_billing_action(
event: &ArtcraftBillingAction,
@@ -26,8 +26,13 @@ pub async fn transactionally_fulfill_artcraft_billing_action(
).await?;
}
ArtcraftBillingAction::SubscriptionCreated(subscription_details) => {
upsert_subscription_details(subscription_details, transaction).await?;
upsert_subscription_details(subscription_details, CrudType::Create, transaction).await?;
}
ArtcraftBillingAction::SubscriptionUpdated(subscription_details) => {
upsert_subscription_details(subscription_details, CrudType::Update, transaction).await?;
}
ArtcraftBillingAction::SubscriptionDeleted(subscription_details) => {
upsert_subscription_details(subscription_details, CrudType::Delete, transaction).await?;
}
_ => {
return Err(anyhow!("Unhandled billing action in fulfillment"));
@@ -1,22 +1,8 @@
use crate::billing_action_fulfillment::artcraft_billing_action::{ArtcraftBillingAction, UpsertableSubscriptionDetails, WalletCreditsPurchaseEvent};
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::billing_action_fulfillment::artcraft_billing_action::ArtcraftBillingAction;
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::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::utils::metadata::get_metadata_user_token::get_metadata_user_token;
use crate::utils::metadata::get_metadata_wallet_token::get_metadata_wallet_token;
use enums::common::subscription_namespace::SubscriptionNamespace;
use log::{error, warn};
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};
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};
use mysql_queries::queries::users::user_subscriptions::get_user_subscription_by_stripe_subscription_id_transactional::get_user_subscription_by_stripe_subscription_id_transactional;
use mysql_queries::queries::users::user_subscriptions::upsert_user_subscription_by_stripe_id::UpsertUserSubscription;
use crate::endpoints::webhook::webhook_event_enrichment::customer_subscription::extract_subscription_details::extract_subscription_details;
use reusable_types::server_environment::ServerEnvironment;
use sqlx::pool::PoolConnection;
use sqlx::{MySql, MySqlConnection, MySqlPool, Transaction};
use stripe_shared::Subscription;
/// Handle event type: 'customer.subscription.created'
@@ -26,101 +12,12 @@ use stripe_shared::Subscription;
pub async fn customer_subscription_created_handler(
subscription: &Subscription,
server_environment: ServerEnvironment,
transaction: &mut Transaction<'_, MySql>,
) -> Result<EnrichedWebhookEvent, StripeArtcraftWebhookError> {
let summary = subscription_summary_extractor(subscription)
.map_err(|err| {
let reason = format!("Error extracting subscription from 'customer.subscription.created' payload: {:?}", err);
error!("{}", reason);
StripeArtcraftWebhookError::ServerError(reason) // NB: This was probably *our* fault.
})?;
let mut event_log_summary = WebhookEventLogSummary {
maybe_user_token: summary.user_token.clone(),
maybe_event_entity_id: Some(summary.stripe_subscription_id.clone()),
maybe_stripe_customer_id: Some(summary.stripe_customer_id.clone()),
action_was_taken: false,
should_ignore_retry: false,
};
let maybe_existing_subscription = get_user_subscription_by_stripe_subscription_id_transactional(
&summary.stripe_subscription_id, transaction)
.await
.map_err(|err| {
let reason = format!("Mysql error: {:?}", err);
error!("{}", reason);
StripeArtcraftWebhookError::ServerError(reason)
})?;
// NB: It's possible to receive events out of order.
// We won't want to play a `create` event on top.
if maybe_existing_subscription.is_some() {
event_log_summary.should_ignore_retry = true;
return Ok(EnrichedWebhookEvent::from_actionless_log(event_log_summary));
}
let maybe_product = get_artcraft_product_by_stripe_id_and_env(
&summary.stripe_product_id, server_environment);
let product = match maybe_product {
Some(StripeArtcraftGenericProductInfo::Subscription(subscription)) => subscription,
Some(StripeArtcraftGenericProductInfo::CreditsPack(credits_pack)) => {
error!("Received 'customer.subscription.updated' for a credits pack product ({}). This should not happen.", &credits_pack.slug.to_str());
event_log_summary.should_ignore_retry = true;
return Ok(EnrichedWebhookEvent::from_actionless_log(event_log_summary));
}
None => {
error!("No matching product for stripe product ID: {}", &summary.stripe_product_id);
event_log_summary.should_ignore_retry = true;
return Ok(EnrichedWebhookEvent::from_actionless_log(event_log_summary));
}
};
// TODO: Multiple ways to get this; better ways to get this
let user_token = match &summary.user_token {
Some(token) => token.clone(),
None => {
warn!("No user token found in `customer.subscription.created` metadata. Cannot proceed.");
return Err(StripeArtcraftWebhookError::BadRequest("no user token in customer.subscription.created".to_string()));
}
};
event_log_summary.should_ignore_retry = true;
let calculated_end_date = calculate_subscription_end_date(&summary);
let summary = extract_subscription_details(subscription, server_environment)?;
Ok(EnrichedWebhookEvent {
maybe_billing_action: Some(ArtcraftBillingAction::SubscriptionCreated(
UpsertableSubscriptionDetails {
// Unique Stripe foreign key
stripe_customer_id: summary.stripe_customer_id,
// Other Stripe foreign keys
stripe_subscription_id: summary.stripe_subscription_id,
stripe_price_id: summary.stripe_price_id,
stripe_product_id: summary.stripe_product_id.clone(), // TODO: Avoid clone.
// Artcraft info
subscription: product.clone(), // TODO: Avoid clone
owner_user_token: user_token,
// Status
stripe_subscription_status: summary.stripe_subscription_status,
// Timing data
stripe_recurring_interval: summary.subscription_interval,
calculated_subscription_expires_at: calculated_end_date,
subscription_start_at: summary.subscription_start_date,
current_billing_period_start_at: summary.current_billing_period_start,
current_billing_period_end_at: summary.current_billing_period_end,
maybe_cancel_at: summary.maybe_cancel_at,
maybe_canceled_at: summary.maybe_canceled_at,
// Misc
stripe_is_production: summary.stripe_is_production,
})),
webhook_event_log_summary: event_log_summary,
maybe_billing_action: Some(ArtcraftBillingAction::SubscriptionCreated(summary.subscription_details)),
webhook_event_log_summary: summary.event_log_summary,
})
}
@@ -1,18 +1,8 @@
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::billing_action_fulfillment::artcraft_billing_action::ArtcraftBillingAction;
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::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 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};
use mysql_queries::queries::users::user_subscriptions::get_user_subscription_by_stripe_subscription_id_transactional::get_user_subscription_by_stripe_subscription_id_transactional;
use mysql_queries::queries::users::user_subscriptions::upsert_user_subscription_by_stripe_id::UpsertUserSubscription;
use crate::endpoints::webhook::webhook_event_enrichment::customer_subscription::extract_subscription_details::extract_subscription_details;
use reusable_types::server_environment::ServerEnvironment;
use reusable_types::stripe::stripe_subscription_status::StripeSubscriptionStatus;
use sqlx::pool::PoolConnection;
use sqlx::{MySql, MySqlPool, Transaction};
use stripe_shared::Subscription;
/// Handle event type: 'customer.subscription.deleted'
@@ -20,101 +10,12 @@ use stripe_shared::Subscription;
pub async fn customer_subscription_deleted_handler(
subscription: &Subscription,
server_environment: ServerEnvironment,
transaction: &mut Transaction<'_, MySql>,
) -> Result<WebhookEventLogSummary, StripeArtcraftWebhookError> {
) -> Result<EnrichedWebhookEvent, StripeArtcraftWebhookError> {
let summary = subscription_summary_extractor(subscription)
.map_err(|err| {
let reason = format!("Error extracting subscription from 'customer.subscription.deleted' payload: {:?}", err);
error!("{}", reason);
StripeArtcraftWebhookError::ServerError(reason) // NB: This was probably *our* fault.
})?;
let summary = extract_subscription_details(subscription, server_environment)?;
let mut should_process_update = true;
let mut result = WebhookEventLogSummary {
maybe_user_token: summary.user_token.clone(),
maybe_event_entity_id: Some(summary.stripe_subscription_id.clone()),
maybe_stripe_customer_id: Some(summary.stripe_customer_id.clone()),
action_was_taken: false,
should_ignore_retry: false,
};
let maybe_product = get_artcraft_product_by_stripe_id_and_env(
&summary.stripe_product_id, server_environment);
let product = match maybe_product {
Some(StripeArtcraftGenericProductInfo::Subscription(subscription)) => subscription,
Some(StripeArtcraftGenericProductInfo::CreditsPack(credits_pack)) => {
error!("Received 'customer.subscription.updated' for a credits pack product ({}). This should not happen.", &credits_pack.slug.to_str());
result.should_ignore_retry = true;
return Ok(result);
}
None => {
error!("No matching product for stripe product ID: {}", &summary.stripe_product_id);
result.should_ignore_retry = true;
return Ok(result);
}
};
// NB: It's possible to receive events out of order.
let maybe_existing_subscription = get_user_subscription_by_stripe_subscription_id_transactional(
&summary.stripe_subscription_id, transaction)
.await
.map_err(|err| {
let reason = format!("Mysql error: {:?}", err);
error!("{}", reason);
StripeArtcraftWebhookError::ServerError(reason)
})?;
if let Some(existing_subscription) = maybe_existing_subscription {
match existing_subscription.maybe_stripe_subscription_status {
Some(StripeSubscriptionStatus::Canceled) => {
// NB: The stored subscription already had a terminal status and the subscription cannot be updated any further.
should_process_update = false;
result.should_ignore_retry = true;
}
_ => {}
}
}
// NB: Even if we haven't received a record before, we should still be able to "tombstone" it
// once we detect the deletion.
if should_process_update {
if let Some(user_token) = summary.user_token.as_ref() {
let upsert = UpsertUserSubscription {
stripe_subscription_id: &summary.stripe_subscription_id,
user_token: user_token.as_str(),
subscription_namespace: SubscriptionNamespace::Artcraft,
subscription_product_slug: &product.slug.to_str(),
maybe_stripe_customer_id: Some(&summary.stripe_customer_id),
maybe_stripe_product_id: Some(&summary.stripe_product_id),
maybe_stripe_price_id: Some(&summary.stripe_price_id),
maybe_stripe_recurring_interval: Some(summary.subscription_interval),
maybe_stripe_subscription_status: Some(summary.stripe_subscription_status),
maybe_stripe_is_production: Some(summary.stripe_is_production),
subscription_start_at: summary.subscription_start_date,
current_billing_period_start_at: summary.current_billing_period_start,
current_billing_period_end_at: summary.current_billing_period_end,
subscription_expires_at: calculate_subscription_end_date(&summary),
maybe_cancel_at: summary.maybe_cancel_at,
maybe_canceled_at: summary.maybe_canceled_at,
};
let _r = upsert.upsert_with_transaction(transaction)
.await
.map_err(|err| {
let reason = format!("Mysql error: {:?}", err);
error!("{}", reason);
StripeArtcraftWebhookError::ServerError(reason)
})?;
result.action_was_taken = true;
}
result.should_ignore_retry = true;
}
Ok(result)
Ok(EnrichedWebhookEvent {
maybe_billing_action: Some(ArtcraftBillingAction::SubscriptionDeleted(summary.subscription_details)),
webhook_event_log_summary: summary.event_log_summary,
})
}
@@ -1,135 +1,20 @@
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::billing_action_fulfillment::artcraft_billing_action::ArtcraftBillingAction;
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::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 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};
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};
use mysql_queries::queries::users::user_subscriptions::get_user_subscription_by_stripe_subscription_id_transactional::get_user_subscription_by_stripe_subscription_id_transactional;
use mysql_queries::queries::users::user_subscriptions::upsert_user_subscription_by_stripe_id::UpsertUserSubscription;
use crate::endpoints::webhook::webhook_event_enrichment::customer_subscription::extract_subscription_details::extract_subscription_details;
use reusable_types::server_environment::ServerEnvironment;
use reusable_types::stripe::stripe_subscription_status::StripeSubscriptionStatus;
use sqlx::pool::PoolConnection;
use sqlx::{MySql, MySqlPool, Transaction};
use stripe_shared::Subscription;
/// Handle event type: 'customer.subscription.updated'
pub async fn customer_subscription_updated_handler(
subscription: &Subscription,
server_environment: ServerEnvironment,
transaction: &mut Transaction<'_, MySql>,
) -> Result<WebhookEventLogSummary, StripeArtcraftWebhookError> {
) -> Result<EnrichedWebhookEvent, StripeArtcraftWebhookError> {
let summary = subscription_summary_extractor(subscription)
.map_err(|err| {
let reason = format!("Error extracting subscription from 'customer.subscription.updated' payload: {:?}", err);
error!("{}", reason);
StripeArtcraftWebhookError::ServerError(reason) // NB: This was probably *our* fault.
})?;
let summary = extract_subscription_details(subscription, server_environment)?;
let mut result = WebhookEventLogSummary {
maybe_user_token: summary.user_token.clone(),
maybe_event_entity_id: Some(summary.stripe_subscription_id.clone()),
maybe_stripe_customer_id: Some(summary.stripe_customer_id.clone()),
action_was_taken: false,
should_ignore_retry: false,
};
let mut should_process_update = true;
let maybe_product = get_artcraft_product_by_stripe_id_and_env(
&summary.stripe_product_id, server_environment);
let product = match maybe_product {
Some(StripeArtcraftGenericProductInfo::Subscription(subscription)) => subscription,
Some(StripeArtcraftGenericProductInfo::CreditsPack(credits_pack)) => {
error!("Received 'customer.subscription.updated' for a credits pack product ({}). This should not happen.", &credits_pack.slug.to_str());
result.should_ignore_retry = true;
return Ok(result);
}
None => {
error!("No matching product for stripe product ID: {}", &summary.stripe_product_id);
result.should_ignore_retry = true;
return Ok(result);
}
};
// NB: It's possible to receive events out of order.
let maybe_existing_subscription = get_user_subscription_by_stripe_subscription_id_transactional(
&summary.stripe_subscription_id, transaction)
.await
.map_err(|err| {
let reason = format!("Mysql error: {:?}", err);
error!("{}", reason);
StripeArtcraftWebhookError::ServerError(reason)
})?;
if let Some(existing_subscription) = maybe_existing_subscription {
match existing_subscription.maybe_stripe_subscription_status {
Some(StripeSubscriptionStatus::Canceled) => {
// NB: The stored subscription already had a terminal status and the subscription cannot be updated any further.
should_process_update = false;
result.should_ignore_retry = true;
}
_ => {}
}
}
if should_process_update {
if let Some(user_token) = summary.user_token.as_ref() {
info!("Upserting subscription");
let upsert = UpsertUserSubscription {
stripe_subscription_id: &summary.stripe_subscription_id,
user_token: user_token.as_str(),
subscription_namespace: SubscriptionNamespace::Artcraft,
subscription_product_slug: &product.slug.to_str(),
maybe_stripe_customer_id: Some(&summary.stripe_customer_id),
maybe_stripe_product_id: Some(&summary.stripe_product_id),
maybe_stripe_price_id: Some(&summary.stripe_price_id),
maybe_stripe_recurring_interval: Some(summary.subscription_interval),
maybe_stripe_subscription_status: Some(summary.stripe_subscription_status),
maybe_stripe_is_production: Some(summary.stripe_is_production),
subscription_start_at: summary.subscription_start_date,
current_billing_period_start_at: summary.current_billing_period_start,
current_billing_period_end_at: summary.current_billing_period_end,
subscription_expires_at: calculate_subscription_end_date(&summary),
maybe_cancel_at: summary.maybe_cancel_at,
maybe_canceled_at: summary.maybe_canceled_at,
};
let _r = upsert.upsert_with_transaction(transaction)
.await
.map_err(|err| {
let reason = format!("Mysql error: {:?}", err);
error!("{}", reason);
StripeArtcraftWebhookError::ServerError(reason)
})?;
info!("Updating user record with stripe customer ID");
// // TODO: Should we care if a user accidentally gets two stripe customer IDs and this
// // overwrites one of them?
// update_user_record_with_new_stripe_customer_id_with_connection(
// transaction,
// user_token,
// Some(&summary.stripe_customer_id))
// .await
// .map_err(|err| {
// let reason = format!("Mysql error: {:?}", err);
// error!("{}", reason);
// StripeArtcraftWebhookError::ServerError(reason)
// })?;
result.action_was_taken = true;
}
}
result.should_ignore_retry = true;
Ok(result)
Ok(EnrichedWebhookEvent {
maybe_billing_action: Some(ArtcraftBillingAction::SubscriptionUpdated(summary.subscription_details)),
webhook_event_log_summary: summary.event_log_summary,
})
}
@@ -0,0 +1,96 @@
use crate::billing_action_fulfillment::artcraft_billing_action::{ArtcraftBillingAction, UpsertableSubscriptionDetails};
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::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::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 log::{error, warn};
use reusable_types::server_environment::ServerEnvironment;
use stripe_shared::Subscription;
pub struct EventLogAndSubscriptionDetails {
pub event_log_summary: WebhookEventLogSummary,
pub subscription_details: UpsertableSubscriptionDetails,
}
pub fn extract_subscription_details(
subscription: &Subscription,
server_environment: ServerEnvironment,
) -> Result<EventLogAndSubscriptionDetails, StripeArtcraftWebhookError> {
let summary = subscription_summary_extractor(subscription)
.map_err(|err| {
let reason = format!("Error extracting subscription from payload: {:?}", err);
error!("{}", reason);
StripeArtcraftWebhookError::ServerError(reason) // NB: This was probably *our* fault.
})?;
let event_log_summary = WebhookEventLogSummary {
maybe_user_token: summary.user_token.clone(),
maybe_event_entity_id: Some(summary.stripe_subscription_id.clone()),
maybe_stripe_customer_id: Some(summary.stripe_customer_id.clone()),
action_was_taken: false,
should_ignore_retry: false,
};
let maybe_product = get_artcraft_product_by_stripe_id_and_env(
&summary.stripe_product_id, server_environment);
let product = match maybe_product {
Some(StripeArtcraftGenericProductInfo::Subscription(subscription)) => subscription,
Some(StripeArtcraftGenericProductInfo::CreditsPack(credits_pack)) => {
error!("Received a non-subscription credits pack product ({}). This should not happen.", &credits_pack.slug.to_str());
return Err(StripeArtcraftWebhookError::BadRequest("wrong product type".to_string()));
}
None => {
error!("No matching product for stripe product ID: {}", &summary.stripe_product_id);
return Err(StripeArtcraftWebhookError::BadRequest("no matching product".to_string()));
}
};
// TODO: Multiple ways to get this; better ways to get this
let user_token = match &summary.user_token {
Some(token) => token.clone(),
None => {
warn!("No user token found in subscription metadata. Cannot proceed.");
return Err(StripeArtcraftWebhookError::BadRequest("no user token in subscription event".to_string()));
}
};
let calculated_end_date = calculate_subscription_end_date(&summary);
let subscription_details = UpsertableSubscriptionDetails {
// Unique Stripe foreign key
stripe_customer_id: summary.stripe_customer_id,
// Other Stripe foreign keys
stripe_subscription_id: summary.stripe_subscription_id,
stripe_price_id: summary.stripe_price_id,
stripe_product_id: summary.stripe_product_id.clone(), // TODO: Avoid clone.
// Artcraft info
subscription: product.clone(), // TODO: Avoid clone
owner_user_token: user_token,
// Status
stripe_subscription_status: summary.stripe_subscription_status,
// Timing data
stripe_recurring_interval: summary.subscription_interval,
calculated_subscription_expires_at: calculated_end_date,
subscription_start_at: summary.subscription_start_date,
current_billing_period_start_at: summary.current_billing_period_start,
current_billing_period_end_at: summary.current_billing_period_end,
maybe_cancel_at: summary.maybe_cancel_at,
maybe_canceled_at: summary.maybe_canceled_at,
// Misc
stripe_is_production: summary.stripe_is_production,
};
Ok(EventLogAndSubscriptionDetails {
event_log_summary,
subscription_details,
})
}
@@ -2,4 +2,5 @@ pub mod calculate_subscription_end_date;
pub mod customer_subscription_created_handler;
pub mod customer_subscription_deleted_handler;
pub mod customer_subscription_updated_handler;
pub mod subscription_event_extractor;
pub mod subscription_event_extractor;
mod extract_subscription_details;
@@ -109,7 +109,6 @@ pub async fn handle_webhook_event_enrichment(
// =============== CUSTOMER SUBSCRIPTIONS ===============
/* TODO TEMP -
EventObject::CustomerSubscriptionCreated(subscription) => {
info!("Event: {}, data: {:?}", webhook_payload.type_, subscription);
// DO NOT USE TO PROVISION SERVICE.
@@ -131,31 +130,27 @@ pub async fn handle_webhook_event_enrichment(
//
// TODO: add `maybe_billing_cycle_anchor`
//
webhook_summary = customer_subscription_created_handler(
return customer_subscription_created_handler(
&subscription,
server_environment,
transaction,
).await?;
).await;
}
EventObject::CustomerSubscriptionUpdated(subscription) => {
info!("Event: {}, data: {:?}", webhook_payload.type_, subscription);
webhook_summary = customer_subscription_updated_handler(
return customer_subscription_updated_handler(
&subscription,
server_environment,
transaction,
).await?;
).await;
}
EventObject::CustomerSubscriptionDeleted(subscription) => {
info!("Event: {}, data: {:?}", webhook_payload.type_, subscription);
webhook_summary = customer_subscription_deleted_handler(
return customer_subscription_deleted_handler(
&subscription,
server_environment,
transaction,
).await?;
).await;
}
*/
// =============== INVOICES ===============