From 2e024efc32cb953d33502b7e3211dd07fab68a9b Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 12 Sep 2025 00:27:23 -0400 Subject: [PATCH] new subscriptions endpoint --- .../api_clients/artcraft_api_defs/src/lib.rs | 3 +- .../subscriptions/get_session_subscription.rs | 24 +++++ .../src/subscriptions/mod.rs | 1 + ...rs => find_subscription_for_owner_user.rs} | 46 +++++----- .../queries/users/user_subscriptions/mod.rs | 2 +- ..._create_customer_portal_session_handler.rs | 5 +- .../get_session_subscription_handler.rs | 88 +++++++++++++++++++ .../endpoints/subscriptions/mod.rs | 1 + .../src/http_server/routes/add_routes.rs | 21 +---- .../routes/add_subscription_routes.rs | 29 ++++++ .../src/http_server/routes/mod.rs | 3 +- 11 files changed, 178 insertions(+), 45 deletions(-) create mode 100644 crates/api_clients/artcraft_api_defs/src/subscriptions/get_session_subscription.rs create mode 100644 crates/api_clients/artcraft_api_defs/src/subscriptions/mod.rs rename crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/{find_artcraft_subscription_for_owner_user.rs => find_subscription_for_owner_user.rs} (77%) create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/get_session_subscription_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/routes/add_subscription_routes.rs diff --git a/crates/api_clients/artcraft_api_defs/src/lib.rs b/crates/api_clients/artcraft_api_defs/src/lib.rs index 7885c1a710..932f093233 100644 --- a/crates/api_clients/artcraft_api_defs/src/lib.rs +++ b/crates/api_clients/artcraft_api_defs/src/lib.rs @@ -4,4 +4,5 @@ pub mod generate; pub mod jobs; pub mod media_file; pub mod prompts; -pub mod stripe_artcraft; \ No newline at end of file +pub mod stripe_artcraft; +pub mod subscriptions; \ No newline at end of file diff --git a/crates/api_clients/artcraft_api_defs/src/subscriptions/get_session_subscription.rs b/crates/api_clients/artcraft_api_defs/src/subscriptions/get_session_subscription.rs new file mode 100644 index 0000000000..3cf348b7b8 --- /dev/null +++ b/crates/api_clients/artcraft_api_defs/src/subscriptions/get_session_subscription.rs @@ -0,0 +1,24 @@ +use enums::common::payments_namespace::PaymentsNamespace; +use serde_derive::{Deserialize, Serialize}; +use tokens::tokens::user_subscriptions::UserSubscriptionToken; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct GetSessionSubscriptionResponse { + pub success: bool, + + /// Information about the user's subscription, if any. + pub active_subscription: Option, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct SubscriptionInfo { + /// Unique token for the subscription. + pub subscription_token: UserSubscriptionToken, + + /// Namespace for the subscription. + pub namespace: PaymentsNamespace, + + /// Local key for the subscription. + pub product_slug: String, +} diff --git a/crates/api_clients/artcraft_api_defs/src/subscriptions/mod.rs b/crates/api_clients/artcraft_api_defs/src/subscriptions/mod.rs new file mode 100644 index 0000000000..1cc61e22e6 --- /dev/null +++ b/crates/api_clients/artcraft_api_defs/src/subscriptions/mod.rs @@ -0,0 +1 @@ +pub mod get_session_subscription; \ No newline at end of file diff --git a/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/find_artcraft_subscription_for_owner_user.rs b/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/find_subscription_for_owner_user.rs similarity index 77% rename from crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/find_artcraft_subscription_for_owner_user.rs rename to crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/find_subscription_for_owner_user.rs index 5dd23aa6bf..c57ad38460 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/find_artcraft_subscription_for_owner_user.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/find_subscription_for_owner_user.rs @@ -2,19 +2,21 @@ use crate::errors::select_optional_record_error::SelectOptionalRecordError; use crate::helpers::boolean_converters::nullable_i8_to_bool_default_false; use crate::types::query_map::QueryMap; use chrono::{DateTime, Utc}; -use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug; 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 ArtcraftSubscription { +pub struct UserSubscription { + pub token: UserSubscriptionToken, + pub user_token: UserToken, pub subscription_namespace: PaymentsNamespace, - pub subscription_product_slug: ArtcraftSubscriptionSlug, + pub subscription_product_slug: String, pub stripe_customer_id: String, pub stripe_subscription_id: String, @@ -33,12 +35,13 @@ pub struct ArtcraftSubscription { /// 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_artcraft_subscription_for_owner_user_using_connection( +pub async fn find_subscription_for_owner_user_using_connection( user_token: &UserToken, + namespace: PaymentsNamespace, connection: &mut PoolConnection, -) -> Result, SelectOptionalRecordError> { +) -> Result, SelectOptionalRecordError> { - let query = query(user_token); + let query = query(user_token, namespace); let result = query .fetch_optional(&mut **connection) @@ -48,12 +51,13 @@ pub async fn find_artcraft_subscription_for_owner_user_using_connection( } -pub async fn find_artcraft_subscription_for_owner_user_using_transaction( +pub async fn find_subscription_for_owner_user_using_transaction( user_token: &UserToken, + namespace: PaymentsNamespace, transaction: &mut sqlx::Transaction<'_, MySql>, -) -> Result, SelectOptionalRecordError> { +) -> Result, SelectOptionalRecordError> { - let query = query(user_token); + let query = query(user_token, namespace); let result = query .fetch_optional(&mut **transaction) @@ -62,9 +66,10 @@ pub async fn find_artcraft_subscription_for_owner_user_using_transaction( map_result(result) } -fn map_result(result: Result, sqlx::Error>) -> Result, SelectOptionalRecordError> { +fn map_result(result: Result, sqlx::Error>) -> Result, SelectOptionalRecordError> { match result { - Ok(Some(record)) => Ok(Some(ArtcraftSubscription { + Ok(Some(record)) => Ok(Some(UserSubscription { + token: record.token, user_token: record.user_token, subscription_namespace: record.subscription_namespace, subscription_product_slug: record.subscription_product_slug, @@ -84,22 +89,21 @@ fn map_result(result: Result, sqlx::Error>) -> R } // QueryAs<'q, Postgres, O, PgArguments> -fn query(user_token: &UserToken) - -> QueryMap Result> +fn query(user_token: &UserToken, namespace: PaymentsNamespace) + -> QueryMap Result> { - const ARTCRAFT_NAMESPACE: &str = PaymentsNamespace::Artcraft.to_str(); - // 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!( - RawArtcraftSubscription, + 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 as `subscription_product_slug: enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug`, + subscription_product_slug, maybe_stripe_customer_id, maybe_stripe_subscription_id, maybe_stripe_subscription_status, @@ -121,17 +125,19 @@ WHERE LIMIT 1 "#, user_token.as_str(), - ARTCRAFT_NAMESPACE, + namespace.to_str(), ) } #[derive(sqlx::FromRow)] -struct RawArtcraftSubscription { +struct RawUserSubscription { + token: UserSubscriptionToken, + user_token: UserToken, subscription_namespace: PaymentsNamespace, - subscription_product_slug: ArtcraftSubscriptionSlug, + subscription_product_slug: String, maybe_stripe_customer_id: Option, maybe_stripe_subscription_id: Option, diff --git a/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/mod.rs b/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/mod.rs index 4dae909cfc..d8d520e62e 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/mod.rs @@ -1,4 +1,4 @@ -pub mod find_artcraft_subscription_for_owner_user; +pub mod find_subscription_for_owner_user; pub mod get_user_subscription_by_stripe_subscription_id; pub mod get_user_subscription_by_stripe_subscription_id_transactional; pub mod list_active_user_subscriptions; diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/stripe_artcraft_create_customer_portal_session_handler.rs b/crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/stripe_artcraft_create_customer_portal_session_handler.rs index 4971fddb15..b84744e4ff 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/stripe_artcraft_create_customer_portal_session_handler.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/customer_portal/stripe_artcraft_create_customer_portal_session_handler.rs @@ -11,7 +11,7 @@ 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_artcraft_subscription_for_owner_user::find_artcraft_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; @@ -72,8 +72,9 @@ pub async fn stripe_artcraft_create_customer_portal_session_handler( let user_token = UserToken::new_from_str(&user_metadata.user_token); - let result = find_artcraft_subscription_for_owner_user_using_connection( + let result = find_subscription_for_owner_user_using_connection( &user_token, + PaymentsNamespace::Artcraft, &mut mysql_connection ).await; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/get_session_subscription_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/get_session_subscription_handler.rs new file mode 100644 index 0000000000..4d78e9c19e --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/get_session_subscription_handler.rs @@ -0,0 +1,88 @@ +use std::fmt; +use std::sync::Arc; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder; +use crate::state::server_state::ServerState; +use actix_web::web::{Json, Path}; +use actix_web::{web, HttpMessage, HttpRequest, HttpResponse}; +use artcraft_api_defs::credits::get_session_credits::GetSessionCreditsResponse; +use artcraft_api_defs::subscriptions::get_session_subscription::{GetSessionSubscriptionResponse, SubscriptionInfo}; +use chrono::{DateTime, Utc}; +use enums::common::payments_namespace::PaymentsNamespace; +use log::{error, warn}; +use tokens::tokens::media_files::MediaFileToken; +use tokens::tokens::prompts::PromptToken; +use utoipa::ToSchema; +use mysql_queries::queries::users::user_subscriptions::find_subscription_for_owner_user::find_subscription_for_owner_user_using_connection; + +/// For the URL PathInfo +#[derive(Deserialize, ToSchema)] +pub struct GetSessionSubscriptionPathInfo { + namespace: PaymentsNamespace, +} + +/// Get subscription for the payment namespace. +/// +/// Use this instead of the older endpoints, +/// +/// - GET /v1/billing/active_subscription (legacy, for FakeYou) +/// - GET /v1/app_state (quite a lot of FakeYou state) +/// +#[utoipa::path( + get, + tag = "Subscriptions", + path = "/v1/subscriptions/namespace/{namespace}", + responses( + (status = 200, description = "Success", body = GetSessionCreditsResponse), + ), + params( + ("path" = GetSessionSubscriptionPathInfo, description = "Path for Request") + ) +)] +pub async fn get_session_subscription_handler( + http_request: HttpRequest, + path: Path, + server_state: web::Data> +) -> Result, CommonWebError> +{ + let mut mysql_connection = server_state.mysql_pool + .acquire() + .await + .map_err(|err| { + error!("Error acquiring MySQL connection: {:?}", err); + CommonWebError::ServerError + })?; + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session_from_connection(&http_request, &mut mysql_connection) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + CommonWebError::ServerError + })?; + + let user_token = match maybe_user_session { + Some(session) => session.user_token, + None => return Err(CommonWebError::NotAuthorized), + }; + + let maybe_subscription = find_subscription_for_owner_user_using_connection( + &user_token, + path.namespace, + &mut mysql_connection + ).await.map_err(|err| { + error!("Error looking up user's ({}) existing subscription: {:?}", &user_token, err); + CommonWebError::ServerError // NB: This was probably *our* fault. + })?; + + Ok(Json(GetSessionSubscriptionResponse { + success: true, + active_subscription: maybe_subscription.map(|sub| SubscriptionInfo { + namespace: sub.subscription_namespace, + product_slug: sub.subscription_product_slug, + subscription_token: sub.token, + }), + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/mod.rs index ff2380c54d..9972ef2227 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/mod.rs @@ -1 +1,2 @@ +pub mod get_session_subscription_handler; pub mod unsubscribe_reason; diff --git a/crates/service/web/storyteller_web/src/http_server/routes/add_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/add_routes.rs index 3748aa3db6..cfc9de0db3 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/add_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/add_routes.rs @@ -130,6 +130,7 @@ use crate::http_server::routes::add_generate_routes::add_generate_routes; use crate::http_server::routes::add_image_studio_routes::add_image_studio_routes; use crate::http_server::routes::add_stripe_artcraft_routes::add_stripe_artcraft_routes; use crate::http_server::routes::add_studio_gen2_routes::add_studio_gen2_routes; +use crate::http_server::routes::add_subscription_routes::add_subscription_routes; use crate::http_server::routes::add_webhook_routes::add_webhook_routes; use crate::http_server::routes::beta_key_routes::add_beta_key_routes; use crate::http_server::routes::job_routes::add_job_routes; @@ -893,26 +894,6 @@ fn add_featured_item_routes (app: App) -> App ) } -// ============= SUBSCRIPTION ROUTES =================== -fn add_subscription_routes (app: App) -> App - where - B: MessageBody, - T: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - InitError = (), - >, -{ - app.service(web::scope("/v1/subscriptions") - .service(web::resource("/unsubscribe_reason") - .route(web::post().to(set_unsubscribe_reason_handler)) - .route(web::head().to(|| HttpResponse::Ok())) - ) - ) -} - fn add_voice_designer_routes (app:App)-> App where B: MessageBody, diff --git a/crates/service/web/storyteller_web/src/http_server/routes/add_subscription_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/add_subscription_routes.rs new file mode 100644 index 0000000000..7a0f110638 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/routes/add_subscription_routes.rs @@ -0,0 +1,29 @@ +use crate::http_server::endpoints::subscriptions::get_session_subscription_handler::get_session_subscription_handler; +use crate::http_server::endpoints::subscriptions::unsubscribe_reason::set_unsubscribe_reason_handler; +use actix_http::body::MessageBody; +use actix_service::ServiceFactory; +use actix_web::dev::{ServiceRequest, ServiceResponse}; +use actix_web::{web, App, Error, HttpResponse}; + +pub fn add_subscription_routes (app: App) -> App +where + B: MessageBody, + T: ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = Error, + InitError = (), + >, +{ + app.service(web::scope("/v1/subscriptions") + .service(web::resource("/unsubscribe_reason") + .route(web::post().to(set_unsubscribe_reason_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/namespace/{namespace}") + .route(web::post().to(get_session_subscription_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + ) +} diff --git a/crates/service/web/storyteller_web/src/http_server/routes/mod.rs b/crates/service/web/storyteller_web/src/http_server/routes/mod.rs index ef07a16865..a3cd885164 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/mod.rs @@ -14,4 +14,5 @@ mod tag_routes; mod user_routes; mod weights_routes; mod workflow_routes; -pub mod add_routes; \ No newline at end of file +pub mod add_routes; +mod add_subscription_routes; \ No newline at end of file