add: coin purchases + donator badge

This commit is contained in:
trisua 2025-08-08 13:25:47 -04:00
parent fd529d3847
commit 44f9edd67e
21 changed files with 345 additions and 38 deletions

View file

@ -1,13 +1,20 @@
use std::time::Duration;
use axum::{http::HeaderMap, response::IntoResponse, Extension, Json};
use std::{str::FromStr, time::Duration};
use axum::{
extract::Query,
http::HeaderMap,
response::{IntoResponse, Redirect},
Extension, Json,
};
use tetratto_core::model::{
auth::{Notification, User},
economy::{CoinTransfer, CoinTransferMethod},
moderation::AuditLogEntry,
permissions::{FinePermission, SecondaryPermission},
ApiReturn, Error,
};
use stripe::{EventObject, EventType};
use crate::State;
use crate::{get_user_from_token, State, cookie::CookieJar};
use serde::Deserialize;
pub async fn stripe_webhook(
Extension(data): Extension<State>,
@ -131,7 +138,7 @@ pub async fn stripe_webhook(
if let Err(e) = data
.create_audit_log_entry(AuditLogEntry::new(
0,
format!("invoice tier update failed: stripe {customer_id}"),
format!("invoice user update failed: stripe {customer_id}"),
))
.await
{
@ -469,3 +476,202 @@ pub async fn stripe_webhook(
payload: (),
})
}
#[derive(Deserialize)]
pub enum ProductIDAlias {
Coins100,
Coins400,
}
#[derive(Deserialize)]
pub struct CreateCheckoutSessionProps {
pub product: ProductIDAlias,
}
pub async fn create_stupid_fucking_checkout_session(
jar: CookieJar,
Extension(data): Extension<State>,
Json(props): Json<CreateCheckoutSessionProps>,
) -> impl IntoResponse {
let data = &(data.read().await);
let user = match get_user_from_token!(jar, data.0) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
let stripe_cnf = match data.0.0.0.stripe {
Some(ref c) => c,
None => return Json(Error::MiscError("Disabled".to_string()).into()),
};
let stripe_client = match data.3 {
Some(ref x) => x,
None => return Json(Error::MiscError("Disabled".to_string()).into()),
};
let session = match stripe::CheckoutSession::create(
&stripe_client,
stripe::CreateCheckoutSession {
customer_creation: if user.stripe_id.is_empty() {
Some(stripe::CheckoutSessionCustomerCreation::Always)
} else {
None
},
customer: if user.stripe_id.is_empty() {
None
} else {
Some(stripe::CustomerId::from_str(&user.stripe_id).unwrap())
},
line_items: Some(vec![stripe::CreateCheckoutSessionLineItems {
quantity: Some(1),
adjustable_quantity: Some(
stripe::CreateCheckoutSessionLineItemsAdjustableQuantity {
enabled: false,
..Default::default()
},
),
price: Some(match props.product {
ProductIDAlias::Coins100 => stripe_cnf.price_ids.coins_100.clone(),
ProductIDAlias::Coins400 => stripe_cnf.price_ids.coins_400.clone(),
}),
..Default::default()
}]),
client_reference_id: Some(&user.id.to_string()),
mode: Some(stripe::CheckoutSessionMode::Payment),
ui_mode: Some(stripe::CheckoutSessionUiMode::Hosted),
success_url: Some(&format!(
"{}/api/v1/service_hooks/stripe/checkout/success?session_id={{CHECKOUT_SESSION_ID}}",
data.0.0.0.host
)),
..Default::default()
},
)
.await
{
Ok(x) => x,
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
};
Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: session.url.unwrap(),
})
}
#[derive(Deserialize)]
pub struct CheckoutSessionSuccessProps {
pub session_id: String,
}
/// By this point, we can assume the customer has properly paid.
///
/// This endpoint will just read the purchase, apply the purchase, and then redirect home.
pub async fn handle_stupid_fucking_checkout_success_session(
jar: CookieJar,
Extension(data): Extension<State>,
Query(props): Query<CheckoutSessionSuccessProps>,
) -> std::result::Result<impl IntoResponse, Json<ApiReturn<()>>> {
let data = &(data.read().await);
let mut user = match get_user_from_token!(jar, data.0) {
Some(ua) => ua,
None => return Err(Json(Error::NotAllowed.into())),
};
if user.checkouts.contains(&props.session_id) {
return Err(Json(
Error::MiscError("You can only do this once".to_string()).into(),
));
}
let stripe_cnf = match data.0.0.0.stripe {
Some(ref c) => c,
None => return Err(Json(Error::MiscError("Disabled".to_string()).into())),
};
let stripe_client = match data.3 {
Some(ref x) => x,
None => return Err(Json(Error::MiscError("Disabled".to_string()).into())),
};
let session = match stripe::CheckoutSession::retrieve(
&stripe_client,
&match stripe::CheckoutSessionId::from_str(&props.session_id) {
Ok(x) => x,
Err(_) => {
return Err(Json(
Error::MiscError("Invalid session ID".to_string()).into(),
));
}
},
&[&"line_items"],
)
.await
{
Ok(x) => x,
Err(e) => return Err(Json(Error::MiscError(e.to_string()).into())),
};
let price_id = session
.line_items
.as_ref()
.unwrap()
.data
.get(0)
.as_ref()
.unwrap()
.price
.as_ref()
.unwrap()
.id
.to_string();
if price_id == stripe_cnf.price_ids.coins_100 {
if let Err(e) = data
.0
.create_transfer(
&mut CoinTransfer::new(
data.0.0.0.system_user,
user.id,
100,
CoinTransferMethod::Transfer,
),
true,
)
.await
{
return Err(Json(e.into()));
}
} else if price_id == stripe_cnf.price_ids.coins_400 {
if let Err(e) = data
.0
.create_transfer(
&mut CoinTransfer::new(
data.0.0.0.system_user,
user.id,
400,
CoinTransferMethod::Transfer,
),
true,
)
.await
{
return Err(Json(e.into()));
}
} else {
tracing::error!(
"received an invalid stripe price id, please check config.stripe.price_ids"
);
return Err(Json(
Error::MiscError("Unknown price ID".to_string()).into(),
));
}
user.checkouts.push(props.session_id);
if let Err(e) = data.0.update_user_checkouts(user.id, user.checkouts).await {
return Err(Json(e.into()));
}
Ok(Redirect::to("/wallet"))
}

View file

@ -563,6 +563,14 @@ pub fn routes() -> Router {
"/service_hooks/stripe",
post(auth::connections::stripe::stripe_webhook),
)
.route(
"/service_hooks/stripe/checkout",
post(auth::connections::stripe::create_stupid_fucking_checkout_session),
)
.route(
"/service_hooks/stripe/checkout/success",
get(auth::connections::stripe::handle_stupid_fucking_checkout_success_session),
)
// channels
.route("/channels", post(channels::channels::create_request))
.route(

View file

@ -152,7 +152,7 @@ pub async fn update_price_request(
if req.price < 25 {
return Json(
Error::MiscError(
"Price is too low, please a price of use 25 coins or more".to_string(),
"Price is too low, please use a price of 25 coins or more".to_string(),
)
.into(),
);