Handle subscription update and create

This commit is contained in:
Brandon Thomas committed 2022-09-18 23:26:39 -04:00
1 parent ca63e00905
commit a2e71d8944
13 files changed
+230 -16

No files matched your search

@@ -140,17 +140,17 @@ pub async fn stripe_webhook_handler(
EventType::CustomerSubscriptionCreated => {
if let EventObject::Subscription(subscription) = webhook_payload.data.object {
webhook_summary = customer_subscription_created_handler(&subscription)?;
webhook_summary = customer_subscription_created_handler(&subscription, &mysql_pool).await?;
}
}
EventType::CustomerSubscriptionUpdated => {
if let EventObject::Subscription(subscription) = webhook_payload.data.object {
webhook_summary = customer_subscription_updated_handler(&subscription)?;
webhook_summary = customer_subscription_updated_handler(&subscription, &mysql_pool).await?;
}
}
EventType::CustomerSubscriptionDeleted => {
if let EventObject::Subscription(subscription) = webhook_payload.data.object {
webhook_summary = customer_subscription_deleted_handler(&subscription)?;
webhook_summary = customer_subscription_deleted_handler(&subscription, &mysql_pool).await?;
}
}
@@ -202,7 +202,7 @@ pub async fn stripe_webhook_handler(
EventType::PaymentIntentSucceeded => {
if let EventObject::PaymentIntent(payment_intent) = webhook_payload.data.object {
let _r = payment_intent_succeeded_handler(&payment_intent)?;
webhook_summary = payment_intent_succeeded_handler(&payment_intent)?;
}
}
@@ -219,7 +219,7 @@ pub async fn stripe_webhook_handler(
EventType::ChargeSucceeded => {
if let EventObject::Charge(charge) = webhook_payload.data.object {
let _r = charge_succeeded_handler(&charge)?;
webhook_summary = charge_succeeded_handler(&charge)?;
}
}
@@ -2,23 +2,56 @@ use crate::stripe::webhook_event_handlers::customer_subscription::subscription_e
use crate::stripe::webhook_event_handlers::stripe_webhook_error::StripeWebhookError;
use crate::stripe::webhook_event_handlers::stripe_webhook_summary::StripeWebhookSummary;
use log::{error, warn};
use sqlx::MySqlPool;
use stripe::Subscription;
use database_queries::queries::billing::subscriptions::upsert_subscription_by_stripe_id::UpsertSubscriptionByStripeId;
/// Handle event type: 'customer.subscription.created'
/// 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.
pub fn customer_subscription_created_handler(subscription: &Subscription) -> Result<StripeWebhookSummary, StripeWebhookError> {
pub async fn customer_subscription_created_handler(
subscription: &Subscription,
mysql_pool: &MySqlPool,
) -> Result<StripeWebhookSummary, StripeWebhookError> {
let summary = subscription_summary_extractor(subscription)
.map_err(|err| {
error!("Error extracting subscription from 'customer.subscription.created' payload: {:?}", err);
StripeWebhookError::ServerError // NB: This was probably *our* fault.
})?;
match summary.user_token.as_deref() {
None => {
error!("Subscription does not have a user token associated with it: {}",
&summary.stripe_subscription_id);
}
Some(user_token) => {
let upsert = UpsertSubscriptionByStripeId {
stripe_subscription_id: &summary.stripe_subscription_id,
user_token: user_token,
subscription_category: "todo",
subscription_product_key: "todo",
maybe_stripe_product_id: Some(&summary.stripe_product_id),
maybe_stripe_customer_id: Some(&summary.stripe_customer_id),
maybe_stripe_is_production: Some(summary.stripe_is_production),
subscription_created_at: summary.subscription_period_start,
subscription_expires_at: summary.subscription_period_end,
};
let _r = upsert.upsert(mysql_pool)
.await
.map_err(|err| {
error!("Mysql error: {:?}", err);
StripeWebhookError::ServerError
})?;
}
}
Ok(StripeWebhookSummary {
maybe_user_token: summary.user_token,
maybe_event_entity_id: Some(summary.stripe_subscription_id),
maybe_stripe_customer_id: Some(summary.stripe_customer_id),
event_was_handled: false,
event_was_handled: true,
})
}
@@ -2,11 +2,15 @@ use crate::stripe::webhook_event_handlers::customer_subscription::subscription_e
use crate::stripe::webhook_event_handlers::stripe_webhook_error::StripeWebhookError;
use crate::stripe::webhook_event_handlers::stripe_webhook_summary::StripeWebhookSummary;
use log::{error, warn};
use sqlx::MySqlPool;
use stripe::Subscription;
/// Handle event type: 'customer.subscription.deleted'
/// Sent when a customer’s subscription ends.
pub fn customer_subscription_deleted_handler(subscription: &Subscription) -> Result<StripeWebhookSummary, StripeWebhookError> {
pub async fn customer_subscription_deleted_handler(
subscription: &Subscription,
mysql_pool: &MySqlPool,
) -> Result<StripeWebhookSummary, StripeWebhookError> {
let summary = subscription_summary_extractor(subscription)
.map_err(|err| {
error!("Error extracting subscription from 'customer.subscription.deleted' payload: {:?}", err);
@@ -1,21 +1,54 @@
use crate::stripe::webhook_event_handlers::customer_subscription::subscription_event_extractor::subscription_summary_extractor;
use crate::stripe::webhook_event_handlers::stripe_webhook_error::StripeWebhookError;
use crate::stripe::webhook_event_handlers::stripe_webhook_summary::StripeWebhookSummary;
use database_queries::queries::billing::subscriptions::upsert_subscription_by_stripe_id::UpsertSubscriptionByStripeId;
use log::{error, warn};
use sqlx::MySqlPool;
use stripe::Subscription;
/// Handle event type: 'customer.subscription.updated'
pub fn customer_subscription_updated_handler(subscription: &Subscription) -> Result<StripeWebhookSummary, StripeWebhookError> {
pub async fn customer_subscription_updated_handler(
subscription: &Subscription,
mysql_pool: &MySqlPool,
) -> Result<StripeWebhookSummary, StripeWebhookError> {
let summary = subscription_summary_extractor(subscription)
.map_err(|err| {
error!("Error extracting subscription from 'customer.subscription.updated' payload: {:?}", err);
StripeWebhookError::ServerError // NB: This was probably *our* fault.
})?;
match summary.user_token.as_deref() {
None => {
error!("Subscription does not have a user token associated with it: {}",
&summary.stripe_subscription_id);
}
Some(user_token) => {
let upsert = UpsertSubscriptionByStripeId {
stripe_subscription_id: &summary.stripe_subscription_id,
user_token: user_token,
subscription_category: "todo",
subscription_product_key: "todo",
maybe_stripe_product_id: Some(&summary.stripe_product_id),
maybe_stripe_customer_id: Some(&summary.stripe_customer_id),
maybe_stripe_is_production: Some(summary.stripe_is_production),
subscription_created_at: summary.subscription_period_start,
subscription_expires_at: summary.subscription_period_end,
};
let _r = upsert.upsert(mysql_pool)
.await
.map_err(|err| {
error!("Mysql error: {:?}", err);
StripeWebhookError::ServerError
})?;
}
}
Ok(StripeWebhookSummary {
maybe_user_token: summary.user_token,
maybe_event_entity_id: Some(summary.stripe_subscription_id),
maybe_stripe_customer_id: Some(summary.stripe_customer_id),
event_was_handled: false,
event_was_handled: true,
})
}
@@ -2,11 +2,13 @@ use container_common::anyhow_result::AnyhowResult;
use crate::stripe::helpers::common_metadata_keys::METADATA_USER_TOKEN;
use crate::stripe::helpers::expand_customer_id::expand_customer_id;
use crate::stripe::webhook_event_handlers::stripe_webhook_error::StripeWebhookError;
use crate::stripe::webhook_event_handlers::stripe_webhook_summary::StripeWebhookSummary;
use log::warn;
use stripe::{Invoice, PaymentIntent};
// Handle event type: 'payment_intent.succeeded'
pub fn payment_intent_succeeded_handler(payment_intent: &PaymentIntent) -> Result<(), StripeWebhookError> {
pub fn payment_intent_succeeded_handler(payment_intent: &PaymentIntent) -> Result<StripeWebhookSummary, StripeWebhookError> {
let payment_intent_id = payment_intent.id.to_string();
let payment_intent_status = payment_intent.status;
@@ -20,8 +22,10 @@ pub fn payment_intent_succeeded_handler(payment_intent: &PaymentIntent) -> Resul
let maybe_user_token = payment_intent.metadata.get(METADATA_USER_TOKEN)
.map(|t| t.to_string());
warn!(">>> payment_intent.succeeded: {:?}, {:?}, {:?}",
payment_intent_status, maybe_stripe_customer_id, maybe_user_token);
Ok(())
Ok(StripeWebhookSummary {
maybe_user_token: maybe_user_token,
maybe_event_entity_id: Some(payment_intent_id),
maybe_stripe_customer_id: maybe_stripe_customer_id,
event_was_handled: false,
})
}
@@ -1 +1,2 @@
pub mod stripe;
pub mod stripe;
pub mod subscriptions;
@@ -0,0 +1 @@
pub mod upsert_subscription_by_stripe_id;
@@ -0,0 +1,96 @@
use anyhow::anyhow;
use chrono::NaiveDateTime;
use container_common::anyhow_result::AnyhowResult;
use sqlx::MySqlPool;
use crate::tokens::Tokens;
// TODO: Make a trait with default impls to handle common query concerns.
pub struct UpsertSubscriptionByStripeId <'a> {
/// Stripe's assigned ID for the subscription
/// This acts as an externally-provided unique key for records in this table.
pub stripe_subscription_id: &'a str,
/// Internal user token
pub user_token: &'a str,
/// The platform key, eg. "fakeyou", "storyteller_stream", "symphonia", etc.
pub subscription_category: &'a str,
/// The name of the product the user is subscribing to within the category.
pub subscription_product_key: &'a str,
pub maybe_stripe_product_id: Option<&'a str>,
pub maybe_stripe_customer_id: Option<&'a str>,
pub maybe_stripe_is_production: Option<bool>,
pub subscription_created_at: NaiveDateTime,
/// When the subscription is set to expire.
/// This controls whether it is active or not.
pub subscription_expires_at: NaiveDateTime,
}
impl <'a> UpsertSubscriptionByStripeId <'a> {
pub async fn upsert(&'a self, mysql_pool: &MySqlPool) -> AnyhowResult<()> {
let token = Tokens::new_subscription_token()?;
// NB: The following behaviors are intentional
// - We only set the token initially; changing it obviously breaks.
// - We only set the user token the first time. Stripe entities may remain coherent,
// but it isn't our business to attempt changes on webhooks.
// - Other "static" fields do not need to change on update, either.
let query = sqlx::query!(
r#"
INSERT INTO user_subscriptions
SET
token = ?,
user_token = ?,
subscription_category = ?,
subscription_product_key = ?,
maybe_stripe_subscription_id = ?,
maybe_stripe_product_id = ?,
maybe_stripe_customer_id = ?,
maybe_stripe_is_production = ?,
subscription_created_at = ?,
subscription_expires_at = ?,
version = version + 1
ON DUPLICATE KEY UPDATE
subscription_expires_at = ?,
version = version + 1
"#,
token,
self.user_token,
self.subscription_category,
self.subscription_product_key,
self.stripe_subscription_id,
self.maybe_stripe_product_id,
self.maybe_stripe_customer_id,
self.maybe_stripe_is_production,
self.subscription_created_at,
self.subscription_expires_at,
// Upsert
self.subscription_expires_at,
);
let query_result = query.execute(mysql_pool).await;
let _record_id = match query_result {
Ok(res) => res.last_insert_id(),
Err(err) => return Err(anyhow!("Error upserting subscription record: {:?}", err)),
};
Ok(())
}
}
@@ -92,4 +92,10 @@ impl Tokens {
pub fn new_twitch_event_rule() -> AnyhowResult<String> {
random_prefix_crockford_token("TER:", 32, false)
}
// ========== [Billing] Tokens not typically visible to users ==========
pub fn new_subscription_token() -> AnyhowResult<String> {
random_prefix_crockford_token("SUB:", 32, false)
}
}
@@ -23,6 +23,7 @@ pub mod configs;
pub mod http_server;
pub mod routes;
pub mod server_state;
pub mod subscriptions;
pub mod threads;
pub mod util;
pub mod validations;
@@ -0,0 +1,2 @@
pub mod subscription_plan;
pub mod subscription_plan_list;
@@ -0,0 +1,33 @@
/**
User has:
fakeyou, pro
fakeyou, pro
stream, basic
*/
#[derive(Clone)]
pub struct SubscriptionPlan {
/// Slug is the unique identifier for the subscription
/// It must be human-readability friendly, consist of only lowercase alphanumerics,
/// underscores, and hyphens, and be 32 characters or fewer.
pub slug: String,
}
impl SubscriptionPlan {
pub fn base_tts_priority_level(&self) -> u32 {
0
}
pub fn base_api_tts_priority_level(&self) -> Option<u32> {
Some(0)
}
pub fn has_feature_foo(&self) -> bool {
false
}
}