From b9de0bf456bd2630b02786ef46d25627a8e77210 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 8 Sep 2025 03:59:00 -0400 Subject: [PATCH] billing anchor --- .../user_tables/user_subscriptions.sql | 4 ++++ .../upsert_user_subscription_by_stripe_id.rs | 12 ++++++++++++ .../customer_subscription_created_handler.rs | 1 + .../customer_subscription_deleted_handler.rs | 1 + .../customer_subscription_updated_handler.rs | 1 + .../artcraft_billing_action.rs | 4 ++++ .../subscriptions/upsert_subscription_details.rs | 1 + .../extract_subscription_details.rs | 2 ++ .../subscription_event_extractor.rs | 13 ++++++++----- 9 files changed, 34 insertions(+), 5 deletions(-) diff --git a/_sql/migrations_squashed/user_tables/user_subscriptions.sql b/_sql/migrations_squashed/user_tables/user_subscriptions.sql index 2111c68c48..68c3118bf5 100644 --- a/_sql/migrations_squashed/user_tables/user_subscriptions.sql +++ b/_sql/migrations_squashed/user_tables/user_subscriptions.sql @@ -59,6 +59,10 @@ CREATE TABLE user_subscriptions ( -- Which day of the month, or month of the year, etc. to anchor the subscription to. -- See docs on the subscription object. + -- Stripe: + -- "The reference point that aligns future billing cycle dates. It sets the day + -- of week for week intervals, the day of month for month and year intervals, and + -- the month of year for year intervals. The timestamp is in UTC format." maybe_stripe_billing_cycle_anchor TIMESTAMP DEFAULT NULL, maybe_stripe_is_production BOOLEAN DEFAULT NULL, diff --git a/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/upsert_user_subscription_by_stripe_id.rs b/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/upsert_user_subscription_by_stripe_id.rs index 216f8d0e5e..d9a04878d0 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/upsert_user_subscription_by_stripe_id.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/upsert_user_subscription_by_stripe_id.rs @@ -39,6 +39,10 @@ pub struct UpsertUserSubscription<'a> { /// This may predate Stripe's subscription object `created` field due to backdating. pub subscription_start_at: NaiveDateTime, + // Which day of the month, month of the year, etc. to anchor the subscription against. + // See the Stripe docs for more. + pub maybe_stripe_billing_cycle_anchor: Option, + // Billing periods for the subscription... pub current_billing_period_start_at: NaiveDateTime, @@ -116,6 +120,8 @@ SET maybe_stripe_recurring_interval = ?, maybe_stripe_subscription_status = ?, maybe_stripe_is_production = ?, + + maybe_stripe_billing_cycle_anchor = ?, subscription_start_at = ?, current_billing_period_start_at = ?, @@ -135,6 +141,8 @@ ON DUPLICATE KEY UPDATE maybe_stripe_recurring_interval = ?, maybe_stripe_subscription_status = ?, + + maybe_stripe_billing_cycle_anchor = ?, current_billing_period_start_at = ?, current_billing_period_end_at = ?, @@ -159,6 +167,8 @@ ON DUPLICATE KEY UPDATE self.maybe_stripe_recurring_interval.as_deref(), self.maybe_stripe_subscription_status.as_deref(), self.maybe_stripe_is_production, + + self.maybe_stripe_billing_cycle_anchor, self.subscription_start_at, self.current_billing_period_start_at, @@ -176,6 +186,8 @@ ON DUPLICATE KEY UPDATE self.maybe_stripe_recurring_interval.as_deref(), self.maybe_stripe_subscription_status.as_deref(), + + self.maybe_stripe_billing_cycle_anchor, self.current_billing_period_start_at, self.current_billing_period_end_at, diff --git a/crates/service/plugins/billing/src/stripe/http_endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_created_handler.rs b/crates/service/plugins/billing/src/stripe/http_endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_created_handler.rs index 3233de1976..ee958a9cd9 100644 --- a/crates/service/plugins/billing/src/stripe/http_endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_created_handler.rs +++ b/crates/service/plugins/billing/src/stripe/http_endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_created_handler.rs @@ -84,6 +84,7 @@ pub async fn customer_subscription_created_handler( 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, + maybe_stripe_billing_cycle_anchor: None, 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), diff --git a/crates/service/plugins/billing/src/stripe/http_endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_deleted_handler.rs b/crates/service/plugins/billing/src/stripe/http_endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_deleted_handler.rs index d5f4f4cd9a..8c56da61b1 100644 --- a/crates/service/plugins/billing/src/stripe/http_endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_deleted_handler.rs +++ b/crates/service/plugins/billing/src/stripe/http_endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_deleted_handler.rs @@ -85,6 +85,7 @@ pub async fn customer_subscription_deleted_handler( 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, + maybe_stripe_billing_cycle_anchor: None, 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), diff --git a/crates/service/plugins/billing/src/stripe/http_endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_updated_handler.rs b/crates/service/plugins/billing/src/stripe/http_endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_updated_handler.rs index 8a683bdfc3..4ed255c611 100644 --- a/crates/service/plugins/billing/src/stripe/http_endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_updated_handler.rs +++ b/crates/service/plugins/billing/src/stripe/http_endpoints/webhook/webhook_event_handlers/customer_subscription/customer_subscription_updated_handler.rs @@ -85,6 +85,7 @@ pub async fn customer_subscription_updated_handler( 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, + maybe_stripe_billing_cycle_anchor: None, 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), diff --git a/crates/service/plugins/billing_artcraft/src/billing_action_fulfillment/artcraft_billing_action.rs b/crates/service/plugins/billing_artcraft/src/billing_action_fulfillment/artcraft_billing_action.rs index 7296137091..3a8b65c5a3 100644 --- a/crates/service/plugins/billing_artcraft/src/billing_action_fulfillment/artcraft_billing_action.rs +++ b/crates/service/plugins/billing_artcraft/src/billing_action_fulfillment/artcraft_billing_action.rs @@ -59,6 +59,10 @@ pub struct UpsertableSubscriptionDetails { pub stripe_recurring_interval : StripeRecurringInterval, + // Which day of the month / month of the year to anchor the subscription against. + // See the Stripe docs. + pub stripe_billing_cycle_anchor: NaiveDateTime, + pub stripe_is_production: bool, /// When the subscription was "created" in Stripe (including any backdating) diff --git a/crates/service/plugins/billing_artcraft/src/billing_action_fulfillment/subscriptions/upsert_subscription_details.rs b/crates/service/plugins/billing_artcraft/src/billing_action_fulfillment/subscriptions/upsert_subscription_details.rs index 677211647b..38391d0608 100644 --- a/crates/service/plugins/billing_artcraft/src/billing_action_fulfillment/subscriptions/upsert_subscription_details.rs +++ b/crates/service/plugins/billing_artcraft/src/billing_action_fulfillment/subscriptions/upsert_subscription_details.rs @@ -66,6 +66,7 @@ pub async fn upsert_subscription_details( maybe_stripe_is_production: Some(details.stripe_is_production), // Timing data + maybe_stripe_billing_cycle_anchor: Some(details.stripe_billing_cycle_anchor), subscription_start_at: details.subscription_start_at, current_billing_period_start_at: details.current_billing_period_start_at, current_billing_period_end_at: details.current_billing_period_end_at, diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/customer_subscription/extract_subscription_details.rs b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/customer_subscription/extract_subscription_details.rs index 204d65497e..4f1b16eb37 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/customer_subscription/extract_subscription_details.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/customer_subscription/extract_subscription_details.rs @@ -60,6 +60,7 @@ pub fn extract_subscription_details( let calculated_end_date = calculate_subscription_end_date(&summary); + let subscription_details = UpsertableSubscriptionDetails { // Unique Stripe foreign key stripe_customer_id: summary.stripe_customer_id, @@ -77,6 +78,7 @@ pub fn extract_subscription_details( stripe_subscription_status: summary.stripe_subscription_status, // Timing data + stripe_billing_cycle_anchor: summary.stripe_billing_cycle_anchor, stripe_recurring_interval: summary.subscription_interval, calculated_subscription_expires_at: calculated_end_date, subscription_start_at: summary.subscription_start_date, diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/customer_subscription/subscription_event_extractor.rs b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/customer_subscription/subscription_event_extractor.rs index e2c8c3f2c8..92774f558b 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/customer_subscription/subscription_event_extractor.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/webhook/webhook_event_enrichment/customer_subscription/subscription_event_extractor.rs @@ -1,4 +1,3 @@ -use crate::configs::stripe_artcraft_metadata_keys::STRIPE_ARTCRAFT_METADATA_USER_TOKEN; use crate::utils::enum_conversion::recurring_interval_to_reusable_type::recurring_interval_to_reusable_type; use crate::utils::enum_conversion::subscription_status_to_reusable_type::subscription_status_to_reusable_type; use crate::utils::expand_ids::expand_customer_id::expand_customer_id; @@ -7,11 +6,9 @@ use crate::utils::metadata::get_metadata_user_token::get_metadata_user_token; use anyhow::anyhow; use chrono::NaiveDateTime; use errors::AnyhowResult; -use log::error; use reusable_types::stripe::stripe_recurring_interval::StripeRecurringInterval; use reusable_types::stripe::stripe_subscription_status::StripeSubscriptionStatus; use stripe_shared::{Subscription, SubscriptionStatus}; -use stripe_types::Expandable; use tokens::tokens::users::UserToken; #[derive(Clone, Debug)] @@ -39,6 +36,10 @@ pub struct SubscriptionSummary { pub subscription_is_active: bool, pub subscription_interval: StripeRecurringInterval, + // Which day of the month / month of the year to anchor the subscription against. + // See the Stripe docs. + pub stripe_billing_cycle_anchor: NaiveDateTime, + /// When the subscription was "created" in Stripe (including any backdating) pub subscription_start_date: NaiveDateTime, @@ -55,11 +56,10 @@ pub struct SubscriptionSummary { /// This should be unit testable against raw webhook JSON. pub fn subscription_summary_extractor(subscription: &Subscription) -> AnyhowResult { let subscription_id = subscription.id.to_string(); + // NB: Our internal user token. let maybe_user_token = get_metadata_user_token(&subscription.metadata); - error!("SUBSCRIPTION EVENT USER TOKEN: {:?}", maybe_user_token); - if subscription.items.data.len() != 1 { return Err(anyhow!("Too many items in subscription {} : {}", subscription_id, subscription.items.data.len())); @@ -83,6 +83,8 @@ pub fn subscription_summary_extractor(subscription: &Subscription) -> AnyhowResu let period_start = NaiveDateTime::from_timestamp(item.current_period_start, 0); let period_end = NaiveDateTime::from_timestamp(item.current_period_end, 0); + let stripe_billing_cycle_anchor = NaiveDateTime::from_timestamp(subscription.billing_cycle_anchor, 0); + let maybe_cancel_at = subscription.cancel_at.map(|t| NaiveDateTime::from_timestamp(t, 0)); let maybe_canceled_at = subscription.canceled_at.map(|t| NaiveDateTime::from_timestamp(t, 0)); @@ -100,6 +102,7 @@ pub fn subscription_summary_extractor(subscription: &Subscription) -> AnyhowResu subscription_start_date: start_date, current_billing_period_start: period_start, current_billing_period_end: period_end, + stripe_billing_cycle_anchor, maybe_cancel_at, maybe_canceled_at, })