Compare commits

...
Author SHA1 Message Date
Brandon Thomas 27f465cb40 stripe info admin endpoint 2026-06-10 08:41:21 -04:00
22 changed files with 439 additions and 2 deletions
@@ -0,0 +1,36 @@
{
"db_name": "MySQL",
"query": "\nSELECT DISTINCT\n subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n maybe_stripe_customer_id as `stripe_customer_id!`\n\nFROM user_subscriptions\n\nWHERE\n user_token = ?\n AND maybe_stripe_customer_id IS NOT NULL\n AND deleted_at IS NULL\nORDER BY subscription_namespace ASC, maybe_stripe_customer_id ASC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "subscription_namespace: enums::common::payments_namespace::PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "stripe_customer_id!",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 1020
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true
]
},
"hash": "516db34b6a642a2f7d7107f186905a79ad694c3c86f3078efc77bcaae7f39f3d"
}
@@ -0,0 +1,47 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n user_token as `user_token: tokens::tokens::users::UserToken`,\n payments_namespace as `payments_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n stripe_customer_id\n\nFROM user_stripe_customer_links\n\nWHERE user_token = ?\nORDER BY id ASC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "payments_namespace: enums::common::payments_namespace::PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "stripe_customer_id",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
},
"hash": "ffb5717340f21f21693bfcade201bc055fb3fb8a26da8d0574e3de44b033586c"
}
@@ -2,5 +2,6 @@ pub mod alerts;
pub mod jobs;
pub mod user;
pub mod user_referrals;
pub mod user_stripe_data;
pub mod wallet_ledger_entries;
pub mod wallets;
@@ -0,0 +1 @@
pub mod moderator_get_user_stripe_customer_ids;
@@ -0,0 +1,40 @@
use enums::common::payments_namespace::PaymentsNamespace;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
use utoipa::ToSchema;
pub const MODERATOR_GET_USER_STRIPE_CUSTOMER_IDS_PATH: &str =
"/v1/moderation/user_stripe_data/{user_token}/customer_ids";
#[derive(Deserialize, ToSchema)]
pub struct ModeratorGetUserStripeCustomerIdsPathInfo {
pub user_token: UserToken,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorGetUserStripeCustomerIdsResponse {
pub success: bool,
/// All stripe customer ids we have on file for the user, across payments
/// namespaces. A user may have zero or several: a customer link and/or
/// subscriptions per namespace (eg. artcraft), plus legacy fakeyou
/// subscriptions. The same customer id can appear once per source.
pub customer_ids: Vec<ModeratorUserStripeCustomerIdEntry>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorUserStripeCustomerIdEntry {
pub stripe_customer_id: String,
pub payments_namespace: PaymentsNamespace,
pub source: ModeratorStripeCustomerIdSource,
}
/// Which table the customer id was found in.
#[derive(Serialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum ModeratorStripeCustomerIdSource {
/// From the `user_stripe_customer_links` table (1:1 per namespace).
CustomerLink,
/// From the user's rows in the `user_subscriptions` table.
Subscription,
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,25 @@
{
"type": "json",
"current_status": "completed",
"percentage_complete": 100,
"job_id": "c1bed796-3f05-4716-8ef0-a4d4900dfc6e",
"elapsed": null,
"prompt": "a dog typing on a keyboard",
"confirmation": "Drew a dog typing",
"full_prompt": "a dog typing on a keyboard",
"flags": null,
"is_alteration": false,
"section_id": 0,
"input_type": "text",
"request_id": "fc39cefd-fcf9-4775-b490-7e17ce670060",
"asr_transcript": null,
"asr_accepted": null,
"seed": null,
"image_reference": null,
"is_image_edit": null,
"moderated": false,
"r_rated": false,
"order": 1,
"model_name": "imagine_h_1",
"image_id": "78b3e6ef-defc-41a6-a9ac-b7a370d26ce5"
}
@@ -0,0 +1,25 @@
{
"type": "json",
"current_status": "start_stage",
"percentage_complete": 0,
"job_id": "57baeffe-d5cf-483b-a5bb-7794899f905c",
"elapsed": null,
"prompt": "a dog typing on a keyboard",
"confirmation": "Drew a dog typing",
"full_prompt": "a dog typing on a keyboard",
"flags": null,
"is_alteration": true,
"section_id": 0,
"input_type": "text",
"request_id": "fc39cefd-fcf9-4775-b490-7e17ce670060",
"asr_transcript": null,
"asr_accepted": null,
"seed": null,
"image_reference": null,
"is_image_edit": null,
"moderated": false,
"r_rated": null,
"order": 7,
"model_name": "imagine_h_1",
"image_id": null
}
@@ -0,0 +1,21 @@
{
"type": "conversation.item.create",
"timestamp": 1761010203630,
"item": {
"type": "message",
"content": [
{
"requestId": "fc39cefd-fcf9-4775-b490-7e17ce670060",
"text": "a dog typing on a keyboard",
"type": "input_text",
"properties": {
"section_count": 0,
"is_kids_mode": false,
"enable_nsfw": true,
"skip_upsampler": false,
"is_initial": false
}
}
]
}
}
@@ -0,0 +1,4 @@
{
"type": "other_type",
"foo": "bar"
}
@@ -0,0 +1 @@
{"type":"json","current_status":"start_stage","percentage_complete":0.0,"job_id":"0080eaac-f7f7-4369-90a2-346491939c1b","elapsed":null,"prompt":"a dog riding a motorcycle","confirmation":"Drew a dog riding a motorcycle","full_prompt":"a dog riding a motorcycle","flags":null,"is_alteration":false,"section_id":0,"input_type":"text","request_id":"bbc15402-3257-4415-922e-b156c151f3c2","asr_transcript":null,"asr_accepted":null,"seed":null,"image_reference":null,"is_image_edit":null,"moderated":false,"r_rated":null,"order":0,"model_name":"imagine_h_1","image_id":null}
@@ -0,0 +1,25 @@
{
"type": "json",
"current_status": "start_stage",
"percentage_complete": 0,
"job_id": "0080eaac-f7f7-4369-90a2-346491939c1b",
"elapsed": null,
"prompt": "a dog riding a motorcycle",
"confirmation": "Drew a dog riding a motorcycle",
"full_prompt": "a dog riding a motorcycle",
"flags": null,
"is_alteration": false,
"section_id": 0,
"input_type": "text",
"request_id": "bbc15402-3257-4415-922e-b156c151f3c2",
"asr_transcript": null,
"asr_accepted": null,
"seed": null,
"image_reference": null,
"is_image_edit": null,
"moderated": false,
"r_rated": null,
"order": 0,
"model_name": "imagine_h_1",
"image_id": null
}
@@ -0,0 +1,50 @@
use sqlx::pool::PoolConnection;
use sqlx::MySql;
use enums::common::payments_namespace::PaymentsNamespace;
use tokens::tokens::users::UserToken;
use crate::queries::users::user_stripe_customer_links::find_user_stripe_customer_link::UserStripeCustomerLink;
/// List every stripe customer link for a user, across all payments namespaces.
/// Users have at most one link per namespace.
pub async fn list_user_stripe_customer_links_for_user(
user_token: &UserToken,
connection: &mut PoolConnection<MySql>,
) -> Result<Vec<UserStripeCustomerLink>, sqlx::Error> {
let records = sqlx::query_as!(
RawUserStripeCustomerLink,
r#"
SELECT
user_token as `user_token: tokens::tokens::users::UserToken`,
payments_namespace as `payments_namespace: enums::common::payments_namespace::PaymentsNamespace`,
stripe_customer_id
FROM user_stripe_customer_links
WHERE user_token = ?
ORDER BY id ASC
"#,
user_token.as_str(),
)
.fetch_all(&mut **connection)
.await?;
let links = records
.into_iter()
.map(|record| UserStripeCustomerLink {
user_token: record.user_token,
payments_namespace: record.payments_namespace,
stripe_customer_id: record.stripe_customer_id,
})
.collect();
Ok(links)
}
#[derive(sqlx::FromRow)]
struct RawUserStripeCustomerLink {
user_token: UserToken,
payments_namespace: PaymentsNamespace,
stripe_customer_id: String,
}
@@ -1,2 +1,3 @@
pub mod find_user_stripe_customer_link;
pub mod upsert_user_stripe_customer_link;
pub mod list_user_stripe_customer_links_for_user;
pub mod upsert_user_stripe_customer_link;
@@ -0,0 +1,40 @@
use sqlx::pool::PoolConnection;
use sqlx::MySql;
use enums::common::payments_namespace::PaymentsNamespace;
use tokens::tokens::users::UserToken;
/// A distinct stripe customer id attached to one of the user's subscriptions.
pub struct UserSubscriptionStripeCustomerId {
pub subscription_namespace: PaymentsNamespace,
pub stripe_customer_id: String,
}
/// List the distinct stripe customer ids across all of a user's subscriptions
/// (current and historical), across all payments namespaces.
pub async fn list_stripe_customer_ids_for_user_subscriptions(
user_token: &UserToken,
connection: &mut PoolConnection<MySql>,
) -> Result<Vec<UserSubscriptionStripeCustomerId>, sqlx::Error> {
let records = sqlx::query_as!(
UserSubscriptionStripeCustomerId,
r#"
SELECT DISTINCT
subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,
maybe_stripe_customer_id as `stripe_customer_id!`
FROM user_subscriptions
WHERE
user_token = ?
AND maybe_stripe_customer_id IS NOT NULL
AND deleted_at IS NULL
ORDER BY subscription_namespace ASC, maybe_stripe_customer_id ASC
"#,
user_token.as_str(),
)
.fetch_all(&mut **connection)
.await?;
Ok(records)
}
@@ -1,7 +1,8 @@
pub mod find_possibly_inactive_first_subscription_for_owner_user;
pub mod lookup_users_by_stripe_customer_id;
pub mod find_subscription_for_owner_user;
pub mod get_user_subscription_by_stripe_subscription_id;
pub mod list_active_user_subscriptions;
pub mod list_stripe_customer_ids_for_user_subscriptions;
pub mod lookup_users_by_stripe_customer_id;
pub mod upsert_user_subscription_by_stripe_id;
pub mod upsert_user_subscription_with_invoice_paid_status_by_stripe_id;
@@ -117,6 +117,7 @@ use artcraft_api_defs::folders::media_files::*;
use artcraft_api_defs::folders::subfolder::*;
use artcraft_api_defs::moderation::user_referrals::list_global_user_referrals::*;
use artcraft_api_defs::moderation::user_referrals::list_user_referrals_for_user::*;
use artcraft_api_defs::moderation::user_stripe_data::moderator_get_user_stripe_customer_ids::*;
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::*;
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::*;
use crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::*;
@@ -558,6 +559,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::moderation::user_bans::moderation_ban_user_handler::moderation_ban_user_handler,
crate::http_server::endpoints::moderation::user_emails::moderator_change_user_email_handler::moderator_change_user_email_handler,
crate::http_server::endpoints::moderation::user_emails::moderator_list_email_address_changes_for_user_handler::moderator_list_email_address_changes_for_user_handler,
crate::http_server::endpoints::moderation::user_stripe_data::moderator_get_user_stripe_customer_ids_handler::moderator_get_user_stripe_customer_ids_handler,
// Folders
crate::http_server::endpoints::folders::folder::create_folder_handler::create_folder_handler,
@@ -816,6 +818,10 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
ModeratorGetWalletLedgerEntryPathInfo,
ModeratorGetWalletLedgerEntryResponse,
ModeratorGetWalletLedgerEntryDetails,
ModeratorGetUserStripeCustomerIdsPathInfo,
ModeratorGetUserStripeCustomerIdsResponse,
ModeratorUserStripeCustomerIdEntry,
ModeratorStripeCustomerIdSource,
GptImage1EditImageRequest,
GptImage1EditImageImageSize,
GptImage1EditImageNumImages,
@@ -10,5 +10,6 @@ pub mod user_emails;
pub mod user_feature_flags;
pub mod user_referrals;
pub mod user_sessions;
pub mod user_stripe_data;
pub mod wallet_ledger_entries;
pub mod wallets;
@@ -0,0 +1 @@
pub mod moderator_get_user_stripe_customer_ids_handler;
@@ -0,0 +1,85 @@
use std::sync::Arc;
use actix_web::web::{Json, Path};
use actix_web::{web, HttpRequest};
use log::warn;
use artcraft_api_defs::moderation::user_stripe_data::moderator_get_user_stripe_customer_ids::{
ModeratorGetUserStripeCustomerIdsPathInfo,
ModeratorGetUserStripeCustomerIdsResponse,
ModeratorStripeCustomerIdSource,
ModeratorUserStripeCustomerIdEntry,
};
use mysql_queries::queries::users::user_stripe_customer_links::list_user_stripe_customer_links_for_user::list_user_stripe_customer_links_for_user;
use mysql_queries::queries::users::user_subscriptions::list_stripe_customer_ids_for_user_subscriptions::list_stripe_customer_ids_for_user_subscriptions;
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, UseDatabase};
use crate::state::server_state::ServerState;
/// Get all stripe customer ids on file for a user (moderation)
#[utoipa::path(
get,
tag = "Moderation",
path = "/v1/moderation/user_stripe_data/{user_token}/customer_ids",
responses(
(status = 200, description = "Success", body = ModeratorGetUserStripeCustomerIdsResponse),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Server error"),
),
params(
("user_token" = UserToken, Path, description = "User token to look up"),
)
)]
pub async fn moderator_get_user_stripe_customer_ids_handler(
http_request: HttpRequest,
path: Path<ModeratorGetUserStripeCustomerIdsPathInfo>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ModeratorGetUserStripeCustomerIdsResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let _moderator_session = require_moderator(
&http_request,
&server_state,
UseDatabase::FromPool(&mut mysql_connection),
)
.await
.map_err(|_| CommonWebError::NotAuthorized)?;
let customer_links = list_user_stripe_customer_links_for_user(&path.user_token, &mut mysql_connection)
.await
.map_err(|err| {
warn!("moderator_get_user_stripe_customer_ids customer link query error: {:?}", err);
CommonWebError::from_error(err)
})?;
let subscription_customer_ids = list_stripe_customer_ids_for_user_subscriptions(&path.user_token, &mut mysql_connection)
.await
.map_err(|err| {
warn!("moderator_get_user_stripe_customer_ids subscription query error: {:?}", err);
CommonWebError::from_error(err)
})?;
let mut customer_ids = Vec::with_capacity(customer_links.len() + subscription_customer_ids.len());
customer_ids.extend(customer_links.into_iter().map(|link| ModeratorUserStripeCustomerIdEntry {
stripe_customer_id: link.stripe_customer_id,
payments_namespace: link.payments_namespace,
source: ModeratorStripeCustomerIdSource::CustomerLink,
}));
customer_ids.extend(subscription_customer_ids.into_iter().map(|subscription| ModeratorUserStripeCustomerIdEntry {
stripe_customer_id: subscription.stripe_customer_id,
payments_namespace: subscription.subscription_namespace,
source: ModeratorStripeCustomerIdSource::Subscription,
}));
Ok(Json(ModeratorGetUserStripeCustomerIdsResponse {
success: true,
customer_ids,
}))
}
@@ -46,6 +46,7 @@ use crate::http_server::endpoints::moderation::user_referrals::moderator_list_gl
use crate::http_server::endpoints::moderation::user_referrals::moderator_list_user_referrals_for_user_handler::moderator_list_user_referrals_for_user_handler;
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::moderator_list_user_session_impersonation_requests_handler;
use crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::moderator_user_session_impersonation_request_handler;
use crate::http_server::endpoints::moderation::user_stripe_data::moderator_get_user_stripe_customer_ids_handler::moderator_get_user_stripe_customer_ids_handler;
pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
where
@@ -171,6 +172,12 @@ pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
.route(web::head().to(|| HttpResponse::Ok()))
)
)
.service(web::scope("/user_stripe_data")
.service(web::resource("/{user_token}/customer_ids")
.route(web::get().to(moderator_get_user_stripe_customer_ids_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
.service(web::resource("/wallet_ledger_entry/{wallet_ledger_entry_token}")
.route(web::get().to(moderator_get_wallet_ledger_entry_handler))
.route(web::head().to(|| HttpResponse::Ok()))