mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 09:48:44 +00:00
Customer portal JSON handler
This commit is contained in:
1 parent
049b0cf375
commit
b403ed7c9e
11 files changed
+211
-169
No files matched your search
@@ -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()))
|
||||
//)
|
||||
)
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
-58
@@ -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;
|
||||
+33
@@ -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)
|
||||
}
|
||||
}
|
||||
+64
@@ -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))
|
||||
}
|
||||
+27
-89
@@ -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))
|
||||
|
||||
+69
@@ -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)]
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in new issue
Block a user