From 447655382bcf2fdd69a1ec6a56e5e4df8a8feef2 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Mon, 22 Apr 2024 21:06:47 +0530 Subject: [PATCH 1/2] 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> --- crates/openapi/src/routes/poll.rs | 3 +- crates/redis_interface/src/commands.rs | 32 ++++ crates/router/src/consts.rs | 7 + crates/router/src/core/authentication.rs | 45 ++++- crates/router/src/core/payments.rs | 209 ++++++++++++++--------- crates/router/src/core/poll.rs | 2 +- crates/router/src/core/utils.rs | 95 ++++++++++- crates/router/src/core/webhooks.rs | 18 ++ crates/router/src/routes/poll.rs | 3 +- crates/router/src/types.rs | 29 ++++ openapi/openapi_spec.json | 3 + 11 files changed, 358 insertions(+), 88 deletions(-) diff --git a/crates/openapi/src/routes/poll.rs b/crates/openapi/src/routes/poll.rs index 3ee725c3d21..db2fd49fe47 100644 --- a/crates/openapi/src/routes/poll.rs +++ b/crates/openapi/src/routes/poll.rs @@ -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", diff --git a/crates/redis_interface/src/commands.rs b/crates/redis_interface/src/commands.rs index d9b7072ff8c..46e3a35fd33 100644 --- a/crates/redis_interface/src/commands.rs +++ b/crates/redis_interface/src/commands.rs @@ -49,6 +49,21 @@ impl super::RedisConnectionPool { .change_context(errors::RedisError::SetFailed) } + pub async fn set_key_without_modifying_ttl( + &self, + key: &str, + value: V, + ) -> CustomResult<(), errors::RedisError> + where + V: TryInto + Debug + Send + Sync, + V::Error: Into + 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( &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( + &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( &self, diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 70add106762..c2a8669030b 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -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; diff --git a/crates/router/src/core/authentication.rs b/crates/router/src/core/authentication.rs index fe793d2e39f..e095244df83 100644 --- a/crates/router/src/core/authentication.rs +++ b/crates/router/src/core/authentication.rs @@ -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( 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( 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( 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"); diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index b9a8795aa54..1109ce26c03 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -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: 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: Sync { merchant_key_store: domain::MerchantKeyStore, req: PaymentsRedirectResponseData, connector_action: CallConnectorAction, - ) -> RouterResponse; + connector: String, + ) -> RouterResult; 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>; @@ -836,7 +839,7 @@ pub trait PaymentRedirectFlow: 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: Sync { key_store, req.clone(), flow_type, + connector.clone(), ) - .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(), - })?; + .await?; - 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 PaymentRedirectFlow 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 PaymentRedirectFlow for PaymentRedirectCom merchant_key_store: domain::MerchantKeyStore, req: PaymentsRedirectResponseData, connector_action: CallConnectorAction, - ) -> RouterResponse { + _connector: String, + ) -> RouterResult { let payment_confirm_req = api::PaymentsRequest { payment_id: Some(req.resource_id.clone()), merchant_id: req.merchant_id.clone(), @@ -896,7 +884,7 @@ impl PaymentRedirectFlow for PaymentRedirectCom }), ..Default::default() }; - Box::pin(payments_core::< + let response = Box::pin(payments_core::< api::CompleteAuthorize, api::PaymentsResponse, _, @@ -915,7 +903,28 @@ impl PaymentRedirectFlow 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 PaymentRedirectFlow 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> { + 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 PaymentRedirectFlow 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 PaymentRedirectFlow 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 PaymentRedirectFlow for PaymentRedirectSyn merchant_key_store: domain::MerchantKeyStore, req: PaymentsRedirectResponseData, connector_action: CallConnectorAction, - ) -> RouterResponse { + _connector: String, + ) -> RouterResult { let payment_sync_req = api::PaymentsRetrieveRequest { resource_id: req.resource_id, merchant_id: req.merchant_id, @@ -1006,7 +1019,7 @@ impl PaymentRedirectFlow 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 PaymentRedirectFlow 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> { 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 PaymentRedirectFlow 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 PaymentRedirectFlow for PaymentAuthenticat merchant_key_store: domain::MerchantKeyStore, req: PaymentsRedirectResponseData, connector_action: CallConnectorAction, - ) -> RouterResponse { + connector: String, + ) -> RouterResult { let payment_confirm_req = api::PaymentsRequest { payment_id: Some(req.resource_id.clone()), merchant_id: req.merchant_id.clone(), @@ -1074,7 +1111,7 @@ impl PaymentRedirectFlow for PaymentAuthenticat }), ..Default::default() }; - Box::pin(payments_core::< + let response = Box::pin(payments_core::< api::Authorize, api::PaymentsResponse, _, @@ -1093,51 +1130,69 @@ impl PaymentRedirectFlow 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::>(&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> { + 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#" - - "#))) - } - } - .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 }, diff --git a/crates/router/src/core/poll.rs b/crates/router/src/core/poll.rs index a4a4fdb9f88..32a7b0f547c 100644 --- a/crates/router/src/core/poll.rs +++ b/crates/router/src/core/poll.rs @@ -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::>(poll_id.as_str()) .await diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 3423ace6e5b..568e34c04a6 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -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 { + // 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#" + + "#))) + } + } + .into_string() + }, + _ => { + html! { + head { + title { "Redirect Form" } + (PreEscaped(format!(r#" + + "#))) + } + } + .into_string() + }, + }; + Ok(html) +} + #[inline] pub fn get_flow_name() -> RouterResult { Ok(std::any::type_name::() diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index d236ad094c6..6e2068422fc 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -532,6 +532,24 @@ pub async fn external_authentication_incoming_webhook_flow = 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; diff --git a/crates/router/src/routes/poll.rs b/crates/router/src/routes/poll.rs index 5e1f53adf49..39bb63832a9 100644 --- a/crates/router/src/routes/poll.rs +++ b/crates/router/src/routes/poll.rs @@ -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", diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index dc43b673ec1..6004131be40 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -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, } +#[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, diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 0bc0059e949..b736475858f 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4546,6 +4546,9 @@ } } } + }, + "404": { + "description": "Poll not found" } }, "security": [ From 589cb4c9f94d6a410627d569aa5495d60ca763ae Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 00:03:23 +0000 Subject: [PATCH 2/2] chore(version): 2024.04.23.0 --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5b2e7da631..3db3413c46b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.04.23.0 + +### Features + +- **euclied_wasm:** [NMI] Add configs for extended 3DS ([#4422](https://github.com/juspay/hyperswitch/pull/4422)) ([`b8be10d`](https://github.com/juspay/hyperswitch/commit/b8be10de52e40d2327819d33c6c1ec40a459bdd5)) +- **router:** Add poll ability in external 3ds authorization flow ([#4393](https://github.com/juspay/hyperswitch/pull/4393)) ([`4476553`](https://github.com/juspay/hyperswitch/commit/447655382bcf2fdd69a1ec6a56e5e4df8a8feef2)) + +### Refactors + +- **wallet:** Use `billing.phone` instead of `telephone_number` ([#4329](https://github.com/juspay/hyperswitch/pull/4329)) ([`3e6bc19`](https://github.com/juspay/hyperswitch/commit/3e6bc191fd5804feface9ee1a0cb7ddbbe025569)) + +### Miscellaneous Tasks + +- Add wasm toml configs for netcetera authnetication connector ([#4426](https://github.com/juspay/hyperswitch/pull/4426)) ([`4851da1`](https://github.com/juspay/hyperswitch/commit/4851da1595074dbb2760e76f83403e8ac9f7895f)) + +**Full Changelog:** [`2024.04.22.0...2024.04.23.0`](https://github.com/juspay/hyperswitch/compare/2024.04.22.0...2024.04.23.0) + +- - - + ## 2024.04.22.0 ### Features