diff --git a/crates/plugins/billing/src/default_routes.rs b/crates/plugins/billing/src/default_routes.rs index c23d159a80..bfdef426e0 100644 --- a/crates/plugins/billing/src/default_routes.rs +++ b/crates/plugins/billing/src/default_routes.rs @@ -6,7 +6,7 @@ use actix_web::error::Error; use actix_web::{App, web, HttpResponse}; use crate::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_json_handler::stripe_create_checkout_session_json_handler; use crate::stripe::http_endpoints::checkout::create::stripe_create_checkout_session_redirect_handler::stripe_create_checkout_session_redirect_handler; -use crate::stripe::http_endpoints::checkout::stripe_checkout_success_handler::stripe_checkout_success_handler; +use crate::stripe::http_endpoints::customer_portal::stripe_create_customer_portal_session_json_handler::stripe_create_customer_portal_session_json_handler; use crate::stripe::http_endpoints::customer_portal::stripe_create_customer_portal_session_redirect_handler::stripe_create_customer_portal_session_redirect_handler; use crate::stripe::http_endpoints::webhook::stripe_webhook_handler::stripe_webhook_handler; @@ -21,38 +21,30 @@ pub fn add_suggested_stripe_billing_routes (app: App) -> App InitError = (), >, { - app.service(web::scope("/stripe") + app.service(web::scope("/v1/stripe") .service(web::resource("/webhook") .route(web::post().to(stripe_webhook_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) .service(web::scope("/checkout") - .service(web::resource("/begin") + .service(web::resource("/create_redirect") .route(web::get().to(stripe_create_checkout_session_json_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) - .service(web::resource("/begin_redirect") + .service(web::resource("/redirect") .route(web::get().to(stripe_create_checkout_session_redirect_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) - .service(web::resource("/success") - .route(web::get().to(stripe_checkout_success_handler)) - .route(web::head().to(|| HttpResponse::Ok())) - ) ) .service(web::scope("/portal") .service(web::resource("/create_redirect") + .route(web::get().to(stripe_create_customer_portal_session_json_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/redirect") .route(web::get().to(stripe_create_customer_portal_session_redirect_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) - //.service(web::resource("/create") - // .route(web::get().to(stripe_create_checkout_session_json_handler)) - // .route(web::head().to(|| HttpResponse::Ok())) - //) - //.service(web::resource("/success") - // .route(web::get().to(stripe_checkout_success_handler)) - // .route(web::head().to(|| HttpResponse::Ok())) - //) ) ) } diff --git a/crates/plugins/billing/src/stripe/http_endpoints/checkout/create/stripe_create_checkout_session_shared.rs b/crates/plugins/billing/src/stripe/http_endpoints/checkout/create/stripe_create_checkout_session_shared.rs index 2024044ee6..85a66bec91 100644 --- a/crates/plugins/billing/src/stripe/http_endpoints/checkout/create/stripe_create_checkout_session_shared.rs +++ b/crates/plugins/billing/src/stripe/http_endpoints/checkout/create/stripe_create_checkout_session_shared.rs @@ -206,7 +206,7 @@ mod tests { }, portal: StripeCustomerPortalConfigs { return_url: FullUrlOrPath::Path("/N/A".to_string()), - portal_config_id: "N/A".to_string() + default_portal_config_id: "N/A".to_string() }, secrets: StripeSecrets { publishable_key: None, diff --git a/crates/plugins/billing/src/stripe/http_endpoints/checkout/mod.rs b/crates/plugins/billing/src/stripe/http_endpoints/checkout/mod.rs index cde4a2bf7f..afc1871c10 100644 --- a/crates/plugins/billing/src/stripe/http_endpoints/checkout/mod.rs +++ b/crates/plugins/billing/src/stripe/http_endpoints/checkout/mod.rs @@ -1,2 +1 @@ -pub mod create; -pub mod stripe_checkout_success_handler; \ No newline at end of file +pub mod create; \ No newline at end of file diff --git a/crates/plugins/billing/src/stripe/http_endpoints/checkout/stripe_checkout_success_handler.rs b/crates/plugins/billing/src/stripe/http_endpoints/checkout/stripe_checkout_success_handler.rs deleted file mode 100644 index 9b4107fef9..0000000000 --- a/crates/plugins/billing/src/stripe/http_endpoints/checkout/stripe_checkout_success_handler.rs +++ /dev/null @@ -1,58 +0,0 @@ -use actix_web::error::ResponseError; -use actix_web::error::UrlencodedError::ContentType; -use actix_web::http::{header, StatusCode}; -use actix_web::web::Path; -use actix_web::{web, HttpResponse, HttpRequest}; -use chrono::{DateTime, Utc}; -use http_server_common::request::get_request_header_optional::get_request_header_optional; -use http_server_common::response::serialize_as_json_error::serialize_as_json_error; -use log::{error, warn}; -use sqlx::MySqlPool; -use std::collections::HashMap; -use std::fmt; - -#[derive(Serialize)] -pub struct CheckoutSessionSuccessHandlerSuccessResponse { - pub success: bool, -} - -#[derive(Debug, Serialize)] -pub enum CheckoutSessionSuccessHandlerError { - ServerError, -} - -impl ResponseError for CheckoutSessionSuccessHandlerError { - fn status_code(&self) -> StatusCode { - match *self { - CheckoutSessionSuccessHandlerError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR, - } - } - - fn error_response(&self) -> HttpResponse { - serialize_as_json_error(self) - } -} - -// NB: Not using derive_more::Display since Clion doesn't understand it. -impl fmt::Display for CheckoutSessionSuccessHandlerError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -pub async fn stripe_checkout_success_handler( - http_request: HttpRequest, - mysql_pool: web::Data, -) -> Result -{ - let response = CheckoutSessionSuccessHandlerSuccessResponse { - success: true, - }; - - let body = serde_json::to_string(&response) - .map_err(|_e| CheckoutSessionSuccessHandlerError::ServerError)?; - - Ok(HttpResponse::Ok() - .content_type("application/json") - .body(body)) -} diff --git a/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/mod.rs b/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/mod.rs index cf5d5f7c3a..f62229c4c3 100644 --- a/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/mod.rs +++ b/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/mod.rs @@ -1 +1,4 @@ -pub mod stripe_create_customer_portal_session_redirect_handler; \ No newline at end of file +pub mod stripe_create_customer_portal_session_error; +pub mod stripe_create_customer_portal_session_json_handler; +pub mod stripe_create_customer_portal_session_redirect_handler; +pub mod stripe_create_customer_portal_session_shared; \ No newline at end of file diff --git a/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/stripe_create_customer_portal_session_error.rs b/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/stripe_create_customer_portal_session_error.rs new file mode 100644 index 0000000000..2c487a4f0d --- /dev/null +++ b/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/stripe_create_customer_portal_session_error.rs @@ -0,0 +1,33 @@ +use actix_http::StatusCode; +use actix_web::{HttpResponse, ResponseError}; +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; + +#[derive(Debug, Serialize, Eq, PartialEq, Copy, Clone)] +pub enum CreateCustomerPortalSessionError { + BadRequest, + InvalidSession, + ServerError, + StripeError, +} + +impl ResponseError for CreateCustomerPortalSessionError { + fn status_code(&self) -> StatusCode { + match *self { + CreateCustomerPortalSessionError::BadRequest => StatusCode::BAD_REQUEST, + CreateCustomerPortalSessionError::InvalidSession => StatusCode::UNAUTHORIZED, + CreateCustomerPortalSessionError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + CreateCustomerPortalSessionError::StripeError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + serialize_as_json_error(self) + } +} + +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl std::fmt::Display for CreateCustomerPortalSessionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/stripe_create_customer_portal_session_json_handler.rs b/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/stripe_create_customer_portal_session_json_handler.rs new file mode 100644 index 0000000000..dabb1be12b --- /dev/null +++ b/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/stripe_create_customer_portal_session_json_handler.rs @@ -0,0 +1,64 @@ +use actix_web::web::Json; +use actix_web::{HttpRequest, HttpResponse, web}; +use crate::stripe::http_endpoints::customer_portal::stripe_create_customer_portal_session_error::CreateCustomerPortalSessionError; +use crate::stripe::http_endpoints::customer_portal::stripe_create_customer_portal_session_shared::stripe_create_customer_portal_session_shared; +use crate::stripe::stripe_config::StripeConfig; +use crate::stripe::traits::internal_product_to_stripe_lookup::InternalProductToStripeLookup; +use crate::stripe::traits::internal_user_lookup::InternalUserLookup; +use url_config::third_party_url_redirector::ThirdPartyUrlRedirector; + +// =============== Request =============== + +#[derive(Deserialize)] +pub struct JsonRequest { + // TODO: In the future, perhaps obscure this with well-known slugs + /// The stripe portal configuration ID. + /// This controls which plans can be switched to. + portal_config_id: Option, +} + +// =============== Success Response =============== + +#[derive(Serialize)] +pub struct SuccessResponse { + pub success: bool, + pub stripe_portal_redirect_url: String, +} + +pub async fn stripe_create_customer_portal_session_json_handler( + http_request: HttpRequest, + request: Json, + stripe_config: web::Data, + stripe_client: web::Data, + url_redirector: web::Data, + internal_product_to_stripe_lookup: web::Data, + internal_user_lookup: web::Data, +) -> Result +{ + let portal_config_id = request.portal_config_id + .as_deref() + .unwrap_or(&stripe_config.portal.default_portal_config_id) + .to_string(); + + let redirect_url = stripe_create_customer_portal_session_shared( + http_request, + stripe_config, + stripe_client, + url_redirector, + internal_product_to_stripe_lookup, + internal_user_lookup, + &portal_config_id, + ).await?; + + let response = SuccessResponse { + success: true, + stripe_portal_redirect_url: redirect_url, + }; + + let body = serde_json::to_string(&response) + .map_err(|_e| CreateCustomerPortalSessionError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} diff --git a/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/stripe_create_customer_portal_session_redirect_handler.rs b/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/stripe_create_customer_portal_session_redirect_handler.rs index 2e88744ca0..66a856ecf3 100644 --- a/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/stripe_create_customer_portal_session_redirect_handler.rs +++ b/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/stripe_create_customer_portal_session_redirect_handler.rs @@ -1,55 +1,26 @@ -use actix_web::error::ResponseError; -use actix_web::error::UrlencodedError::ContentType; -use actix_web::http::{header, StatusCode}; -use actix_web::web::{Path, Query}; -use actix_web::{web, HttpResponse, HttpRequest}; -use chrono::{DateTime, Utc}; -use crate::stripe::stripe_config::{FullUrlOrPath, StripeConfig}; +use actix_http::header; +use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::web::Query; +use crate::stripe::http_endpoints::customer_portal::stripe_create_customer_portal_session_error::CreateCustomerPortalSessionError; +use crate::stripe::http_endpoints::customer_portal::stripe_create_customer_portal_session_shared::stripe_create_customer_portal_session_shared; +use crate::stripe::stripe_config::StripeConfig; use crate::stripe::traits::internal_product_to_stripe_lookup::InternalProductToStripeLookup; use crate::stripe::traits::internal_user_lookup::InternalUserLookup; -use http_server_common::response::serialize_as_json_error::serialize_as_json_error; -use log::{error, warn}; -use sqlx::MySqlPool; -use std::collections::HashMap; -use std::fmt; -use std::str::FromStr; -use stripe::{BillingPortalSession, CheckoutSession, CheckoutSessionMode, CreateBillingPortalSession, CreateCheckoutSession, CreateCheckoutSessionLineItems, CustomerId}; use url_config::third_party_url_redirector::ThirdPartyUrlRedirector; -// =============== Error =============== +// =============== Request =============== -#[derive(Debug, Serialize, Eq, PartialEq)] -pub enum CreateCustomerPortalSessionError { - BadRequest, - InvalidSession, - ServerError, - StripeError, -} - -impl ResponseError for CreateCustomerPortalSessionError { - fn status_code(&self) -> StatusCode { - match *self { - CreateCustomerPortalSessionError::BadRequest => StatusCode::BAD_REQUEST, - CreateCustomerPortalSessionError::InvalidSession => StatusCode::UNAUTHORIZED, - CreateCustomerPortalSessionError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, - CreateCustomerPortalSessionError::StripeError => StatusCode::INTERNAL_SERVER_ERROR, - } - } - - fn error_response(&self) -> HttpResponse { - serialize_as_json_error(self) - } -} - -// NB: Not using derive_more::Display since Clion doesn't understand it. -impl fmt::Display for CreateCustomerPortalSessionError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } +#[derive(Deserialize)] +pub struct QueryParams { + // TODO: In the future, perhaps obscure this with well-known slugs + /// The stripe portal configuration ID. + /// This controls which plans can be switched to. + portal_config_id: Option, } pub async fn stripe_create_customer_portal_session_redirect_handler( http_request: HttpRequest, + query: Query, stripe_config: web::Data, stripe_client: web::Data, url_redirector: web::Data, @@ -57,53 +28,20 @@ pub async fn stripe_create_customer_portal_session_redirect_handler( internal_user_lookup: web::Data, ) -> Result { - let maybe_user_metadata = internal_user_lookup - .lookup_user_from_http_request(&http_request) - .await - .map_err(|err| { - error!("Error looking up user: {:?}", err); - CreateCustomerPortalSessionError::ServerError // NB: This was probably *our* fault. - })?; + let portal_config_id = query.portal_config_id + .as_deref() + .unwrap_or(&stripe_config.portal.default_portal_config_id) + .to_string(); - // NB: Our integration relies on an internal user token being present. - let user_metadata = match maybe_user_metadata { - None => return Err(CreateCustomerPortalSessionError::InvalidSession), - Some(user_metadata) => user_metadata, - }; - - let stripe_customer_id = match user_metadata.maybe_existing_stripe_customer_id { - Some(stripe_customer_id) => { - CustomerId::from_str(&stripe_customer_id) - .map_err(|err| { - error!("Problem constructing user's stripe customer id: {:?}", err); - CreateCustomerPortalSessionError::ServerError // NB: This was probably *our* fault. - })? - } - None => { - error!("No stripe customer ID to create a portal with"); - return Err(CreateCustomerPortalSessionError::InvalidSession); - } - }; - - let mut params = CreateBillingPortalSession::new(stripe_customer_id); - - let return_url = match &stripe_config.portal.return_url { - FullUrlOrPath::FullUrl(url) => url.to_string(), - FullUrlOrPath::Path(path) => url_redirector.redirect_url_for_path(&http_request, path) - .map_err(|_| CreateCustomerPortalSessionError::ServerError)?, - }; - - params.return_url = Some(&return_url); - params.configuration = Some(&stripe_config.portal.portal_config_id); - - let response = BillingPortalSession::create(stripe_client.as_ref(), params) - .await - .map_err(|e| { - error!("Error: {:?}", e); - CreateCustomerPortalSessionError::StripeError - })?; - - let redirect_url = response.url; + let redirect_url = stripe_create_customer_portal_session_shared( + http_request, + stripe_config, + stripe_client, + url_redirector, + internal_product_to_stripe_lookup, + internal_user_lookup, + &portal_config_id, + ).await?; Ok(HttpResponse::Found() .append_header((header::LOCATION, redirect_url)) diff --git a/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/stripe_create_customer_portal_session_shared.rs b/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/stripe_create_customer_portal_session_shared.rs new file mode 100644 index 0000000000..29a818866f --- /dev/null +++ b/crates/plugins/billing/src/stripe/http_endpoints/customer_portal/stripe_create_customer_portal_session_shared.rs @@ -0,0 +1,69 @@ +use actix_web::{HttpRequest, web}; +use crate::stripe::http_endpoints::customer_portal::stripe_create_customer_portal_session_error::CreateCustomerPortalSessionError; +use crate::stripe::stripe_config::{FullUrlOrPath, StripeConfig}; +use crate::stripe::traits::internal_product_to_stripe_lookup::InternalProductToStripeLookup; +use crate::stripe::traits::internal_user_lookup::InternalUserLookup; +use log::error; +use std::str::FromStr; +use stripe::{BillingPortalSession, CreateBillingPortalSession, CustomerId}; +use url_config::third_party_url_redirector::ThirdPartyUrlRedirector; + +pub async fn stripe_create_customer_portal_session_shared( + http_request: HttpRequest, + stripe_config: web::Data, + stripe_client: web::Data, + url_redirector: web::Data, + internal_product_to_stripe_lookup: web::Data, + internal_user_lookup: web::Data, + portal_config_id: &str, +) + -> Result +{ + let maybe_user_metadata = internal_user_lookup + .lookup_user_from_http_request(&http_request) + .await + .map_err(|err| { + error!("Error looking up user: {:?}", err); + CreateCustomerPortalSessionError::ServerError // NB: This was probably *our* fault. + })?; + + // NB: Our integration relies on an internal user token being present. + let user_metadata = match maybe_user_metadata { + None => return Err(CreateCustomerPortalSessionError::InvalidSession), + Some(user_metadata) => user_metadata, + }; + + let stripe_customer_id = match user_metadata.maybe_existing_stripe_customer_id { + Some(stripe_customer_id) => { + CustomerId::from_str(&stripe_customer_id) + .map_err(|err| { + error!("Problem constructing user's stripe customer id: {:?}", err); + CreateCustomerPortalSessionError::ServerError // NB: This was probably *our* fault. + })? + } + None => { + error!("No stripe customer ID to create a portal with"); + return Err(CreateCustomerPortalSessionError::InvalidSession); + } + }; + + let mut params = CreateBillingPortalSession::new(stripe_customer_id); + + let return_url = match &stripe_config.portal.return_url { + FullUrlOrPath::FullUrl(url) => url.to_string(), + FullUrlOrPath::Path(path) => url_redirector.redirect_url_for_path(&http_request, path) + .map_err(|_| CreateCustomerPortalSessionError::ServerError)?, + }; + + params.return_url = Some(&return_url); + params.configuration = Some(portal_config_id); + + let response = BillingPortalSession::create(stripe_client.as_ref(), params) + .await + .map_err(|e| { + error!("Stripe billing portal error: {:?}", e); + CreateCustomerPortalSessionError::StripeError + })?; + + Ok(response.url.to_string()) // NB: Redirect URL +} diff --git a/crates/plugins/billing/src/stripe/stripe_config.rs b/crates/plugins/billing/src/stripe/stripe_config.rs index a79ef97e92..9e1bc2cddb 100644 --- a/crates/plugins/billing/src/stripe/stripe_config.rs +++ b/crates/plugins/billing/src/stripe/stripe_config.rs @@ -26,7 +26,9 @@ pub struct StripeCheckoutConfigs { #[derive(Clone)] pub struct StripeCustomerPortalConfigs { pub return_url: FullUrlOrPath, - pub portal_config_id: String, // TODO: This should be dynamic. Some locales will see different. + + /// The portal config id to use as a fallback. + pub default_portal_config_id: String, } #[derive(Clone)] diff --git a/crates/service/storyteller_web/src/main.rs b/crates/service/storyteller_web/src/main.rs index a31b3698c8..bedbbb0137 100644 --- a/crates/service/storyteller_web/src/main.rs +++ b/crates/service/storyteller_web/src/main.rs @@ -316,7 +316,7 @@ async fn main() -> AnyhowResult<()> { }, portal: StripeCustomerPortalConfigs { return_url: FullUrlOrPath::Path(easyenv::get_env_string_required("STRIPE_PORTAL_RETURN_URL_PATH")?), - portal_config_id: easyenv::get_env_string_required("STRIPE_PORTAL_CONFIG_ID")?, + default_portal_config_id: easyenv::get_env_string_required("STRIPE_PORTAL_CONFIG_ID")?, }, secrets: StripeSecrets { publishable_key: easyenv::get_env_string_optional("STRIPE_PUBLISHABLE_KEY"),