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:
Sai Harsha Vardhan
2024-04-22 21:06:47 +05:30
committed by GitHub
parent 4851da1595
commit 447655382b
11 changed files with 358 additions and 88 deletions

View File

@ -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",

View File

@ -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,

View File

@ -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;

View File

@ -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");

View File

@ -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 },

View File

@ -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

View File

@ -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>()

View File

@ -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;

View File

@ -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",

View File

@ -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,

View File

@ -4546,6 +4546,9 @@
}
}
}
},
"404": {
"description": "Poll not found"
}
},
"security": [