mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feat(router): add poll ability in external 3ds authorization flow (#4393)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
4851da1595
commit
447655382b
@ -6,7 +6,8 @@
|
||||
("poll_id" = String, Path, description = "The identifier for poll")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "The poll status was retrieved successfully", body = PollResponse)
|
||||
(status = 200, description = "The poll status was retrieved successfully", body = PollResponse),
|
||||
(status = 404, description = "Poll not found")
|
||||
),
|
||||
tag = "Poll",
|
||||
operation_id = "Retrieve Poll Status",
|
||||
|
||||
@ -49,6 +49,21 @@ impl super::RedisConnectionPool {
|
||||
.change_context(errors::RedisError::SetFailed)
|
||||
}
|
||||
|
||||
pub async fn set_key_without_modifying_ttl<V>(
|
||||
&self,
|
||||
key: &str,
|
||||
value: V,
|
||||
) -> CustomResult<(), errors::RedisError>
|
||||
where
|
||||
V: TryInto<RedisValue> + Debug + Send + Sync,
|
||||
V::Error: Into<fred::error::RedisError> + Send + Sync,
|
||||
{
|
||||
self.pool
|
||||
.set(key, value, Some(Expiration::KEEPTTL), None, false)
|
||||
.await
|
||||
.change_context(errors::RedisError::SetFailed)
|
||||
}
|
||||
|
||||
pub async fn set_multiple_keys_if_not_exist<V>(
|
||||
&self,
|
||||
value: V,
|
||||
@ -96,6 +111,23 @@ impl super::RedisConnectionPool {
|
||||
self.set_key(key, serialized.as_slice()).await
|
||||
}
|
||||
|
||||
#[instrument(level = "DEBUG", skip(self))]
|
||||
pub async fn serialize_and_set_key_without_modifying_ttl<V>(
|
||||
&self,
|
||||
key: &str,
|
||||
value: V,
|
||||
) -> CustomResult<(), errors::RedisError>
|
||||
where
|
||||
V: serde::Serialize + Debug,
|
||||
{
|
||||
let serialized = value
|
||||
.encode_to_vec()
|
||||
.change_context(errors::RedisError::JsonSerializationFailed)?;
|
||||
|
||||
self.set_key_without_modifying_ttl(key, serialized.as_slice())
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "DEBUG", skip(self))]
|
||||
pub async fn serialize_and_set_key_with_expiry<V>(
|
||||
&self,
|
||||
|
||||
@ -102,3 +102,10 @@ pub const AUTHENTICATION_ID_PREFIX: &str = "authn";
|
||||
|
||||
// URL for checking the outgoing call
|
||||
pub const OUTGOING_CALL_URL: &str = "https://api.stripe.com/healthcheck";
|
||||
|
||||
// 15 minutes = 900 seconds
|
||||
pub const POLL_ID_TTL: i64 = 900;
|
||||
|
||||
// Default Poll Config
|
||||
pub const DEFAULT_POLL_DELAY_IN_SECS: i8 = 2;
|
||||
pub const DEFAULT_POLL_FREQUENCY: i8 = 5;
|
||||
|
||||
@ -7,14 +7,15 @@ use api_models::payments;
|
||||
use common_enums::Currency;
|
||||
use common_utils::{errors::CustomResult, ext_traits::ValueExt};
|
||||
use error_stack::{report, ResultExt};
|
||||
use masking::PeekInterface;
|
||||
use masking::{ExposeInterface, PeekInterface};
|
||||
|
||||
use super::errors;
|
||||
use crate::{
|
||||
consts::POLL_ID_TTL,
|
||||
core::{errors::ApiErrorResponse, payments as payments_core},
|
||||
routes::AppState,
|
||||
types::{self as core_types, api, authentication::AuthenticationResponseData, storage},
|
||||
utils::OptionExt,
|
||||
utils::{check_if_pull_mechanism_for_external_3ds_enabled_from_connector_metadata, OptionExt},
|
||||
};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@ -117,12 +118,19 @@ pub async fn perform_post_authentication<F: Clone + Send>(
|
||||
authentication,
|
||||
should_continue_confirm_transaction,
|
||||
} => {
|
||||
// let (auth, authentication_data) = authentication;
|
||||
let is_pull_mechanism_enabled =
|
||||
check_if_pull_mechanism_for_external_3ds_enabled_from_connector_metadata(
|
||||
merchant_connector_account
|
||||
.get_metadata()
|
||||
.map(|metadata| metadata.expose()),
|
||||
);
|
||||
let authentication_status =
|
||||
if !authentication.authentication_status.is_terminal_status() {
|
||||
if !authentication.authentication_status.is_terminal_status()
|
||||
&& is_pull_mechanism_enabled
|
||||
{
|
||||
let router_data = transformers::construct_post_authentication_router_data(
|
||||
authentication_connector.clone(),
|
||||
business_profile,
|
||||
business_profile.clone(),
|
||||
merchant_connector_account,
|
||||
&authentication,
|
||||
)?;
|
||||
@ -132,7 +140,7 @@ pub async fn perform_post_authentication<F: Clone + Send>(
|
||||
let updated_authentication = utils::update_trackers(
|
||||
state,
|
||||
router_data,
|
||||
authentication,
|
||||
authentication.clone(),
|
||||
payment_data.token.clone(),
|
||||
None,
|
||||
)
|
||||
@ -147,6 +155,31 @@ pub async fn perform_post_authentication<F: Clone + Send>(
|
||||
if !(authentication_status == api_models::enums::AuthenticationStatus::Success) {
|
||||
*should_continue_confirm_transaction = false;
|
||||
}
|
||||
// When authentication status is non-terminal, Set poll_id in redis to allow the fetch status of poll through retrieve_poll_status api from client
|
||||
if !authentication_status.is_terminal_status() {
|
||||
let req_poll_id = super::utils::get_external_authentication_request_poll_id(
|
||||
&payment_data.payment_intent.payment_id,
|
||||
);
|
||||
let poll_id = super::utils::get_poll_id(
|
||||
business_profile.merchant_id.clone(),
|
||||
req_poll_id.clone(),
|
||||
);
|
||||
let redis_conn = state
|
||||
.store
|
||||
.get_redis_conn()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to get redis connection")?;
|
||||
redis_conn
|
||||
.set_key_with_expiry(
|
||||
&poll_id,
|
||||
api_models::poll::PollStatus::Pending.to_string(),
|
||||
POLL_ID_TTL,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::StorageError::KVError)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to add poll_id in redis")?;
|
||||
}
|
||||
}
|
||||
types::PostAuthenthenticationFlowInput::PaymentMethodAuthNFlow { other_fields: _ } => {
|
||||
// todo!("Payment method post authN operation");
|
||||
|
||||
@ -30,7 +30,6 @@ use events::EventInfo;
|
||||
use futures::future::join_all;
|
||||
use helpers::ApplePayData;
|
||||
use masking::Secret;
|
||||
use maud::{html, PreEscaped};
|
||||
pub use payment_address::PaymentAddress;
|
||||
use redis_interface::errors::RedisError;
|
||||
use router_env::{instrument, tracing};
|
||||
@ -765,6 +764,10 @@ pub struct PaymentsRedirectResponseData {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait PaymentRedirectFlow<Ctx: PaymentMethodRetrieve>: Sync {
|
||||
// Associated type for call_payment_flow response
|
||||
type PaymentFlowResponse;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn call_payment_flow(
|
||||
&self,
|
||||
state: &AppState,
|
||||
@ -773,14 +776,14 @@ pub trait PaymentRedirectFlow<Ctx: PaymentMethodRetrieve>: Sync {
|
||||
merchant_key_store: domain::MerchantKeyStore,
|
||||
req: PaymentsRedirectResponseData,
|
||||
connector_action: CallConnectorAction,
|
||||
) -> RouterResponse<api::PaymentsResponse>;
|
||||
connector: String,
|
||||
) -> RouterResult<Self::PaymentFlowResponse>;
|
||||
|
||||
fn get_payment_action(&self) -> services::PaymentAction;
|
||||
|
||||
fn generate_response(
|
||||
&self,
|
||||
payments_response: &api_models::payments::PaymentsResponse,
|
||||
business_profile: diesel_models::business_profile::BusinessProfile,
|
||||
payment_flow_response: &Self::PaymentFlowResponse,
|
||||
payment_id: String,
|
||||
connector: String,
|
||||
) -> RouterResult<services::ApplicationResponse<api::RedirectionResponse>>;
|
||||
@ -836,7 +839,7 @@ pub trait PaymentRedirectFlow<Ctx: PaymentMethodRetrieve>: Sync {
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to decide the response flow")?;
|
||||
|
||||
let response = self
|
||||
let payment_flow_response = self
|
||||
.call_payment_flow(
|
||||
&state,
|
||||
req_state,
|
||||
@ -844,30 +847,11 @@ pub trait PaymentRedirectFlow<Ctx: PaymentMethodRetrieve>: Sync {
|
||||
key_store,
|
||||
req.clone(),
|
||||
flow_type,
|
||||
connector.clone(),
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
|
||||
let payments_response = match response? {
|
||||
services::ApplicationResponse::Json(response) => Ok(response),
|
||||
services::ApplicationResponse::JsonWithHeaders((response, _)) => Ok(response),
|
||||
_ => Err(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to get the response in json"),
|
||||
}?;
|
||||
|
||||
let profile_id = payments_response
|
||||
.profile_id
|
||||
.as_ref()
|
||||
.get_required_value("profile_id")?;
|
||||
|
||||
let business_profile = state
|
||||
.store
|
||||
.find_business_profile_by_profile_id(profile_id)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound {
|
||||
id: profile_id.to_string(),
|
||||
})?;
|
||||
|
||||
self.generate_response(&payments_response, business_profile, resource_id, connector)
|
||||
self.generate_response(&payment_flow_response, resource_id, connector)
|
||||
}
|
||||
}
|
||||
|
||||
@ -876,6 +860,9 @@ pub struct PaymentRedirectCompleteAuthorize;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentRedirectCompleteAuthorize {
|
||||
type PaymentFlowResponse = router_types::RedirectPaymentFlowResponse;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn call_payment_flow(
|
||||
&self,
|
||||
state: &AppState,
|
||||
@ -884,7 +871,8 @@ impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentRedirectCom
|
||||
merchant_key_store: domain::MerchantKeyStore,
|
||||
req: PaymentsRedirectResponseData,
|
||||
connector_action: CallConnectorAction,
|
||||
) -> RouterResponse<api::PaymentsResponse> {
|
||||
_connector: String,
|
||||
) -> RouterResult<Self::PaymentFlowResponse> {
|
||||
let payment_confirm_req = api::PaymentsRequest {
|
||||
payment_id: Some(req.resource_id.clone()),
|
||||
merchant_id: req.merchant_id.clone(),
|
||||
@ -896,7 +884,7 @@ impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentRedirectCom
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
Box::pin(payments_core::<
|
||||
let response = Box::pin(payments_core::<
|
||||
api::CompleteAuthorize,
|
||||
api::PaymentsResponse,
|
||||
_,
|
||||
@ -915,7 +903,28 @@ impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentRedirectCom
|
||||
None,
|
||||
HeaderPayload::default(),
|
||||
))
|
||||
.await
|
||||
.await?;
|
||||
let payments_response = match response {
|
||||
services::ApplicationResponse::Json(response) => Ok(response),
|
||||
services::ApplicationResponse::JsonWithHeaders((response, _)) => Ok(response),
|
||||
_ => Err(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to get the response in json"),
|
||||
}?;
|
||||
let profile_id = payments_response
|
||||
.profile_id
|
||||
.as_ref()
|
||||
.get_required_value("profile_id")?;
|
||||
let business_profile = state
|
||||
.store
|
||||
.find_business_profile_by_profile_id(profile_id)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound {
|
||||
id: profile_id.to_string(),
|
||||
})?;
|
||||
Ok(router_types::RedirectPaymentFlowResponse {
|
||||
payments_response,
|
||||
business_profile,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_payment_action(&self) -> services::PaymentAction {
|
||||
@ -924,11 +933,11 @@ impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentRedirectCom
|
||||
|
||||
fn generate_response(
|
||||
&self,
|
||||
payments_response: &api_models::payments::PaymentsResponse,
|
||||
business_profile: diesel_models::business_profile::BusinessProfile,
|
||||
payment_flow_response: &Self::PaymentFlowResponse,
|
||||
payment_id: String,
|
||||
connector: String,
|
||||
) -> RouterResult<services::ApplicationResponse<api::RedirectionResponse>> {
|
||||
let payments_response = &payment_flow_response.payments_response;
|
||||
// There might be multiple redirections needed for some flows
|
||||
// If the status is requires customer action, then send the startpay url again
|
||||
// The redirection data must have been provided and updated by the connector
|
||||
@ -964,7 +973,7 @@ impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentRedirectCom
|
||||
| api_models::enums::IntentStatus::Failed
|
||||
| api_models::enums::IntentStatus::Cancelled | api_models::enums::IntentStatus::RequiresCapture| api_models::enums::IntentStatus::Processing=> helpers::get_handle_response_url(
|
||||
payment_id,
|
||||
&business_profile,
|
||||
&payment_flow_response.business_profile,
|
||||
payments_response,
|
||||
connector,
|
||||
),
|
||||
@ -981,6 +990,9 @@ pub struct PaymentRedirectSync;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentRedirectSync {
|
||||
type PaymentFlowResponse = router_types::RedirectPaymentFlowResponse;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn call_payment_flow(
|
||||
&self,
|
||||
state: &AppState,
|
||||
@ -989,7 +1001,8 @@ impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentRedirectSyn
|
||||
merchant_key_store: domain::MerchantKeyStore,
|
||||
req: PaymentsRedirectResponseData,
|
||||
connector_action: CallConnectorAction,
|
||||
) -> RouterResponse<api::PaymentsResponse> {
|
||||
_connector: String,
|
||||
) -> RouterResult<Self::PaymentFlowResponse> {
|
||||
let payment_sync_req = api::PaymentsRetrieveRequest {
|
||||
resource_id: req.resource_id,
|
||||
merchant_id: req.merchant_id,
|
||||
@ -1006,7 +1019,7 @@ impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentRedirectSyn
|
||||
expand_attempts: None,
|
||||
expand_captures: None,
|
||||
};
|
||||
Box::pin(payments_core::<
|
||||
let response = Box::pin(payments_core::<
|
||||
api::PSync,
|
||||
api::PaymentsResponse,
|
||||
_,
|
||||
@ -1025,20 +1038,40 @@ impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentRedirectSyn
|
||||
None,
|
||||
HeaderPayload::default(),
|
||||
))
|
||||
.await
|
||||
.await?;
|
||||
let payments_response = match response {
|
||||
services::ApplicationResponse::Json(response) => Ok(response),
|
||||
services::ApplicationResponse::JsonWithHeaders((response, _)) => Ok(response),
|
||||
_ => Err(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to get the response in json"),
|
||||
}?;
|
||||
let profile_id = payments_response
|
||||
.profile_id
|
||||
.as_ref()
|
||||
.get_required_value("profile_id")?;
|
||||
let business_profile = state
|
||||
.store
|
||||
.find_business_profile_by_profile_id(profile_id)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound {
|
||||
id: profile_id.to_string(),
|
||||
})?;
|
||||
Ok(router_types::RedirectPaymentFlowResponse {
|
||||
payments_response,
|
||||
business_profile,
|
||||
})
|
||||
}
|
||||
fn generate_response(
|
||||
&self,
|
||||
payments_response: &api_models::payments::PaymentsResponse,
|
||||
business_profile: diesel_models::business_profile::BusinessProfile,
|
||||
payment_flow_response: &Self::PaymentFlowResponse,
|
||||
payment_id: String,
|
||||
connector: String,
|
||||
) -> RouterResult<services::ApplicationResponse<api::RedirectionResponse>> {
|
||||
Ok(services::ApplicationResponse::JsonForRedirection(
|
||||
helpers::get_handle_response_url(
|
||||
payment_id,
|
||||
&business_profile,
|
||||
payments_response,
|
||||
&payment_flow_response.business_profile,
|
||||
&payment_flow_response.payments_response,
|
||||
connector,
|
||||
)?,
|
||||
))
|
||||
@ -1054,6 +1087,9 @@ pub struct PaymentAuthenticateCompleteAuthorize;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentAuthenticateCompleteAuthorize {
|
||||
type PaymentFlowResponse = router_types::AuthenticatePaymentFlowResponse;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn call_payment_flow(
|
||||
&self,
|
||||
state: &AppState,
|
||||
@ -1062,7 +1098,8 @@ impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentAuthenticat
|
||||
merchant_key_store: domain::MerchantKeyStore,
|
||||
req: PaymentsRedirectResponseData,
|
||||
connector_action: CallConnectorAction,
|
||||
) -> RouterResponse<api::PaymentsResponse> {
|
||||
connector: String,
|
||||
) -> RouterResult<Self::PaymentFlowResponse> {
|
||||
let payment_confirm_req = api::PaymentsRequest {
|
||||
payment_id: Some(req.resource_id.clone()),
|
||||
merchant_id: req.merchant_id.clone(),
|
||||
@ -1074,7 +1111,7 @@ impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentAuthenticat
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
Box::pin(payments_core::<
|
||||
let response = Box::pin(payments_core::<
|
||||
api::Authorize,
|
||||
api::PaymentsResponse,
|
||||
_,
|
||||
@ -1093,51 +1130,69 @@ impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentAuthenticat
|
||||
None,
|
||||
HeaderPayload::with_source(enums::PaymentSource::ExternalAuthenticator),
|
||||
))
|
||||
.await
|
||||
.await?;
|
||||
let payments_response = match response {
|
||||
services::ApplicationResponse::Json(response) => Ok(response),
|
||||
services::ApplicationResponse::JsonWithHeaders((response, _)) => Ok(response),
|
||||
_ => Err(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to get the response in json"),
|
||||
}?;
|
||||
let default_poll_config = router_types::PollConfig::default();
|
||||
let default_config_str = default_poll_config
|
||||
.encode_to_string_of_json()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Error while stringifying default poll config")?;
|
||||
let poll_config = state
|
||||
.store
|
||||
.find_config_by_key_unwrap_or(
|
||||
&format!("poll_config_external_three_ds_{connector}"),
|
||||
Some(default_config_str),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("The poll config was not found in the DB")?;
|
||||
let poll_config =
|
||||
serde_json::from_str::<Option<router_types::PollConfig>>(&poll_config.config)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Error while parsing PollConfig")?
|
||||
.unwrap_or(default_poll_config);
|
||||
let profile_id = payments_response
|
||||
.profile_id
|
||||
.as_ref()
|
||||
.get_required_value("profile_id")?;
|
||||
let business_profile = state
|
||||
.store
|
||||
.find_business_profile_by_profile_id(profile_id)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound {
|
||||
id: profile_id.to_string(),
|
||||
})?;
|
||||
Ok(router_types::AuthenticatePaymentFlowResponse {
|
||||
payments_response,
|
||||
poll_config,
|
||||
business_profile,
|
||||
})
|
||||
}
|
||||
fn generate_response(
|
||||
&self,
|
||||
payments_response: &api_models::payments::PaymentsResponse,
|
||||
business_profile: diesel_models::business_profile::BusinessProfile,
|
||||
payment_flow_response: &Self::PaymentFlowResponse,
|
||||
payment_id: String,
|
||||
connector: String,
|
||||
) -> RouterResult<services::ApplicationResponse<api::RedirectionResponse>> {
|
||||
let payments_response = &payment_flow_response.payments_response;
|
||||
let redirect_response = helpers::get_handle_response_url(
|
||||
payment_id,
|
||||
&business_profile,
|
||||
payment_id.clone(),
|
||||
&payment_flow_response.business_profile,
|
||||
payments_response,
|
||||
connector,
|
||||
connector.clone(),
|
||||
)?;
|
||||
let return_url_with_query_params = redirect_response.return_url_with_query_params;
|
||||
// html script to check if inside iframe, then send post message to parent for redirection else redirect self to return_url
|
||||
let html = html! {
|
||||
head {
|
||||
title { "Redirect Form" }
|
||||
(PreEscaped(format!(r#"
|
||||
<script>
|
||||
let return_url = "{return_url_with_query_params}";
|
||||
try {{
|
||||
// if inside iframe, send post message to parent for redirection
|
||||
if (window.self !== window.parent) {{
|
||||
window.top.postMessage({{openurl: return_url}}, '*')
|
||||
// if parent, redirect self to return_url
|
||||
}} else {{
|
||||
window.location.href = return_url
|
||||
}}
|
||||
}}
|
||||
catch(err) {{
|
||||
// if error occurs, send post message to parent and wait for 10 secs to redirect. if doesn't redirect, redirect self to return_url
|
||||
window.parent.postMessage({{openurl: return_url}}, '*')
|
||||
setTimeout(function() {{
|
||||
window.location.href = return_url
|
||||
}}, 10000);
|
||||
console.log(err.message)
|
||||
}}
|
||||
</script>
|
||||
"#)))
|
||||
}
|
||||
}
|
||||
.into_string();
|
||||
let html = utils::get_html_redirect_response_for_external_authentication(
|
||||
redirect_response.return_url_with_query_params,
|
||||
payments_response,
|
||||
payment_id,
|
||||
&payment_flow_response.poll_config,
|
||||
)?;
|
||||
Ok(services::ApplicationResponse::Form(Box::new(
|
||||
services::RedirectionFormData {
|
||||
redirect_form: services::RedirectForm::Html { html_data: html },
|
||||
|
||||
@ -19,7 +19,7 @@ pub async fn retrieve_poll_status(
|
||||
.attach_printable("Failed to get redis connection")?;
|
||||
let request_poll_id = req.poll_id;
|
||||
// prepend 'poll_{merchant_id}_' to restrict access to only fetching Poll IDs, as this is a freely passed string in the request
|
||||
let poll_id = format!("poll_{}_{}", merchant_account.merchant_id, request_poll_id);
|
||||
let poll_id = super::utils::get_poll_id(merchant_account.merchant_id, request_poll_id.clone());
|
||||
let redis_value = redis_conn
|
||||
.get_key::<Option<String>>(poll_id.as_str())
|
||||
.await
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
use std::{marker::PhantomData, str::FromStr};
|
||||
|
||||
use api_models::enums::{DisputeStage, DisputeStatus};
|
||||
use common_enums::RequestIncrementalAuthorization;
|
||||
use common_enums::{IntentStatus, RequestIncrementalAuthorization};
|
||||
#[cfg(feature = "payouts")]
|
||||
use common_utils::{crypto::Encryptable, pii::Email};
|
||||
use common_utils::{errors::CustomResult, ext_traits::AsyncExt};
|
||||
use error_stack::{report, ResultExt};
|
||||
use maud::{html, PreEscaped};
|
||||
use router_env::{instrument, tracing};
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -23,7 +24,7 @@ use crate::{
|
||||
types::{
|
||||
self, domain,
|
||||
storage::{self, enums},
|
||||
ErrorResponse,
|
||||
ErrorResponse, PollConfig,
|
||||
},
|
||||
utils::{generate_id, generate_uuid, OptionExt, ValueExt},
|
||||
};
|
||||
@ -1090,6 +1091,96 @@ pub async fn get_profile_id_from_business_details(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_poll_id(merchant_id: String, unique_id: String) -> String {
|
||||
format!("poll_{}_{}", merchant_id, unique_id)
|
||||
}
|
||||
|
||||
pub fn get_external_authentication_request_poll_id(payment_id: &String) -> String {
|
||||
format!("external_authentication_{}", payment_id)
|
||||
}
|
||||
|
||||
pub fn get_html_redirect_response_for_external_authentication(
|
||||
return_url_with_query_params: String,
|
||||
payment_response: &api_models::payments::PaymentsResponse,
|
||||
payment_id: String,
|
||||
poll_config: &PollConfig,
|
||||
) -> RouterResult<String> {
|
||||
// if intent_status is requires_customer_action then set poll_id, fetch poll config and do a poll_status post message, else do open_url post message to redirect to return_url
|
||||
let html = match payment_response.status {
|
||||
IntentStatus::RequiresCustomerAction => {
|
||||
// Request poll id sent to client for retrieve_poll_status api
|
||||
let req_poll_id = get_external_authentication_request_poll_id(&payment_id);
|
||||
let poll_frequency = poll_config.frequency;
|
||||
let poll_delay_in_secs = poll_config.delay_in_secs;
|
||||
html! {
|
||||
head {
|
||||
title { "Redirect Form" }
|
||||
(PreEscaped(format!(r#"
|
||||
<script>
|
||||
let return_url = "{return_url_with_query_params}";
|
||||
let poll_status_data = {{
|
||||
'poll_id': '{req_poll_id}',
|
||||
'frequency': '{poll_frequency}',
|
||||
'delay_in_secs': '{poll_delay_in_secs}',
|
||||
'return_url_with_query_params': return_url
|
||||
}};
|
||||
try {{
|
||||
// if inside iframe, send post message to parent for redirection
|
||||
if (window.self !== window.parent) {{
|
||||
window.top.postMessage({{poll_status: poll_status_data}}, '*')
|
||||
// if parent, redirect self to return_url
|
||||
}} else {{
|
||||
window.location.href = return_url
|
||||
}}
|
||||
}}
|
||||
catch(err) {{
|
||||
// if error occurs, send post message to parent and wait for 10 secs to redirect. if doesn't redirect, redirect self to return_url
|
||||
window.top.postMessage({{poll_status: poll_status_data}}, '*')
|
||||
setTimeout(function() {{
|
||||
window.location.href = return_url
|
||||
}}, 10000);
|
||||
console.log(err.message)
|
||||
}}
|
||||
</script>
|
||||
"#)))
|
||||
}
|
||||
}
|
||||
.into_string()
|
||||
},
|
||||
_ => {
|
||||
html! {
|
||||
head {
|
||||
title { "Redirect Form" }
|
||||
(PreEscaped(format!(r#"
|
||||
<script>
|
||||
let return_url = "{return_url_with_query_params}";
|
||||
try {{
|
||||
// if inside iframe, send post message to parent for redirection
|
||||
if (window.self !== window.parent) {{
|
||||
window.top.postMessage({{openurl: return_url}}, '*')
|
||||
// if parent, redirect self to return_url
|
||||
}} else {{
|
||||
window.location.href = return_url
|
||||
}}
|
||||
}}
|
||||
catch(err) {{
|
||||
// if error occurs, send post message to parent and wait for 10 secs to redirect. if doesn't redirect, redirect self to return_url
|
||||
window.top.postMessage({{openurl: return_url}}, '*')
|
||||
setTimeout(function() {{
|
||||
window.location.href = return_url
|
||||
}}, 10000);
|
||||
console.log(err.message)
|
||||
}}
|
||||
</script>
|
||||
"#)))
|
||||
}
|
||||
}
|
||||
.into_string()
|
||||
},
|
||||
};
|
||||
Ok(html)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_flow_name<F>() -> RouterResult<String> {
|
||||
Ok(std::any::type_name::<F>()
|
||||
|
||||
@ -532,6 +532,24 @@ pub async fn external_authentication_incoming_webhook_flow<Ctx: PaymentMethodRet
|
||||
let status = payments_response.status;
|
||||
let event_type: Option<enums::EventType> =
|
||||
payments_response.status.foreign_into();
|
||||
// Set poll_id as completed in redis to allow the fetch status of poll through retrieve_poll_status api from client
|
||||
let poll_id = super::utils::get_poll_id(
|
||||
merchant_account.merchant_id.clone(),
|
||||
super::utils::get_external_authentication_request_poll_id(&payment_id),
|
||||
);
|
||||
let redis_conn = state
|
||||
.store
|
||||
.get_redis_conn()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to get redis connection")?;
|
||||
redis_conn
|
||||
.set_key_without_modifying_ttl(
|
||||
&poll_id,
|
||||
api_models::poll::PollStatus::Completed.to_string(),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to add poll_id in redis")?;
|
||||
// If event is NOT an UnsupportedEvent, trigger Outgoing Webhook
|
||||
if let Some(outgoing_event_type) = event_type {
|
||||
let primary_object_created_at = payments_response.created;
|
||||
|
||||
@ -16,7 +16,8 @@ use crate::{
|
||||
("poll_id" = String, Path, description = "The identifier for poll")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "The poll status was retrieved successfully", body = PollResponse)
|
||||
(status = 200, description = "The poll status was retrieved successfully", body = PollResponse),
|
||||
(status = 404, description = "Poll not found")
|
||||
),
|
||||
tag = "Poll",
|
||||
operation_id = "Retrieve Poll Status",
|
||||
|
||||
@ -34,6 +34,7 @@ pub use crate::core::payments::{payment_address::PaymentAddress, CustomerDetails
|
||||
#[cfg(feature = "payouts")]
|
||||
use crate::core::utils::IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLOW;
|
||||
use crate::{
|
||||
consts,
|
||||
core::{
|
||||
errors::{self},
|
||||
payments::{types, PaymentData, RecurringMandatePaymentData},
|
||||
@ -1146,6 +1147,34 @@ pub struct RetrieveFileResponse {
|
||||
pub file_data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct PollConfig {
|
||||
pub delay_in_secs: i8,
|
||||
pub frequency: i8,
|
||||
}
|
||||
|
||||
impl Default for PollConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
delay_in_secs: consts::DEFAULT_POLL_DELAY_IN_SECS,
|
||||
frequency: consts::DEFAULT_POLL_FREQUENCY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RedirectPaymentFlowResponse {
|
||||
pub payments_response: api_models::payments::PaymentsResponse,
|
||||
pub business_profile: diesel_models::business_profile::BusinessProfile,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AuthenticatePaymentFlowResponse {
|
||||
pub payments_response: api_models::payments::PaymentsResponse,
|
||||
pub poll_config: PollConfig,
|
||||
pub business_profile: diesel_models::business_profile::BusinessProfile,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct ConnectorResponse {
|
||||
pub merchant_id: String,
|
||||
|
||||
@ -4546,6 +4546,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Poll not found"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
|
||||
Reference in New Issue
Block a user