new subscriptions endpoint

This commit is contained in:
Brandon Thomas committed 2025-09-12 01:00:05 -04:00
1 parent 4bb3f6af2d
commit 2e024efc32
11 files changed
+178 -45

No files matched your search

@@ -4,4 +4,5 @@ pub mod generate;
pub mod jobs;
pub mod media_file;
pub mod prompts;
pub mod stripe_artcraft;
pub mod stripe_artcraft;
pub mod subscriptions;
@@ -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<SubscriptionInfo>,
}
#[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,
}
@@ -0,0 +1 @@
pub mod get_session_subscription;
@@ -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<MySql>,
) -> Result<Option<ArtcraftSubscription>, SelectOptionalRecordError> {
) -> Result<Option<UserSubscription>, 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<Option<ArtcraftSubscription>, SelectOptionalRecordError> {
) -> Result<Option<UserSubscription>, 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<Option<RawArtcraftSubscription>, sqlx::Error>) -> Result<Option<ArtcraftSubscription>, SelectOptionalRecordError> {
fn map_result(result: Result<Option<RawUserSubscription>, sqlx::Error>) -> Result<Option<UserSubscription>, 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<Option<RawArtcraftSubscription>, sqlx::Error>) -> R
}
// QueryAs<'q, Postgres, O, PgArguments>
fn query(user_token: &UserToken)
-> QueryMap<impl Send + FnMut(MySqlRow) -> Result<RawArtcraftSubscription, sqlx::Error>>
fn query(user_token: &UserToken, namespace: PaymentsNamespace)
-> QueryMap<impl Send + FnMut(MySqlRow) -> Result<RawUserSubscription, sqlx::Error>>
{
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<String>,
maybe_stripe_subscription_id: Option<String>,
@@ -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;
@@ -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;
@@ -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<GetSessionSubscriptionPathInfo>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GetSessionSubscriptionResponse>, 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,
}),
}))
}
@@ -1 +1,2 @@
pub mod get_session_subscription_handler;
pub mod unsubscribe_reason;
@@ -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<T, B> (app: App<T>) -> App<T>
)
}
// ============= SUBSCRIPTION ROUTES ===================
fn add_subscription_routes<T, B> (app: App<T>) -> App<T>
where
B: MessageBody,
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
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<T,B> (app:App<T>)-> App<T>
where
B: MessageBody,
@@ -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<T, B> (app: App<T>) -> App<T>
where
B: MessageBody,
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
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()))
)
)
}
@@ -14,4 +14,5 @@ mod tag_routes;
mod user_routes;
mod weights_routes;
mod workflow_routes;
pub mod add_routes;
pub mod add_routes;
mod add_subscription_routes;