mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 09:28:11 +00:00
pull existing inactive subscription, but no fakeyou subs
This commit is contained in:
1 parent
e0400b651e
commit
bcb74b377a
3 files changed
+243
-14
No files matched your search
+198
@@ -0,0 +1,198 @@
|
||||
use crate::errors::select_optional_record_error::SelectOptionalRecordError;
|
||||
use crate::types::query_map::QueryMap;
|
||||
use enums::common::payments_namespace::PaymentsNamespace;
|
||||
use sqlx;
|
||||
use sqlx::mysql::MySqlRow;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::MySql;
|
||||
use tokens::tokens::user_subscriptions::UserSubscriptionToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct PossiblyInactiveUserSubscription {
|
||||
pub token: UserSubscriptionToken,
|
||||
|
||||
pub user_token: UserToken,
|
||||
|
||||
pub subscription_namespace: PaymentsNamespace,
|
||||
pub subscription_product_slug: String,
|
||||
|
||||
pub stripe_subscription_status: String,
|
||||
|
||||
pub stripe_customer_id: String,
|
||||
pub stripe_product_id: String,
|
||||
pub stripe_subscription_id: String,
|
||||
}
|
||||
|
||||
/// This is for restarting subscriptions after they have expired.
|
||||
///
|
||||
/// Technically, there may be more than one subscription record.
|
||||
/// We sort the results and only return the first active artcraft subscription
|
||||
/// by numeric ID so we should have a consistent view.
|
||||
pub async fn find_possibly_inactive_first_subscription_for_owner_user_using_connection(
|
||||
user_token: &UserToken,
|
||||
namespace: PaymentsNamespace,
|
||||
connection: &mut PoolConnection<MySql>,
|
||||
) -> Result<Option<PossiblyInactiveUserSubscription>, SelectOptionalRecordError> {
|
||||
|
||||
let query = query_only_active(user_token, namespace);
|
||||
|
||||
let result = query
|
||||
.fetch_optional(&mut **connection)
|
||||
.await;
|
||||
|
||||
let maybe_result = map_result(result)?;
|
||||
|
||||
if let Some(result) = maybe_result {
|
||||
return Ok(Some(result));
|
||||
}
|
||||
|
||||
let query = query_possibly_inactive(user_token, namespace);
|
||||
|
||||
let result = query
|
||||
.fetch_optional(&mut **connection)
|
||||
.await;
|
||||
|
||||
map_result(result)
|
||||
}
|
||||
|
||||
|
||||
/// This is for restarting subscriptions after they have expired.
|
||||
/// (See notes above.)
|
||||
pub async fn find_possibly_inactive_first_subscription_for_owner_user_using_transaction(
|
||||
user_token: &UserToken,
|
||||
namespace: PaymentsNamespace,
|
||||
transaction: &mut sqlx::Transaction<'_, MySql>,
|
||||
) -> Result<Option<PossiblyInactiveUserSubscription>, SelectOptionalRecordError> {
|
||||
|
||||
let query = query_only_active(user_token, namespace);
|
||||
|
||||
let result = query
|
||||
.fetch_optional(&mut **transaction)
|
||||
.await;
|
||||
|
||||
let maybe_result = map_result(result)?;
|
||||
|
||||
if let Some(result) = maybe_result {
|
||||
return Ok(Some(result));
|
||||
}
|
||||
|
||||
let query = query_possibly_inactive(user_token, namespace);
|
||||
|
||||
let result = query
|
||||
.fetch_optional(&mut **transaction)
|
||||
.await;
|
||||
|
||||
map_result(result)
|
||||
}
|
||||
|
||||
fn map_result(result: Result<Option<RawUserSubscription>, sqlx::Error>) -> Result<Option<PossiblyInactiveUserSubscription>, SelectOptionalRecordError> {
|
||||
match result {
|
||||
Ok(Some(record)) => Ok(Some(PossiblyInactiveUserSubscription {
|
||||
token: record.token,
|
||||
user_token: record.user_token,
|
||||
subscription_namespace: record.subscription_namespace,
|
||||
subscription_product_slug: record.subscription_product_slug,
|
||||
stripe_customer_id: record.maybe_stripe_customer_id
|
||||
.ok_or_else(|| SelectOptionalRecordError::RequiredFieldWasNull("maybe_stripe_customer_id"))?,
|
||||
stripe_product_id: record.maybe_stripe_product_id
|
||||
.ok_or_else(|| SelectOptionalRecordError::RequiredFieldWasNull("maybe_stripe_product_id"))?,
|
||||
stripe_subscription_id: record.maybe_stripe_subscription_id
|
||||
.ok_or_else(|| SelectOptionalRecordError::RequiredFieldWasNull("maybe_stripe_subscription_id"))?,
|
||||
stripe_subscription_status: record.maybe_stripe_subscription_status
|
||||
.ok_or_else(|| SelectOptionalRecordError::RequiredFieldWasNull("maybe_stripe_subscription_status"))?,
|
||||
})),
|
||||
Ok(None) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn query_only_active(user_token: &UserToken, namespace: PaymentsNamespace)
|
||||
-> QueryMap<impl Send + FnMut(MySqlRow) -> Result<RawUserSubscription, sqlx::Error>>
|
||||
{
|
||||
// NB: We want to eventually support multiple subscriptions per user (eg. company use case),
|
||||
// so we do not have a unique key on user token (but we do on stripe subscription id).
|
||||
// In the meantime, to ensure we use the same subscription each time, we order by id and
|
||||
// take the first one.
|
||||
sqlx::query_as!(
|
||||
RawUserSubscription,
|
||||
r#"
|
||||
SELECT
|
||||
token as `token: tokens::tokens::user_subscriptions::UserSubscriptionToken`,
|
||||
user_token as `user_token: tokens::tokens::users::UserToken`,
|
||||
subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,
|
||||
subscription_product_slug,
|
||||
maybe_stripe_subscription_status,
|
||||
maybe_stripe_customer_id,
|
||||
maybe_stripe_product_id,
|
||||
maybe_stripe_subscription_id
|
||||
|
||||
FROM user_subscriptions
|
||||
|
||||
WHERE
|
||||
user_token = ?
|
||||
AND subscription_namespace = ?
|
||||
AND maybe_stripe_customer_id IS NOT NULL
|
||||
AND maybe_stripe_subscription_id IS NOT NULL
|
||||
AND maybe_stripe_subscription_status IS NOT NULL
|
||||
AND maybe_stripe_subscription_status NOT IN ('canceled', 'incomplete_expired')
|
||||
AND subscription_expires_at > CURRENT_TIMESTAMP
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
"#,
|
||||
user_token.as_str(),
|
||||
namespace.to_str(),
|
||||
)
|
||||
}
|
||||
|
||||
fn query_possibly_inactive(user_token: &UserToken, namespace: PaymentsNamespace)
|
||||
-> QueryMap<impl Send + FnMut(MySqlRow) -> Result<RawUserSubscription, sqlx::Error>>
|
||||
{
|
||||
// NB: We want to eventually support multiple subscriptions per user (eg. company use case),
|
||||
// so we do not have a unique key on user token (but we do on stripe subscription id).
|
||||
// In the meantime, to ensure we use the same subscription each time, we order by id and
|
||||
// take the first one.
|
||||
sqlx::query_as!(
|
||||
RawUserSubscription,
|
||||
r#"
|
||||
SELECT
|
||||
token as `token: tokens::tokens::user_subscriptions::UserSubscriptionToken`,
|
||||
user_token as `user_token: tokens::tokens::users::UserToken`,
|
||||
subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,
|
||||
subscription_product_slug,
|
||||
maybe_stripe_subscription_status,
|
||||
maybe_stripe_customer_id,
|
||||
maybe_stripe_product_id,
|
||||
maybe_stripe_subscription_id
|
||||
|
||||
FROM user_subscriptions
|
||||
|
||||
WHERE
|
||||
user_token = ?
|
||||
AND subscription_namespace = ?
|
||||
AND maybe_stripe_customer_id IS NOT NULL
|
||||
AND maybe_stripe_subscription_id IS NOT NULL
|
||||
AND maybe_stripe_subscription_status IS NOT NULL
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
"#,
|
||||
user_token.as_str(),
|
||||
namespace.to_str(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct RawUserSubscription {
|
||||
token: UserSubscriptionToken,
|
||||
|
||||
user_token: UserToken,
|
||||
|
||||
subscription_namespace: PaymentsNamespace,
|
||||
subscription_product_slug: String,
|
||||
|
||||
maybe_stripe_subscription_status: Option<String>,
|
||||
|
||||
maybe_stripe_customer_id: Option<String>,
|
||||
maybe_stripe_product_id: Option<String>,
|
||||
maybe_stripe_subscription_id: Option<String>,
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod find_possibly_inactive_first_subscription_for_owner_user;
|
||||
pub mod find_subscription_for_owner_user;
|
||||
pub mod get_user_subscription_by_stripe_subscription_id;
|
||||
pub mod list_active_user_subscriptions;
|
||||
|
||||
+44
-14
@@ -9,7 +9,10 @@ use component_traits::traits::internal_user_lookup::InternalUserLookup;
|
||||
use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug;
|
||||
use enums::common::payments_namespace::PaymentsNamespace;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::users::user_subscriptions::find_possibly_inactive_first_subscription_for_owner_user::find_possibly_inactive_first_subscription_for_owner_user_using_connection;
|
||||
use mysql_queries::queries::users::user_subscriptions::find_subscription_for_owner_user::find_subscription_for_owner_user_using_connection;
|
||||
use reusable_types::server_environment::ServerEnvironment;
|
||||
use sqlx::MySqlPool;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
@@ -39,6 +42,7 @@ pub async fn stripe_artcraft_create_subscription_session_handler(
|
||||
server_environment: Data<ServerEnvironment>,
|
||||
internal_user_lookup: Data<dyn InternalUserLookup>,
|
||||
internal_session_cache_purge: Data<dyn InternalSessionCachePurge>,
|
||||
mysql_pool: Data<MySqlPool>,
|
||||
) -> Result<Json<StripeArtcraftCreateSubscriptionCheckoutResponse>, CommonWebError>
|
||||
{
|
||||
let slug = match request.plan {
|
||||
@@ -53,13 +57,21 @@ pub async fn stripe_artcraft_create_subscription_session_handler(
|
||||
|
||||
let plan = get_artcraft_subscription_by_slug_and_env(slug, **server_environment);
|
||||
|
||||
let mut mysql_connection = mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Could not acquire mysql connection: {:?}", err);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let price_id = match cadence {
|
||||
PlanBillingCadence::Monthly => plan.monthly_price_id.clone(),
|
||||
PlanBillingCadence::Yearly => plan.yearly_price_id.clone(),
|
||||
};
|
||||
|
||||
let maybe_user_metadata = internal_user_lookup
|
||||
.lookup_user_from_http_request(&http_request)
|
||||
.lookup_user_from_http_request_and_mysql_connection(&http_request, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error looking up user: {:?}", err);
|
||||
@@ -72,20 +84,37 @@ pub async fn stripe_artcraft_create_subscription_session_handler(
|
||||
Some(user_metadata) => user_metadata,
|
||||
};
|
||||
|
||||
info!("Subscriptions: {:?}", &user_metadata.existing_subscription_keys);
|
||||
// NB: Currently the stripe customer id field in the `users` table is only for FakeYou subscriptions,
|
||||
// so we need to look up any existing Artcraft subscription separately. This is needed to pre-fill
|
||||
// the Stripe billing form.
|
||||
let maybe_active_subscription = find_subscription_for_owner_user_using_connection(
|
||||
&user_metadata.user_token_typed,
|
||||
PaymentsNamespace::Artcraft,
|
||||
&mut mysql_connection
|
||||
).await.map_err(|err| {
|
||||
error!("Error looking up user's ({}) existing subscription: {:?}", &user_metadata.user_token_typed, err);
|
||||
CommonWebError::ServerError // NB: This was probably *our* fault.
|
||||
})?;
|
||||
|
||||
let artcraft_subscriptions = user_metadata
|
||||
.existing_subscription_keys
|
||||
.iter()
|
||||
.filter(|it| it.internal_subscription_namespace == PaymentsNamespace::Artcraft)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// TODO: This will not handle a future where we have multiple "namespaces" or can offer users more than one subscription.
|
||||
// It will actively block users from subscribing to two or more websites.
|
||||
if !artcraft_subscriptions.is_empty() {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("user already has plan".to_string()))
|
||||
if maybe_active_subscription.is_some() {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
"user already has an active subscription plan; use the portal to update the plan"
|
||||
.to_string()))
|
||||
}
|
||||
|
||||
let maybe_inactive_subscription = find_possibly_inactive_first_subscription_for_owner_user_using_connection(
|
||||
&user_metadata.user_token_typed,
|
||||
PaymentsNamespace::Artcraft,
|
||||
&mut mysql_connection
|
||||
).await.map_err(|err| {
|
||||
error!("Error looking up user's ({}) possibly existing inactive subscription: {:?}",
|
||||
&user_metadata.user_token_typed, err);
|
||||
CommonWebError::ServerError // NB: This was probably *our* fault.
|
||||
})?;
|
||||
|
||||
let maybe_existing_inactive_stripe_customer_id = maybe_inactive_subscription.as_ref()
|
||||
.map(|sub| sub.stripe_customer_id.as_str());
|
||||
|
||||
let success_url = stripe_config.checkout_success_url.clone();
|
||||
let cancel_url = stripe_config.checkout_cancel_url.clone();
|
||||
|
||||
@@ -150,8 +179,9 @@ pub async fn stripe_artcraft_create_subscription_session_handler(
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
if let Some(existing_stripe_customer_id) = user_metadata.maybe_existing_stripe_customer_id.as_deref() {
|
||||
match CustomerId::from_str(existing_stripe_customer_id) {
|
||||
if let Some(existing_inactive_stripe_customer_id) = maybe_existing_inactive_stripe_customer_id{
|
||||
info!("Adding existing stripe customer id to checkout session: {}", existing_inactive_stripe_customer_id);
|
||||
match CustomerId::from_str(existing_inactive_stripe_customer_id) {
|
||||
Ok(customer_id) => {
|
||||
checkout_builder = checkout_builder.customer(customer_id);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user