2025-05-05 19:38:01 -04:00
|
|
|
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,
|
2025-05-09 22:36:16 -04:00
|
|
|
sig.to_str().unwrap(),
|
2025-05-05 19:38:01 -04:00
|
|
|
&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();
|
|
|
|
|
2025-05-07 21:54:21 -04:00
|
|
|
// allow 30s for everything to finalize
|
|
|
|
tokio::time::sleep(Duration::from_secs(30)).await;
|
2025-05-05 19:38:01 -04:00
|
|
|
|
|
|
|
// 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()),
|
|
|
|
};
|
|
|
|
|
2025-05-07 21:54:21 -04:00
|
|
|
if user.permissions.check(FinePermission::SUPPORTER) {
|
|
|
|
return Json(ApiReturn {
|
|
|
|
ok: true,
|
|
|
|
message: "Already applied".to_string(),
|
|
|
|
payload: (),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-05-05 19:38:01 -04:00
|
|
|
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: (),
|
|
|
|
})
|
|
|
|
}
|