use std::vec::IntoIter; use common_utils::{ext_traits::Encode, types::MinorUnit}; use diesel_models::enums as storage_enums; use error_stack::ResultExt; use hyperswitch_domain_models::ext_traits::OptionExt; use router_env::{ logger, tracing::{self, instrument}, }; use crate::{ core::{ errors::{self, RouterResult, StorageErrorExt}, payments::{ self, flows::{ConstructFlowSpecificData, Feature}, operations, }, routing::helpers as routing_helpers, }, db::StorageInterface, routes::{ self, app::{self, ReqState}, metrics, }, services, types::{self, api, domain, storage, transformers::ForeignFrom}, }; #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] #[cfg(feature = "v1")] pub async fn do_gsm_actions<'a, F, ApiRequest, FData, D>( state: &app::SessionState, req_state: ReqState, payment_data: &mut D, mut connector_routing_data: IntoIter, original_connector_data: &api::ConnectorData, mut router_data: types::RouterData, merchant_context: &domain::MerchantContext, operation: &operations::BoxedOperation<'_, F, ApiRequest, D>, customer: &Option, validate_result: &operations::ValidateResult, schedule_time: Option, frm_suggestion: Option, business_profile: &domain::Profile, ) -> RouterResult> where F: Clone + Send + Sync + 'static + 'a, FData: Send + Sync + types::Capturable + Clone + 'static + 'a, payments::PaymentResponse: operations::Operation, D: payments::OperationSessionGetters + payments::OperationSessionSetters + Send + Sync + Clone, D: ConstructFlowSpecificData, types::RouterData: Feature, dyn api::Connector: services::api::ConnectorIntegration, { let mut retries = None; metrics::AUTO_RETRY_ELIGIBLE_REQUEST_COUNT.add(1, &[]); let mut initial_gsm = get_gsm(state, &router_data).await?; let step_up_possible = initial_gsm .as_ref() .and_then(|data| data.feature_data.get_retry_feature_data()) .map(|data| data.is_step_up_possible()) .unwrap_or(false); #[cfg(feature = "v1")] let is_no_three_ds_payment = matches!( payment_data.get_payment_attempt().authentication_type, Some(storage_enums::AuthenticationType::NoThreeDs) ); #[cfg(feature = "v2")] let is_no_three_ds_payment = matches!( payment_data.get_payment_attempt().authentication_type, storage_enums::AuthenticationType::NoThreeDs ); let should_step_up = if step_up_possible && is_no_three_ds_payment { is_step_up_enabled_for_merchant_connector( state, merchant_context.get_merchant_account().get_id(), original_connector_data.connector_name, ) .await } else { false }; if should_step_up { router_data = do_retry( &state.clone(), req_state.clone(), original_connector_data, operation, customer, merchant_context, payment_data, router_data, validate_result, schedule_time, true, frm_suggestion, business_profile, false, //should_retry_with_pan is not applicable for step-up None, ) .await?; } // Step up is not applicable so proceed with auto retries flow else { loop { // Use initial_gsm for first time alone let gsm = match initial_gsm.as_ref() { Some(gsm) => Some(gsm.clone()), None => get_gsm(state, &router_data).await?, }; match get_gsm_decision(gsm) { storage_enums::GsmDecision::Retry => { retries = get_retries( state, retries, merchant_context.get_merchant_account().get_id(), business_profile, ) .await; if retries.is_none() || retries == Some(0) { metrics::AUTO_RETRY_EXHAUSTED_COUNT.add(1, &[]); logger::info!("retries exhausted for auto_retry payment"); break; } if connector_routing_data.len() == 0 { logger::info!("connectors exhausted for auto_retry payment"); metrics::AUTO_RETRY_EXHAUSTED_COUNT.add(1, &[]); break; } let is_network_token = payment_data .get_payment_method_data() .map(|pmd| pmd.is_network_token_payment_method_data()) .unwrap_or(false); let clear_pan_possible = initial_gsm .and_then(|data| data.feature_data.get_retry_feature_data()) .map(|data| data.is_clear_pan_possible()) .unwrap_or(false); let should_retry_with_pan = is_network_token && clear_pan_possible && business_profile.is_clear_pan_retries_enabled; // Currently we are taking off_session as a source of truth to identify MIT payments. let is_mit_payment = payment_data .get_payment_intent() .off_session .unwrap_or(false); let (connector, routing_decision) = if is_mit_payment { let connector_routing_data = super::get_connector_data(&mut connector_routing_data)?; let payment_method_info = payment_data .get_payment_method_info() .get_required_value("payment_method_info")? .clone(); let mandate_reference_id = payments::get_mandate_reference_id( connector_routing_data.action_type.clone(), connector_routing_data.clone(), payment_data, &payment_method_info, )?; payment_data.set_mandate_id(api_models::payments::MandateIds { mandate_id: None, mandate_reference_id, //mandate_ref_id }); (connector_routing_data.connector_data, None) } else if should_retry_with_pan { // If should_retry_with_pan is true, it indicates that we are retrying with PAN using the same connector. (original_connector_data.clone(), None) } else { let connector_routing_data = super::get_connector_data(&mut connector_routing_data)?; let routing_decision = connector_routing_data.network.map(|card_network| { routing_helpers::RoutingDecisionData::get_debit_routing_decision_data( card_network, None, ) }); (connector_routing_data.connector_data, routing_decision) }; router_data = do_retry( &state.clone(), req_state.clone(), &connector, operation, customer, merchant_context, payment_data, router_data, validate_result, schedule_time, //this is an auto retry payment, but not step-up false, frm_suggestion, business_profile, should_retry_with_pan, routing_decision, ) .await?; retries = retries.map(|i| i - 1); } storage_enums::GsmDecision::DoDefault => break, } initial_gsm = None; } } Ok(router_data) } #[instrument(skip_all)] pub async fn is_step_up_enabled_for_merchant_connector( state: &app::SessionState, merchant_id: &common_utils::id_type::MerchantId, connector_name: types::Connector, ) -> bool { let key = merchant_id.get_step_up_enabled_key(); let db = &*state.store; db.find_config_by_key_unwrap_or(key.as_str(), Some("[]".to_string())) .await .change_context(errors::ApiErrorResponse::InternalServerError) .and_then(|step_up_config| { serde_json::from_str::>(&step_up_config.config) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Step-up config parsing failed") }) .map_err(|err| { logger::error!(step_up_config_error=?err); }) .ok() .map(|connectors_enabled| connectors_enabled.contains(&connector_name)) .unwrap_or(false) } #[cfg(feature = "v1")] pub async fn get_merchant_max_auto_retries_enabled( db: &dyn StorageInterface, merchant_id: &common_utils::id_type::MerchantId, ) -> Option { let key = merchant_id.get_max_auto_retries_enabled(); db.find_config_by_key(key.as_str()) .await .change_context(errors::ApiErrorResponse::InternalServerError) .and_then(|retries_config| { retries_config .config .parse::() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Retries config parsing failed") }) .map_err(|err| { logger::error!(retries_error=?err); None:: }) .ok() } #[cfg(feature = "v1")] #[instrument(skip_all)] pub async fn get_retries( state: &app::SessionState, retries: Option, merchant_id: &common_utils::id_type::MerchantId, profile: &domain::Profile, ) -> Option { match retries { Some(retries) => Some(retries), None => get_merchant_max_auto_retries_enabled(state.store.as_ref(), merchant_id) .await .or(profile.max_auto_retries_enabled.map(i32::from)), } } #[instrument(skip_all)] pub async fn get_gsm( state: &app::SessionState, router_data: &types::RouterData, ) -> RouterResult> { let error_response = router_data.response.as_ref().err(); let error_code = error_response.map(|err| err.code.to_owned()); let error_message = error_response.map(|err| err.message.to_owned()); let connector_name = router_data.connector.to_string(); let flow = get_flow_name::()?; Ok( payments::helpers::get_gsm_record(state, error_code, error_message, connector_name, flow) .await, ) } #[instrument(skip_all)] pub fn get_gsm_decision( option_gsm: Option, ) -> storage_enums::GsmDecision { let option_gsm_decision = option_gsm .as_ref() .map(|gsm| gsm.feature_data.get_decision()); if option_gsm_decision.is_some() { metrics::AUTO_RETRY_GSM_MATCH_COUNT.add(1, &[]); } option_gsm_decision.unwrap_or_default() } #[inline] fn get_flow_name() -> RouterResult { Ok(std::any::type_name::() .to_string() .rsplit("::") .next() .ok_or(errors::ApiErrorResponse::InternalServerError) .attach_printable("Flow stringify failed")? .to_string()) } #[cfg(feature = "v1")] #[allow(clippy::too_many_arguments)] #[instrument(skip_all)] pub async fn do_retry<'a, F, ApiRequest, FData, D>( state: &'a routes::SessionState, req_state: ReqState, connector: &'a api::ConnectorData, operation: &'a operations::BoxedOperation<'a, F, ApiRequest, D>, customer: &'a Option, merchant_context: &domain::MerchantContext, payment_data: &'a mut D, router_data: types::RouterData, validate_result: &operations::ValidateResult, schedule_time: Option, is_step_up: bool, frm_suggestion: Option, business_profile: &domain::Profile, should_retry_with_pan: bool, routing_decision: Option, ) -> RouterResult> where F: Clone + Send + Sync + 'static + 'a, FData: Send + Sync + types::Capturable + Clone + 'static + 'a, payments::PaymentResponse: operations::Operation, D: payments::OperationSessionGetters + payments::OperationSessionSetters + Send + Sync + Clone, D: ConstructFlowSpecificData, types::RouterData: Feature, dyn api::Connector: services::api::ConnectorIntegration, { metrics::AUTO_RETRY_PAYMENT_COUNT.add(1, &[]); modify_trackers( state, connector.connector_name.to_string(), payment_data, merchant_context.get_merchant_key_store(), merchant_context.get_merchant_account().storage_scheme, router_data, is_step_up, ) .await?; let (merchant_connector_account, router_data, tokenization_action) = payments::call_connector_service_prerequisites( state, merchant_context, connector.clone(), operation, payment_data, customer, validate_result, business_profile, should_retry_with_pan, routing_decision, ) .await?; let (router_data, _mca) = payments::decide_unified_connector_service_call( state, req_state, merchant_context, connector.clone(), operation, payment_data, customer, payments::CallConnectorAction::Trigger, validate_result, schedule_time, hyperswitch_domain_models::payments::HeaderPayload::default(), frm_suggestion, business_profile, true, None, merchant_connector_account, router_data, tokenization_action, ) .await?; Ok(router_data) } #[cfg(feature = "v2")] #[instrument(skip_all)] pub async fn modify_trackers( state: &routes::SessionState, connector: String, payment_data: &mut D, key_store: &domain::MerchantKeyStore, storage_scheme: storage_enums::MerchantStorageScheme, router_data: types::RouterData, is_step_up: bool, ) -> RouterResult<()> where F: Clone + Send, FData: Send, D: payments::OperationSessionGetters + payments::OperationSessionSetters + Send + Sync, { todo!() } #[cfg(feature = "v1")] #[instrument(skip_all)] pub async fn modify_trackers( state: &routes::SessionState, connector: String, payment_data: &mut D, key_store: &domain::MerchantKeyStore, storage_scheme: storage_enums::MerchantStorageScheme, router_data: types::RouterData, is_step_up: bool, ) -> RouterResult<()> where F: Clone + Send, FData: Send + types::Capturable, D: payments::OperationSessionGetters + payments::OperationSessionSetters + Send + Sync, { let new_attempt_count = payment_data.get_payment_intent().attempt_count + 1; let new_payment_attempt = make_new_payment_attempt( connector, payment_data.get_payment_attempt().clone(), new_attempt_count, is_step_up, payment_data.get_payment_intent().setup_future_usage, ); let db = &*state.store; let key_manager_state = &state.into(); let additional_payment_method_data = payments::helpers::update_additional_payment_data_with_connector_response_pm_data( payment_data .get_payment_attempt() .payment_method_data .clone(), router_data .connector_response .clone() .and_then(|connector_response| connector_response.additional_payment_method_data), )?; let debit_routing_savings = payment_data.get_payment_method_data().and_then(|data| { payments::helpers::get_debit_routing_savings_amount( data, payment_data.get_payment_attempt(), ) }); match router_data.response { Ok(types::PaymentsResponseData::TransactionResponse { resource_id, connector_metadata, redirection_data, charges, .. }) => { let encoded_data = payment_data.get_payment_attempt().encoded_data.clone(); let authentication_data = (*redirection_data) .as_ref() .map(Encode::encode_to_value) .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Could not parse the connector response")?; let payment_attempt_update = storage::PaymentAttemptUpdate::ResponseUpdate { status: router_data.status, connector: None, connector_transaction_id: match resource_id { types::ResponseId::NoResponseId => None, types::ResponseId::ConnectorTransactionId(id) | types::ResponseId::EncodedData(id) => Some(id), }, connector_response_reference_id: payment_data .get_payment_attempt() .connector_response_reference_id .clone(), authentication_type: None, payment_method_id: payment_data.get_payment_attempt().payment_method_id.clone(), mandate_id: payment_data .get_mandate_id() .and_then(|mandate| mandate.mandate_id.clone()), connector_metadata, payment_token: None, error_code: None, error_message: None, error_reason: None, amount_capturable: if router_data.status.is_terminal_status() { Some(MinorUnit::new(0)) } else { None }, updated_by: storage_scheme.to_string(), authentication_data, encoded_data, unified_code: None, unified_message: None, capture_before: None, extended_authorization_applied: None, payment_method_data: additional_payment_method_data, connector_mandate_detail: None, charges, setup_future_usage_applied: None, debit_routing_savings, network_transaction_id: payment_data .get_payment_attempt() .network_transaction_id .clone(), is_overcapture_enabled: None, }; #[cfg(feature = "v1")] db.update_payment_attempt_with_attempt_id( payment_data.get_payment_attempt().clone(), payment_attempt_update, storage_scheme, ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; #[cfg(feature = "v2")] db.update_payment_attempt_with_attempt_id( key_manager_state, key_store, payment_data.get_payment_attempt().clone(), payment_attempt_update, storage_scheme, ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; } Ok(_) => { logger::error!("unexpected response: this response was not expected in Retry flow"); return Ok(()); } Err(ref error_response) => { let option_gsm = get_gsm(state, &router_data).await?; let auth_update = if Some(router_data.auth_type) != payment_data.get_payment_attempt().authentication_type { Some(router_data.auth_type) } else { None }; let payment_attempt_update = storage::PaymentAttemptUpdate::ErrorUpdate { connector: None, error_code: Some(Some(error_response.code.clone())), error_message: Some(Some(error_response.message.clone())), status: storage_enums::AttemptStatus::Failure, error_reason: Some(error_response.reason.clone()), amount_capturable: Some(MinorUnit::new(0)), updated_by: storage_scheme.to_string(), unified_code: option_gsm.clone().map(|gsm| gsm.unified_code), unified_message: option_gsm.map(|gsm| gsm.unified_message), connector_transaction_id: error_response.connector_transaction_id.clone(), payment_method_data: additional_payment_method_data, authentication_type: auth_update, issuer_error_code: error_response.network_decline_code.clone(), issuer_error_message: error_response.network_error_message.clone(), network_details: Some(ForeignFrom::foreign_from(error_response)), }; #[cfg(feature = "v1")] db.update_payment_attempt_with_attempt_id( payment_data.get_payment_attempt().clone(), payment_attempt_update, storage_scheme, ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; #[cfg(feature = "v2")] db.update_payment_attempt_with_attempt_id( key_manager_state, key_store, payment_data.get_payment_attempt().clone(), payment_attempt_update, storage_scheme, ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; } } #[cfg(feature = "v1")] let payment_attempt = db .insert_payment_attempt(new_payment_attempt, storage_scheme) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error inserting payment attempt")?; #[cfg(feature = "v2")] let payment_attempt = db .insert_payment_attempt( key_manager_state, key_store, new_payment_attempt, storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error inserting payment attempt")?; // update payment_attempt, connector_response and payment_intent in payment_data payment_data.set_payment_attempt(payment_attempt); let payment_intent = db .update_payment_intent( key_manager_state, payment_data.get_payment_intent().clone(), storage::PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate { active_attempt_id: payment_data.get_payment_attempt().get_id().to_owned(), attempt_count: new_attempt_count, updated_by: storage_scheme.to_string(), }, key_store, storage_scheme, ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; payment_data.set_payment_intent(payment_intent); Ok(()) } #[cfg(feature = "v1")] #[instrument(skip_all)] pub fn make_new_payment_attempt( connector: String, old_payment_attempt: storage::PaymentAttempt, new_attempt_count: i16, is_step_up: bool, setup_future_usage_intent: Option, ) -> storage::PaymentAttemptNew { let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now()); storage::PaymentAttemptNew { connector: Some(connector), attempt_id: old_payment_attempt .payment_id .get_attempt_id(new_attempt_count), payment_id: old_payment_attempt.payment_id, merchant_id: old_payment_attempt.merchant_id, status: old_payment_attempt.status, currency: old_payment_attempt.currency, save_to_locker: old_payment_attempt.save_to_locker, offer_amount: old_payment_attempt.offer_amount, payment_method_id: old_payment_attempt.payment_method_id, payment_method: old_payment_attempt.payment_method, payment_method_type: old_payment_attempt.payment_method_type, capture_method: old_payment_attempt.capture_method, capture_on: old_payment_attempt.capture_on, confirm: old_payment_attempt.confirm, authentication_type: if is_step_up { Some(storage_enums::AuthenticationType::ThreeDs) } else { old_payment_attempt.authentication_type }, amount_to_capture: old_payment_attempt.amount_to_capture, mandate_id: old_payment_attempt.mandate_id, browser_info: old_payment_attempt.browser_info, payment_token: old_payment_attempt.payment_token, client_source: old_payment_attempt.client_source, client_version: old_payment_attempt.client_version, created_at, modified_at, last_synced, profile_id: old_payment_attempt.profile_id, organization_id: old_payment_attempt.organization_id, net_amount: old_payment_attempt.net_amount, error_message: Default::default(), cancellation_reason: Default::default(), error_code: Default::default(), connector_metadata: Default::default(), payment_experience: Default::default(), payment_method_data: Default::default(), business_sub_label: Default::default(), straight_through_algorithm: Default::default(), preprocessing_step_id: Default::default(), mandate_details: Default::default(), error_reason: Default::default(), connector_response_reference_id: Default::default(), multiple_capture_count: Default::default(), amount_capturable: Default::default(), updated_by: Default::default(), authentication_data: Default::default(), encoded_data: Default::default(), merchant_connector_id: Default::default(), unified_code: Default::default(), unified_message: Default::default(), external_three_ds_authentication_attempted: Default::default(), authentication_connector: Default::default(), authentication_id: Default::default(), mandate_data: Default::default(), payment_method_billing_address_id: Default::default(), fingerprint_id: Default::default(), customer_acceptance: Default::default(), connector_mandate_detail: Default::default(), request_extended_authorization: Default::default(), extended_authorization_applied: Default::default(), capture_before: Default::default(), card_discovery: old_payment_attempt.card_discovery, processor_merchant_id: old_payment_attempt.processor_merchant_id, created_by: old_payment_attempt.created_by, setup_future_usage_applied: setup_future_usage_intent, // setup future usage is picked from intent for new payment attempt routing_approach: old_payment_attempt.routing_approach, connector_request_reference_id: Default::default(), network_transaction_id: old_payment_attempt.network_transaction_id, network_details: Default::default(), } } #[cfg(feature = "v2")] #[instrument(skip_all)] pub fn make_new_payment_attempt( _connector: String, _old_payment_attempt: storage::PaymentAttempt, _new_attempt_count: i16, _is_step_up: bool, ) -> storage::PaymentAttempt { todo!() } pub async fn get_merchant_config_for_gsm( db: &dyn StorageInterface, merchant_id: &common_utils::id_type::MerchantId, ) -> bool { let config = db .find_config_by_key_unwrap_or( &merchant_id.get_should_call_gsm_key(), Some("false".to_string()), ) .await; match config { Ok(conf) => conf.config == "true", Err(error) => { logger::error!(?error); false } } } #[cfg(feature = "v1")] pub async fn config_should_call_gsm( db: &dyn StorageInterface, merchant_id: &common_utils::id_type::MerchantId, profile: &domain::Profile, ) -> bool { let merchant_config_gsm = get_merchant_config_for_gsm(db, merchant_id).await; let profile_config_gsm = profile.is_auto_retries_enabled; merchant_config_gsm || profile_config_gsm } pub trait GsmValidation { // TODO : move this function to appropriate place later. fn should_call_gsm(&self) -> bool; } impl GsmValidation for types::RouterData { #[inline(always)] fn should_call_gsm(&self) -> bool { if self.response.is_err() { true } else { match self.status { storage_enums::AttemptStatus::Started | storage_enums::AttemptStatus::AuthenticationPending | storage_enums::AttemptStatus::AuthenticationSuccessful | storage_enums::AttemptStatus::Authorized | storage_enums::AttemptStatus::Charged | storage_enums::AttemptStatus::Authorizing | storage_enums::AttemptStatus::CodInitiated | storage_enums::AttemptStatus::Voided | storage_enums::AttemptStatus::VoidedPostCharge | storage_enums::AttemptStatus::VoidInitiated | storage_enums::AttemptStatus::CaptureInitiated | storage_enums::AttemptStatus::RouterDeclined | storage_enums::AttemptStatus::VoidFailed | storage_enums::AttemptStatus::AutoRefunded | storage_enums::AttemptStatus::CaptureFailed | storage_enums::AttemptStatus::PartialCharged | storage_enums::AttemptStatus::PartialChargedAndChargeable | storage_enums::AttemptStatus::Pending | storage_enums::AttemptStatus::PaymentMethodAwaited | storage_enums::AttemptStatus::ConfirmationAwaited | storage_enums::AttemptStatus::Unresolved | storage_enums::AttemptStatus::DeviceDataCollectionPending | storage_enums::AttemptStatus::IntegrityFailure | storage_enums::AttemptStatus::Expired | storage_enums::AttemptStatus::PartiallyAuthorized => false, storage_enums::AttemptStatus::AuthenticationFailed | storage_enums::AttemptStatus::AuthorizationFailed | storage_enums::AttemptStatus::Failure => true, } } } }