From aae6a6b75e0ffdaf0c93b26151152279a15a162e Mon Sep 17 00:00:00 2001 From: hrithikesh026 Date: Wed, 22 Oct 2025 18:23:16 +0530 Subject: [PATCH] chore: introduce dedicated function to check for should continue after preprocessing --- .../src/connectors/cybersource.rs | 68 ++++++----- crates/hyperswitch_interfaces/src/api.rs | 41 ++----- .../src/connector_integration_interface.rs | 2 +- ...urrently in hyperswitch, under authoriz.md | 23 ++++ .../payments/flows/complete_authorize_flow.rs | 107 +++++++++++++----- 5 files changed, 146 insertions(+), 95 deletions(-) create mode 100644 crates/router/src/core/payments/Currently in hyperswitch, under authoriz.md diff --git a/crates/hyperswitch_connectors/src/connectors/cybersource.rs b/crates/hyperswitch_connectors/src/connectors/cybersource.rs index 1020736eb9..f485af538c 100644 --- a/crates/hyperswitch_connectors/src/connectors/cybersource.rs +++ b/crates/hyperswitch_connectors/src/connectors/cybersource.rs @@ -2286,7 +2286,7 @@ impl ConnectorSpecifications for Cybersource { fn get_preprocessing_flow_if_needed( &self, current_flow_info: api::CurrentFlowInfo<'_>, - ) -> Option { + ) -> Option { match current_flow_info { api::CurrentFlowInfo::Authorize { .. } => { // during authorize flow, there is no pre processing flow. Only alternate PreAuthenticate flow @@ -2297,45 +2297,43 @@ impl ConnectorSpecifications for Cybersource { let redirect_response = request_data.redirect_response.as_ref()?; match redirect_response.params.as_ref() { Some(param) if !param.peek().is_empty() => { - let flow_name = api::PreProcessingFlowName::Authenticate; - let should_continue = - |authn_result: &api::PreProcessingFlowResponse<'_>| -> bool { - (matches!( - authn_result.response, - Ok(PaymentsResponseData::TransactionResponse { - ref redirection_data, - .. - }) if redirection_data.is_none() - ) && authn_result.attempt_status - != common_enums::AttemptStatus::AuthenticationFailed) - }; - Some(api::PreProcessingFlowDetails { - flow_name, - should_continue: Box::new(should_continue), - }) - } - Some(_) | None => { - let flow_name = api::PreProcessingFlowName::PostAuthenticate; - let should_continue = - |authn_result: &api::PreProcessingFlowResponse<'_>| -> bool { - (matches!( - authn_result.response, - Ok(PaymentsResponseData::TransactionResponse { - ref redirection_data, - .. - }) if redirection_data.is_none() - ) && authn_result.attempt_status - != common_enums::AttemptStatus::AuthenticationFailed) - }; - Some(api::PreProcessingFlowDetails { - flow_name, - should_continue: Box::new(should_continue), - }) + Some(api::PreProcessingFlowName::Authenticate) } + Some(_) | None => Some(api::PreProcessingFlowName::PostAuthenticate), } } } } + fn decide_should_continue_after_preprocessing( + &self, + current_flow: api::CurrentFlowInfo<'_>, + pre_processing_flow_name: api::PreProcessingFlowName, + preprocessing_flow_response: api::PreProcessingFlowResponse<'_>, + ) -> bool { + match (current_flow, pre_processing_flow_name) { + (api::CurrentFlowInfo::Authorize { .. }, _) => { + // during authorize flow, there is no pre processing flow. Only alternate PreAuthenticate flow + true + } + ( + api::CurrentFlowInfo::CompleteAuthorize { .. }, + api::PreProcessingFlowName::Authenticate, + ) + | ( + api::CurrentFlowInfo::CompleteAuthorize { .. }, + api::PreProcessingFlowName::PostAuthenticate, + ) => { + (matches!( + preprocessing_flow_response.response, + Ok(PaymentsResponseData::TransactionResponse { + ref redirection_data, + .. + }) if redirection_data.is_none() + ) && preprocessing_flow_response.attempt_status + != common_enums::AttemptStatus::AuthenticationFailed) + } + } + } fn get_alternate_flow_if_needed( &self, current_flow: api::CurrentFlowInfo<'_>, diff --git a/crates/hyperswitch_interfaces/src/api.rs b/crates/hyperswitch_interfaces/src/api.rs index 0baf18d9ef..3acdf0cccd 100644 --- a/crates/hyperswitch_interfaces/src/api.rs +++ b/crates/hyperswitch_interfaces/src/api.rs @@ -429,43 +429,26 @@ pub struct PreProcessingFlowResponse<'a> { pub attempt_status: enums::AttemptStatus, } -/// Details related to preprocessing flow -pub struct PreProcessingFlowDetails { - /// Name of the preprocessing flow - pub flow_name: PreProcessingFlowName, - - /// Based on the response of the preprocessing flow, decide whether to continue with the current flow or not - pub should_continue: Box) -> bool>, -} - -/// Custom Debug implementation for PreProcessingFlowDetails -/// -/// Since Closure does not implement Debug trait -impl Debug for PreProcessingFlowDetails { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self { - flow_name, - should_continue: _, - } = self; - f.debug_struct("PreProcessingFlowDetails") - .field("flow_name", flow_name) - .field( - "should_continue", - &" bool>", - ) - .finish() - } -} - /// The trait that provides specifications about the connector pub trait ConnectorSpecifications { /// Preprocessing flow name if any, that must be made before the current flow. fn get_preprocessing_flow_if_needed( &self, _current_flow: CurrentFlowInfo<'_>, - ) -> Option { + ) -> Option { None } + /// Based on the current flow and preprocessing_flow_response, decide if the main flow must be called or not + /// + /// By default, always continue with the main flow after the preprocessing flow. + fn decide_should_continue_after_preprocessing( + &self, + _current_flow: CurrentFlowInfo<'_>, + _pre_processing_flow_name: PreProcessingFlowName, + _preprocessing_flow_response: PreProcessingFlowResponse<'_>, + ) -> bool { + true + } /// If Some is returned, the returned api flow must be made instead of the current flow. fn get_alternate_flow_if_needed( &self, diff --git a/crates/hyperswitch_interfaces/src/connector_integration_interface.rs b/crates/hyperswitch_interfaces/src/connector_integration_interface.rs index edcf0ed2f3..aa3740b3c2 100644 --- a/crates/hyperswitch_interfaces/src/connector_integration_interface.rs +++ b/crates/hyperswitch_interfaces/src/connector_integration_interface.rs @@ -511,7 +511,7 @@ impl ConnectorSpecifications for ConnectorEnum { fn get_preprocessing_flow_if_needed( &self, current_flow_info: api::CurrentFlowInfo<'_>, - ) -> Option { + ) -> Option { match self { Self::Old(connector) => connector.get_preprocessing_flow_if_needed(current_flow_info), Self::New(connector) => connector.get_preprocessing_flow_if_needed(current_flow_info), diff --git a/crates/router/src/core/payments/Currently in hyperswitch, under authoriz.md b/crates/router/src/core/payments/Currently in hyperswitch, under authoriz.md new file mode 100644 index 0000000000..8e216ca08d --- /dev/null +++ b/crates/router/src/core/payments/Currently in hyperswitch, under authoriz.md @@ -0,0 +1,23 @@ +Currently in hyperswitch, under authorize flow, there can be a lot of flows that must be called before actually calling the authorize. Like session/access token, customer create, order create etc. +Currently these are scattered all across the handler function 'pub async fn payments_operation_core(' . +I want to standardize the flows like this. + +PrimaryFlows and Secondary Flows. +PrimaryFlows: The actual flow. +SecondaryFlow: These flows might come as prerequisites before the Primary Flows. There can be multiple Secondary Flows for a PrimaryFlow. + +PrimaryFlows can be defined as: + 1. A flow where the response is returned to the client. + Eg: Authorize, Capture + Authorize can have SessionToken, OrderCreate etc. + +If Authorize is a PrimaryFlow, Then SessionTokena and OrderCreate will be SecondaryFlow. +The order will be like SessionToken(2ndary) -> OrderCreate(2ndary) -> Authorize(Primary). +Similarly, +* SessionToken(2ndary) -> PreAuthN(Primary). +* SessionToken(2ndary) -> AuthN(Primary). +* PostAuthN(2ndary) -> Authorization(Primary) + +Lets have marker trait for Primary and Secondary Flows. Feel free to come up with a better nomenclature for things. + +Execution order must be known at compile time. \ No newline at end of file diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs index 4a466646e0..4e82f51bc1 100644 --- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -219,43 +219,55 @@ impl Feature async fn call_preprocessing_through_unified_connector_service<'a>( self, - _state: &SessionState, - _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, - _lineage_ids: &grpc_client::LineageIds, - #[cfg(feature = "v1")] _merchant_connector_account: helpers::MerchantConnectorAccountType, + state: &SessionState, + header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + lineage_ids: &grpc_client::LineageIds, + #[cfg(feature = "v1")] merchant_connector_account: helpers::MerchantConnectorAccountType, #[cfg(feature = "v2")] - _merchant_connector_account: domain::MerchantConnectorAccountTypeDetails, - _merchant_context: &domain::MerchantContext, + merchant_connector_account: domain::MerchantConnectorAccountTypeDetails, + merchant_context: &domain::MerchantContext, connector_data: &api::ConnectorData, - _unified_connector_service_execution_mode: common_enums::ExecutionMode, - _merchant_order_reference_id: Option, + unified_connector_service_execution_mode: common_enums::ExecutionMode, + merchant_order_reference_id: Option, ) -> RouterResult<(Self, bool)> { let current_flow = api_interface::CurrentFlowInfo::CompleteAuthorize { request_data: &self.request, }; - if let Some(preprocessing_flow_details) = connector_data + let optional_preprocessing_flow = connector_data .connector - .get_preprocessing_flow_if_needed(current_flow) - { - let updated_router_data = match preprocessing_flow_details.flow_name { - api_interface::PreProcessingFlowName::Authenticate => { - // Call UCS for Authenticate flow - self - } - api_interface::PreProcessingFlowName::PostAuthenticate => { - // Call UCS for PostAuthenticate flow - self - } - }; - let pre_processing_flow_response = api_interface::PreProcessingFlowResponse { - response: &updated_router_data.response, - attempt_status: updated_router_data.status, - }; - let should_continue = - (preprocessing_flow_details.should_continue)(&pre_processing_flow_response); - Ok((updated_router_data, should_continue)) - } else { - Ok((self, true)) + .get_preprocessing_flow_if_needed(current_flow); + match optional_preprocessing_flow { + Some(preprocessing_flow) => { + let updated_router_data = handle_preprocessing_through_unified_connector_service( + self, + state, + header_payload, + lineage_ids, + merchant_connector_account.clone(), + merchant_context, + connector_data, + unified_connector_service_execution_mode, + merchant_order_reference_id.clone(), + preprocessing_flow, + ) + .await?; + let pre_processing_flow_response = api_interface::PreProcessingFlowResponse { + response: &updated_router_data.response, + attempt_status: updated_router_data.status, + }; + let current_flow = api_interface::CurrentFlowInfo::CompleteAuthorize { + request_data: &updated_router_data.request, + }; + let should_continue = connector_data + .connector + .decide_should_continue_after_preprocessing( + current_flow, + preprocessing_flow, + pre_processing_flow_response, + ); + Ok((updated_router_data, should_continue)) + } + None => Ok((self, true)), } } @@ -277,6 +289,41 @@ impl Feature } } +async fn handle_preprocessing_through_unified_connector_service( + router_data: types::RouterData< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, + _state: &SessionState, + _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _lineage_ids: &grpc_client::LineageIds, + #[cfg(feature = "v1")] _merchant_connector_account: helpers::MerchantConnectorAccountType, + #[cfg(feature = "v2")] _merchant_connector_account: domain::MerchantConnectorAccountTypeDetails, + _merchant_context: &domain::MerchantContext, + _connector_data: &api::ConnectorData, + _unified_connector_service_execution_mode: common_enums::ExecutionMode, + _merchant_order_reference_id: Option, + preprocessing_flow_name: api_interface::PreProcessingFlowName, +) -> RouterResult< + types::RouterData< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + >, +> { + match preprocessing_flow_name { + api_interface::PreProcessingFlowName::Authenticate => { + // Call UCS for Authenticate flow + Ok(router_data) + } + api_interface::PreProcessingFlowName::PostAuthenticate => { + // Call UCS for PostAuthenticate flow + Ok(router_data) + } + } +} + pub async fn complete_authorize_preprocessing_steps( state: &SessionState, router_data: &types::RouterData,