Customer portal JSON handler

This commit is contained in:
Brandon Thomas committed 2022-11-15 19:07:42 -05:00
1 parent 049b0cf375
commit b403ed7c9e
11 files changed
+211 -169

No files matched your search

+8 -16
View File
@@ -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<T, B> (app: App<T, B>) -> App<T, B>
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()))
//)
)
)
}
@@ -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,
@@ -1,2 +1 @@
pub mod create;
pub mod stripe_checkout_success_handler;
pub mod create;
@@ -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<MySqlPool>,
) -> Result<HttpResponse, CheckoutSessionSuccessHandlerError>
{
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))
}
@@ -1 +1,4 @@
pub mod stripe_create_customer_portal_session_redirect_handler;
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;
@@ -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)
}
}
@@ -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<String>,
}
// =============== 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<JsonRequest>,
stripe_config: web::Data<StripeConfig>,
stripe_client: web::Data<stripe::Client>,
url_redirector: web::Data<ThirdPartyUrlRedirector>,
internal_product_to_stripe_lookup: web::Data<dyn InternalProductToStripeLookup>,
internal_user_lookup: web::Data<dyn InternalUserLookup>,
) -> Result<HttpResponse, CreateCustomerPortalSessionError>
{
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))
}
@@ -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<String>,
}
pub async fn stripe_create_customer_portal_session_redirect_handler(
http_request: HttpRequest,
query: Query<QueryParams>,
stripe_config: web::Data<StripeConfig>,
stripe_client: web::Data<stripe::Client>,
url_redirector: web::Data<ThirdPartyUrlRedirector>,
@@ -57,53 +28,20 @@ pub async fn stripe_create_customer_portal_session_redirect_handler(
internal_user_lookup: web::Data<dyn InternalUserLookup>,
) -> Result<HttpResponse, CreateCustomerPortalSessionError>
{
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))
@@ -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<StripeConfig>,
stripe_client: web::Data<stripe::Client>,
url_redirector: web::Data<ThirdPartyUrlRedirector>,
internal_product_to_stripe_lookup: web::Data<dyn InternalProductToStripeLookup>,
internal_user_lookup: web::Data<dyn InternalUserLookup>,
portal_config_id: &str,
)
-> Result<String, CreateCustomerPortalSessionError>
{
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
}
@@ -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)]
+1 -1
View File
@@ -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"),