use std::time::Duration; use axum::{http::HeaderMap, response::IntoResponse, Extension, Json}; use tetratto_core::model::{ auth::{User, Notification}, moderation::AuditLogEntry, 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; let stripe_cnf = match data.0.0.stripe { Some(ref c) => c, 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.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!("payment {} (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 CustomerSubscriptionCreated 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(); let lines = invoice.lines.unwrap(); if lines.total_count.unwrap() > 1 { if let Err(e) = data .create_audit_log_entry(AuditLogEntry::new( 0, format!("too many invoice line items: stripe {customer_id}"), )) .await { return Json(e.into()); } return Json(Error::MiscError("Too many line items".to_string()).into()); } let item = match lines.data.get(0) { Some(i) => i, None => { if let Err(e) = data .create_audit_log_entry(AuditLogEntry::new( 0, format!("too few invoice line items: stripe {customer_id}"), )) .await { return Json(e.into()); } return Json(Error::MiscError("Too few line items".to_string()).into()); } }; let product_id = item .price .as_ref() .unwrap() .product .as_ref() .unwrap() .id() .to_string(); // pull user and update role let mut retries: usize = 0; let mut user: Option = None; loop { if retries >= 5 { // we've already tried 5 times (25 seconds of waiting)... it's not // going to happen // // we're going to report this error to the audit log so someone can // check manually later if let Err(e) = data .create_audit_log_entry(AuditLogEntry::new( 0, format!("invoice tier update failed: stripe {customer_id}"), )) .await { return Json(e.into()); } return Json(Error::GeneralNotFound("user".to_string()).into()); } match data.get_user_by_stripe_id(customer_id.as_str()).await { Ok(ua) => { if !user.is_none() { break; } user = Some(ua); break; } Err(_) => { tracing::info!("checkout session not stored in db yet"); retries += 1; tokio::time::sleep(Duration::from_secs(5)).await; continue; } } } let user = user.unwrap(); if product_id == stripe_cnf.product_ids.supporter { // supporter tracing::info!("found subscription user in {retries} tries"); 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 data.0.0.security.enable_invite_codes && user.awaiting_purchase { if let Err(e) = data .update_user_awaiting_purchased_status(user.id, false, user.clone(), false) .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()); } } else { tracing::error!( "received an invalid stripe product id, please check config.stripe.product_ids" ); return Json(Error::MiscError("Unknown product ID".to_string()).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 data.0.0.security.enable_invite_codes && user.was_purchased && user.invite_code == 0 { // user doesn't come from an invite code, and is a purchased account // this means their account must be locked if they stop paying if let Err(e) = data .update_user_awaiting_purchased_status(user.id, true, user.clone(), false) .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()); } } EventType::InvoicePaymentFailed => { // payment failed 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 (pay fail) {} (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 data.0.0.security.enable_invite_codes && user.was_purchased && user.invite_code == 0 { // user doesn't come from an invite code, and is a purchased account // this means their account must be locked if they stop paying if let Err(e) = data .update_user_awaiting_purchased_status(user.id, true, user.clone(), false) .await { return Json(e.into()); } } if let Err(e) = data .create_notification(Notification::new( "It seems your recent payment has failed :(".to_string(), "No worries! Your subscription is still active and will be retried. Your supporter status will resume when you have a successful payment." .to_string(), user.id, )) .await { return Json(e.into()); } } _ => return Json(Error::Unknown.into()), } Json(ApiReturn { ok: true, message: "Acceptable".to_string(), payload: (), }) }