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, 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::() .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 30s for everything to finalize tokio::time::sleep(Duration::from_secs(30)).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()), }; if user.permissions.check(FinePermission::SUPPORTER) { return Json(ApiReturn { ok: true, message: "Already applied".to_string(), payload: (), }); } 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: (), }) }