add: stripe integration

This commit is contained in:
trisua 2025-05-05 19:38:01 -04:00
parent 2fa5a4dc1f
commit 1d120555a0
31 changed files with 1137 additions and 122 deletions

View file

@ -1,5 +1,6 @@
pub mod last_fm;
pub mod spotify;
pub mod stripe;
use std::collections::HashMap;

View file

@ -0,0 +1,148 @@
use std::time::Duration;
use axum::{http::HeaderMap, response::IntoResponse, Extension, Json};
use tetratto_core::model::{auth::Notification, permissions::FinePermission, ApiReturn, Error};
use stripe::{EventObject, EventType};
use crate::State;
pub async fn stripe_webhook(
Extension(data): Extension<State>,
headers: HeaderMap,
body: String,
) -> impl IntoResponse {
let data = &(data.read().await).0;
if data.0.stripe.is_none() {
return Json(Error::MiscError("Disabled".to_string()).into());
}
let sig = match headers.get("Stripe-Signature") {
Some(s) => s,
None => return Json(Error::NotAllowed.into()),
};
let req = match stripe::Webhook::construct_event(
&body,
&sig.to_str().unwrap(),
&data.0.stripe.as_ref().unwrap().webhook_signing_secret,
) {
Ok(e) => e,
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
};
match req.type_ {
EventType::CheckoutSessionCompleted => {
// checkout session ended, store user customer id (from stripe) in their user data
let session = match req.data.object {
EventObject::CheckoutSession(c) => c,
_ => unreachable!("cannot be this"),
};
let user_id = session
.client_reference_id
.unwrap()
.parse::<usize>()
.unwrap();
let customer_id = session.customer.unwrap().id();
let user = match data.get_user_by_id(user_id).await {
Ok(ua) => ua,
Err(e) => return Json(e.into()),
};
tracing::info!("subscribe {} (stripe: {})", user.id, customer_id);
if let Err(e) = data
.update_user_stripe_id(user.id, customer_id.as_str())
.await
{
return Json(e.into());
}
}
EventType::InvoicePaymentSucceeded => {
// payment finished and subscription created
// we're doing this *instead* of CustomerSubscriptionDeleted because
// the invoice happens *after* the checkout session ends... which is what we need
let invoice = match req.data.object {
EventObject::Invoice(c) => c,
_ => unreachable!("cannot be this"),
};
let customer_id = invoice.customer.unwrap().id();
// allow 10s for everything to finalize
tokio::time::sleep(Duration::from_secs(10)).await;
// pull user and update role
let user = match data.get_user_by_stripe_id(customer_id.as_str()).await {
Ok(ua) => ua,
Err(e) => return Json(e.into()),
};
tracing::info!("invoice {} (stripe: {})", user.id, customer_id);
let new_user_permissions = user.permissions | FinePermission::SUPPORTER;
if let Err(e) = data
.update_user_role(user.id, new_user_permissions, user.clone(), true)
.await
{
return Json(e.into());
}
if let Err(e) = data
.create_notification(Notification::new(
"Welcome new supporter!".to_string(),
"Thank you for your support! Your account has been updated with your new role."
.to_string(),
user.id,
))
.await
{
return Json(e.into());
}
}
EventType::CustomerSubscriptionDeleted => {
// payment failed and subscription deleted
let subscription = match req.data.object {
EventObject::Subscription(c) => c,
_ => unreachable!("cannot be this"),
};
let customer_id = subscription.customer.id();
let user = match data.get_user_by_stripe_id(customer_id.as_str()).await {
Ok(ua) => ua,
Err(e) => return Json(e.into()),
};
tracing::info!("unsubscribe {} (stripe: {})", user.id, customer_id);
let new_user_permissions = user.permissions - FinePermission::SUPPORTER;
if let Err(e) = data
.update_user_role(user.id, new_user_permissions, user.clone(), true)
.await
{
return Json(e.into());
}
if let Err(e) = data
.create_notification(Notification::new(
"Sorry to see you go... :(".to_string(),
"Thank you for your past support! Please feel free to leave us feedback on why you decided to cancel."
.to_string(),
user.id,
))
.await
{
return Json(e.into());
}
}
_ => return Json(Error::Unknown.into()),
}
Json(ApiReturn {
ok: true,
message: "Acceptable".to_string(),
payload: (),
})
}

View file

@ -247,7 +247,7 @@ pub async fn update_user_role_request(
None => return Json(Error::NotAllowed.into()),
};
match data.update_user_role(id, req.role, user).await {
match data.update_user_role(id, req.role, user, false).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "User updated".to_string(),