mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-07 02:21:23 +00:00
refactor subscriptions handlers
This commit is contained in:
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.
|
||||
|
||||
+12
-14
@@ -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)
|
||||
|
||||
+37
@@ -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
|
||||
|
||||
+8
-3
@@ -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"));
|
||||
|
||||
+5
-108
@@ -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,
|
||||
})
|
||||
}
|
||||
+9
-108
@@ -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,
|
||||
})
|
||||
}
|
||||
+9
-124
@@ -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,
|
||||
})
|
||||
}
|
||||
+96
@@ -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
-1
@@ -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;
|
||||
+6
-11
@@ -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 ===============
|
||||
|
||||
|
||||
Reference in new issue
Block a user