use std::{str::FromStr, time::Duration}; use axum::{http::HeaderMap, response::IntoResponse, Extension, Json}; use axum_extra::extract::CookieJar; use tetratto_core::model::{ auth::{User, Notification}, moderation::AuditLogEntry, permissions::FinePermission, ApiReturn, Error, }; use stripe::{EventObject, EventType}; use crate::{get_user_from_token, 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 invoice = match req.data.object { EventObject::Invoice(i) => i, _ => unreachable!("cannot be this"), }; let customer_id = invoice.customer.expect("TETRATTO_STRIPE_NO_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()), }; if !user.permissions.check(FinePermission::SUPPORTER) { // the user isn't currently a supporter, there's no reason to send this notification return Json(ApiReturn { ok: true, message: "Acceptable".to_string(), payload: (), }); } 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: (), }) } pub async fn onboarding_account_link_request( jar: CookieJar, Extension(data): Extension, ) -> 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()), }; if user.seller_data.account_id.is_some() { return Json(Error::NotAllowed.into()); } let client = match data.3 { Some(ref c) => c, None => return Json(Error::Unknown.into()), }; match stripe::AccountLink::create( &client, stripe::CreateAccountLink { account: match user.seller_data.account_id { Some(id) => stripe::AccountId::from_str(&id).unwrap(), None => return Json(Error::NotAllowed.into()), }, type_: stripe::AccountLinkType::AccountOnboarding, collect: None, expand: &[], refresh_url: Some(&format!( "{}/auth/connections_link/seller/refresh", data.0.0.0.host )), return_url: Some(&format!( "{}/auth/connections_link/seller/return", data.0.0.0.host )), collection_options: None, }, ) .await { Ok(x) => Json(ApiReturn { ok: true, message: "Acceptable".to_string(), payload: Some(x.url), }), Err(e) => Json(Error::MiscError(e.to_string()).into()), } } pub async fn create_seller_account_request( jar: CookieJar, Extension(data): Extension, ) -> impl IntoResponse { let data = &(data.read().await); let mut user = match get_user_from_token!(jar, data.0) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; if user.seller_data.account_id.is_some() { return Json(Error::NotAllowed.into()); } let client = match data.3 { Some(ref c) => c, None => return Json(Error::Unknown.into()), }; let account = match stripe::Account::create( &client, stripe::CreateAccount { type_: Some(stripe::AccountType::Express), capabilities: Some(stripe::CreateAccountCapabilities { card_payments: Some(stripe::CreateAccountCapabilitiesCardPayments { requested: Some(true), }), transfers: Some(stripe::CreateAccountCapabilitiesTransfers { requested: Some(true), }), ..Default::default() }), ..Default::default() }, ) .await { Ok(a) => a, Err(e) => return Json(Error::MiscError(e.to_string()).into()), }; user.seller_data.account_id = Some(account.id.to_string()); match data .0 .update_user_seller_data(user.id, user.seller_data) .await { Ok(_) => Json(ApiReturn { ok: true, message: "Acceptable".to_string(), payload: (), }), Err(e) => return Json(e.into()), } } pub async fn login_link_request( jar: CookieJar, Extension(data): Extension, ) -> 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()), }; if user.seller_data.account_id.is_none() | !user.seller_data.completed_onboarding { return Json(Error::NotAllowed.into()); } let client = match data.3 { Some(ref c) => c, None => return Json(Error::Unknown.into()), }; match stripe::LoginLink::create( &client, &stripe::AccountId::from_str(&user.seller_data.account_id.unwrap()).unwrap(), &data.0.0.0.host, ) .await { Ok(x) => Json(ApiReturn { ok: true, message: "Acceptable".to_string(), payload: Some(x.url), }), Err(e) => Json(Error::MiscError(e.to_string()).into()), } }