mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 21:07:58 +08:00
843 lines
32 KiB
Rust
843 lines
32 KiB
Rust
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<api::ConnectorRoutingData>,
|
|
original_connector_data: &api::ConnectorData,
|
|
mut router_data: types::RouterData<F, FData, types::PaymentsResponseData>,
|
|
merchant_context: &domain::MerchantContext,
|
|
operation: &operations::BoxedOperation<'_, F, ApiRequest, D>,
|
|
customer: &Option<domain::Customer>,
|
|
validate_result: &operations::ValidateResult,
|
|
schedule_time: Option<time::PrimitiveDateTime>,
|
|
frm_suggestion: Option<storage_enums::FrmSuggestion>,
|
|
business_profile: &domain::Profile,
|
|
) -> RouterResult<types::RouterData<F, FData, types::PaymentsResponseData>>
|
|
where
|
|
F: Clone + Send + Sync + 'static + 'a,
|
|
FData: Send + Sync + types::Capturable + Clone + 'static + 'a,
|
|
payments::PaymentResponse: operations::Operation<F, FData>,
|
|
D: payments::OperationSessionGetters<F>
|
|
+ payments::OperationSessionSetters<F>
|
|
+ Send
|
|
+ Sync
|
|
+ Clone,
|
|
D: ConstructFlowSpecificData<F, FData, types::PaymentsResponseData>,
|
|
types::RouterData<F, FData, types::PaymentsResponseData>: Feature<F, FData>,
|
|
dyn api::Connector: services::api::ConnectorIntegration<F, FData, types::PaymentsResponseData>,
|
|
{
|
|
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::<Vec<types::Connector>>(&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<i32> {
|
|
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::<i32>()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Retries config parsing failed")
|
|
})
|
|
.map_err(|err| {
|
|
logger::error!(retries_error=?err);
|
|
None::<i32>
|
|
})
|
|
.ok()
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[instrument(skip_all)]
|
|
pub async fn get_retries(
|
|
state: &app::SessionState,
|
|
retries: Option<i32>,
|
|
merchant_id: &common_utils::id_type::MerchantId,
|
|
profile: &domain::Profile,
|
|
) -> Option<i32> {
|
|
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<F, FData>(
|
|
state: &app::SessionState,
|
|
router_data: &types::RouterData<F, FData, types::PaymentsResponseData>,
|
|
) -> RouterResult<Option<hyperswitch_domain_models::gsm::GatewayStatusMap>> {
|
|
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::<F>()?;
|
|
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<hyperswitch_domain_models::gsm::GatewayStatusMap>,
|
|
) -> 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<F>() -> RouterResult<String> {
|
|
Ok(std::any::type_name::<F>()
|
|
.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<domain::Customer>,
|
|
merchant_context: &domain::MerchantContext,
|
|
payment_data: &'a mut D,
|
|
router_data: types::RouterData<F, FData, types::PaymentsResponseData>,
|
|
validate_result: &operations::ValidateResult,
|
|
schedule_time: Option<time::PrimitiveDateTime>,
|
|
is_step_up: bool,
|
|
frm_suggestion: Option<storage_enums::FrmSuggestion>,
|
|
business_profile: &domain::Profile,
|
|
should_retry_with_pan: bool,
|
|
routing_decision: Option<routing_helpers::RoutingDecisionData>,
|
|
) -> RouterResult<types::RouterData<F, FData, types::PaymentsResponseData>>
|
|
where
|
|
F: Clone + Send + Sync + 'static + 'a,
|
|
FData: Send + Sync + types::Capturable + Clone + 'static + 'a,
|
|
payments::PaymentResponse: operations::Operation<F, FData>,
|
|
D: payments::OperationSessionGetters<F>
|
|
+ payments::OperationSessionSetters<F>
|
|
+ Send
|
|
+ Sync
|
|
+ Clone,
|
|
D: ConstructFlowSpecificData<F, FData, types::PaymentsResponseData>,
|
|
types::RouterData<F, FData, types::PaymentsResponseData>: Feature<F, FData>,
|
|
dyn api::Connector: services::api::ConnectorIntegration<F, FData, types::PaymentsResponseData>,
|
|
{
|
|
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<F, FData, D>(
|
|
state: &routes::SessionState,
|
|
connector: String,
|
|
payment_data: &mut D,
|
|
key_store: &domain::MerchantKeyStore,
|
|
storage_scheme: storage_enums::MerchantStorageScheme,
|
|
router_data: types::RouterData<F, FData, types::PaymentsResponseData>,
|
|
is_step_up: bool,
|
|
) -> RouterResult<()>
|
|
where
|
|
F: Clone + Send,
|
|
FData: Send,
|
|
D: payments::OperationSessionGetters<F> + payments::OperationSessionSetters<F> + Send + Sync,
|
|
{
|
|
todo!()
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[instrument(skip_all)]
|
|
pub async fn modify_trackers<F, FData, D>(
|
|
state: &routes::SessionState,
|
|
connector: String,
|
|
payment_data: &mut D,
|
|
key_store: &domain::MerchantKeyStore,
|
|
storage_scheme: storage_enums::MerchantStorageScheme,
|
|
router_data: types::RouterData<F, FData, types::PaymentsResponseData>,
|
|
is_step_up: bool,
|
|
) -> RouterResult<()>
|
|
where
|
|
F: Clone + Send,
|
|
FData: Send + types::Capturable,
|
|
D: payments::OperationSessionGetters<F> + payments::OperationSessionSetters<F> + 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_enums::FutureUsage>,
|
|
) -> 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<F: Send + Clone + Sync, FData: Send + Sync, Resp> {
|
|
// TODO : move this function to appropriate place later.
|
|
fn should_call_gsm(&self) -> bool;
|
|
}
|
|
|
|
impl<F: Send + Clone + Sync, FData: Send + Sync>
|
|
GsmValidation<F, FData, types::PaymentsResponseData>
|
|
for types::RouterData<F, FData, types::PaymentsResponseData>
|
|
{
|
|
#[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,
|
|
}
|
|
}
|
|
}
|
|
}
|