Files
Apoorv Dixit c3361ef5eb feat(payments): get new filters for payments list (#4174)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Sampras Lopes <lsampras@pm.me>
2024-04-16 10:01:28 +00:00

3802 lines
141 KiB
Rust

pub mod access_token;
pub mod conditional_configs;
pub mod customers;
pub mod flows;
pub mod helpers;
pub mod operations;
#[cfg(feature = "retry")]
pub mod retry;
pub mod routing;
pub mod tokenization;
pub mod transformers;
pub mod types;
#[cfg(feature = "olap")]
use std::collections::{HashMap, HashSet};
use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoIter};
#[cfg(feature = "olap")]
use api_models::admin::MerchantConnectorInfo;
use api_models::{
self, enums,
mandates::RecurringDetails,
payments::{self as payments_api, HeaderPayload},
};
use common_utils::{ext_traits::AsyncExt, pii, types::Surcharge};
use data_models::mandates::{CustomerAcceptance, MandateData};
use diesel_models::{ephemeral_key, fraud_check::FraudCheck};
use error_stack::{report, ResultExt};
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};
#[cfg(feature = "olap")]
use router_types::transformers::ForeignFrom;
use scheduler::utils as pt_utils;
#[cfg(feature = "olap")]
use strum::IntoEnumIterator;
use time;
pub use self::operations::{
PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate,
PaymentIncrementalAuthorization, PaymentReject, PaymentResponse, PaymentSession, PaymentStatus,
PaymentUpdate,
};
use self::{
conditional_configs::perform_decision_management,
flows::{ConstructFlowSpecificData, Feature},
helpers::get_key_params_for_surcharge_details,
operations::{payment_complete_authorize, BoxedOperation, Operation},
routing::{self as self_routing, SessionFlowRoutingInput},
};
use super::{
errors::StorageErrorExt, payment_methods::surcharge_decision_configs, routing::TransactionData,
};
#[cfg(feature = "frm")]
use crate::core::fraud_check as frm_core;
use crate::{
configs::settings::{ApplePayPreDecryptFlow, PaymentMethodTypeTokenFilter},
core::{
authentication as authentication_core,
errors::{self, CustomResult, RouterResponse, RouterResult},
payment_methods::PaymentMethodRetrieve,
utils,
},
db::StorageInterface,
logger,
routes::{app::ReqState, metrics, payment_methods::ParentPaymentMethodToken, AppState},
services::{self, api::Authenticate},
types::{
self as router_types,
api::{self, authentication, ConnectorCallType},
domain,
storage::{self, enums as storage_enums, payment_attempt::PaymentAttemptExt},
transformers::{ForeignInto, ForeignTryInto},
BrowserInformation,
},
utils::{
add_apple_pay_flow_metrics, add_connector_http_status_code_metrics, Encode, OptionExt,
ValueExt,
},
workflows::payment_sync,
};
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
#[instrument(skip_all, fields(payment_id, merchant_id))]
pub async fn payments_operation_core<F, Req, Op, FData, Ctx>(
state: &AppState,
req_state: ReqState,
merchant_account: domain::MerchantAccount,
key_store: domain::MerchantKeyStore,
operation: Op,
req: Req,
call_connector_action: CallConnectorAction,
auth_flow: services::AuthFlow,
eligible_connectors: Option<Vec<common_enums::RoutableConnectors>>,
header_payload: HeaderPayload,
) -> RouterResult<(
PaymentData<F>,
Req,
Option<domain::Customer>,
Option<u16>,
Option<u128>,
)>
where
F: Send + Clone + Sync,
Req: Authenticate + Clone,
Op: Operation<F, Req, Ctx> + Send + Sync,
// To create connector flow specific interface data
PaymentData<F>: ConstructFlowSpecificData<F, FData, router_types::PaymentsResponseData>,
router_types::RouterData<F, FData, router_types::PaymentsResponseData>: Feature<F, FData>,
// To construct connector flow specific api
dyn router_types::api::Connector:
services::api::ConnectorIntegration<F, FData, router_types::PaymentsResponseData>,
// To perform router related operation for PaymentResponse
PaymentResponse: Operation<F, FData, Ctx>,
FData: Send + Sync,
Ctx: PaymentMethodRetrieve,
{
let operation: BoxedOperation<'_, F, Req, Ctx> = Box::new(operation);
tracing::Span::current().record("merchant_id", merchant_account.merchant_id.as_str());
let (operation, validate_result) = operation
.to_validate_request()?
.validate_request(&req, &merchant_account)?;
tracing::Span::current().record("payment_id", &format!("{}", validate_result.payment_id));
let operations::GetTrackerResponse {
operation,
customer_details,
mut payment_data,
business_profile,
mandate_type,
} = operation
.to_get_tracker()?
.get_trackers(
state,
&validate_result.payment_id,
&req,
&merchant_account,
&key_store,
auth_flow,
header_payload.payment_confirm_source,
)
.await?;
let (operation, customer) = operation
.to_domain()?
.get_or_create_customer_details(
&*state.store,
&mut payment_data,
customer_details,
&key_store,
merchant_account.storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)
.attach_printable("Failed while fetching/creating customer")?;
call_decision_manager(state, &merchant_account, &mut payment_data).await?;
let connector = get_connector_choice(
&operation,
state,
&req,
&merchant_account,
&business_profile,
&key_store,
&mut payment_data,
eligible_connectors,
mandate_type,
)
.await?;
let should_add_task_to_process_tracker = should_add_task_to_process_tracker(&payment_data);
payment_data = tokenize_in_router_when_confirm_false_or_external_authentication(
state,
&operation,
&mut payment_data,
&validate_result,
&key_store,
&customer,
)
.await?;
let mut connector_http_status_code = None;
let mut external_latency = None;
if let Some(connector_details) = connector {
// Fetch and check FRM configs
#[cfg(feature = "frm")]
let mut frm_info = None;
#[cfg(feature = "frm")]
let db = &*state.store;
#[allow(unused_variables, unused_mut)]
let mut should_continue_transaction: bool = true;
#[cfg(feature = "frm")]
let mut should_continue_capture: bool = true;
#[cfg(feature = "frm")]
let frm_configs = if state.conf.frm.enabled {
Box::pin(frm_core::call_frm_before_connector_call(
db,
&operation,
&merchant_account,
&mut payment_data,
state,
&mut frm_info,
&customer,
&mut should_continue_transaction,
&mut should_continue_capture,
key_store.clone(),
))
.await?
} else {
None
};
#[cfg(feature = "frm")]
logger::debug!(
"frm_configs: {:?}\nshould_cancel_transaction: {:?}\nshould_continue_capture: {:?}",
frm_configs,
should_continue_transaction,
should_continue_capture,
);
operation
.to_domain()?
.call_external_three_ds_authentication_if_eligible(
state,
&mut payment_data,
&mut should_continue_transaction,
&connector_details,
&business_profile,
&key_store,
)
.await?;
if should_continue_transaction {
#[cfg(feature = "frm")]
match (
should_continue_capture,
payment_data.payment_attempt.capture_method,
) {
(false, Some(storage_enums::CaptureMethod::Automatic))
| (false, Some(storage_enums::CaptureMethod::Scheduled)) => {
payment_data.payment_attempt.capture_method =
Some(storage_enums::CaptureMethod::Manual);
}
_ => (),
};
payment_data = match connector_details {
api::ConnectorCallType::PreDetermined(connector) => {
let schedule_time = if should_add_task_to_process_tracker {
payment_sync::get_sync_process_schedule_time(
&*state.store,
connector.connector.id(),
&merchant_account.merchant_id,
0,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed while getting process schedule time")?
} else {
None
};
let router_data = call_connector_service(
state,
req_state,
&merchant_account,
&key_store,
connector,
&operation,
&mut payment_data,
&customer,
call_connector_action,
&validate_result,
schedule_time,
header_payload,
#[cfg(feature = "frm")]
frm_info.as_ref().and_then(|fi| fi.suggested_action),
#[cfg(not(feature = "frm"))]
None,
)
.await?;
let operation = Box::new(PaymentResponse);
connector_http_status_code = router_data.connector_http_status_code;
external_latency = router_data.external_latency;
//add connector http status code metrics
add_connector_http_status_code_metrics(connector_http_status_code);
operation
.to_post_update_tracker()?
.update_tracker(
state,
&validate_result.payment_id,
payment_data,
router_data,
merchant_account.storage_scheme,
)
.await?
}
api::ConnectorCallType::Retryable(connectors) => {
let mut connectors = connectors.into_iter();
let connector_data = get_connector_data(&mut connectors)?;
let schedule_time = if should_add_task_to_process_tracker {
payment_sync::get_sync_process_schedule_time(
&*state.store,
connector_data.connector.id(),
&merchant_account.merchant_id,
0,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed while getting process schedule time")?
} else {
None
};
let router_data = call_connector_service(
state,
req_state.clone(),
&merchant_account,
&key_store,
connector_data.clone(),
&operation,
&mut payment_data,
&customer,
call_connector_action,
&validate_result,
schedule_time,
header_payload,
#[cfg(feature = "frm")]
frm_info.as_ref().and_then(|fi| fi.suggested_action),
#[cfg(not(feature = "frm"))]
None,
)
.await?;
#[cfg(feature = "retry")]
let mut router_data = router_data;
#[cfg(feature = "retry")]
{
use crate::core::payments::retry::{self, GsmValidation};
let config_bool = retry::config_should_call_gsm(
&*state.store,
&merchant_account.merchant_id,
)
.await;
if config_bool && router_data.should_call_gsm() {
router_data = retry::do_gsm_actions(
state,
req_state,
&mut payment_data,
connectors,
connector_data,
router_data,
&merchant_account,
&key_store,
&operation,
&customer,
&validate_result,
schedule_time,
#[cfg(feature = "frm")]
frm_info.as_ref().and_then(|fi| fi.suggested_action),
#[cfg(not(feature = "frm"))]
None,
)
.await?;
};
}
let operation = Box::new(PaymentResponse);
connector_http_status_code = router_data.connector_http_status_code;
external_latency = router_data.external_latency;
//add connector http status code metrics
add_connector_http_status_code_metrics(connector_http_status_code);
operation
.to_post_update_tracker()?
.update_tracker(
state,
&validate_result.payment_id,
payment_data,
router_data,
merchant_account.storage_scheme,
)
.await?
}
api::ConnectorCallType::SessionMultiple(connectors) => {
let session_surcharge_details =
call_surcharge_decision_management_for_session_flow(
state,
&merchant_account,
&mut payment_data,
&connectors,
)
.await?;
Box::pin(call_multiple_connectors_service(
state,
&merchant_account,
&key_store,
connectors,
&operation,
payment_data,
&customer,
session_surcharge_details,
))
.await?
}
};
#[cfg(feature = "frm")]
if let Some(fraud_info) = &mut frm_info {
Box::pin(frm_core::post_payment_frm_core(
state,
&merchant_account,
&mut payment_data,
fraud_info,
frm_configs
.clone()
.ok_or(errors::ApiErrorResponse::MissingRequiredField {
field_name: "frm_configs",
})
.attach_printable("Frm configs label not found")?,
&customer,
key_store.clone(),
))
.await?;
}
} else {
(_, payment_data) = operation
.to_update_tracker()?
.update_trackers(
state,
req_state,
payment_data.clone(),
customer.clone(),
validate_result.storage_scheme,
None,
&key_store,
#[cfg(feature = "frm")]
frm_info.and_then(|info| info.suggested_action),
#[cfg(not(feature = "frm"))]
None,
header_payload,
)
.await?;
}
payment_data
.payment_attempt
.payment_token
.as_ref()
.zip(payment_data.payment_attempt.payment_method)
.map(ParentPaymentMethodToken::create_key_for_token)
.async_map(|key_for_hyperswitch_token| async move {
if key_for_hyperswitch_token
.should_delete_payment_method_token(payment_data.payment_intent.status)
{
let _ = key_for_hyperswitch_token.delete(state).await;
}
})
.await;
} else {
(_, payment_data) = operation
.to_update_tracker()?
.update_trackers(
state,
req_state,
payment_data.clone(),
customer.clone(),
validate_result.storage_scheme,
None,
&key_store,
None,
header_payload,
)
.await?;
}
let cloned_payment_data = payment_data.clone();
let cloned_customer = customer.clone();
crate::utils::trigger_payments_webhook(
merchant_account,
business_profile,
&key_store,
cloned_payment_data,
cloned_customer,
state,
operation,
)
.await
.map_err(|error| logger::warn!(payments_outgoing_webhook_error=?error))
.ok();
Ok((
payment_data,
req,
customer,
connector_http_status_code,
external_latency,
))
}
#[instrument(skip_all)]
pub async fn call_decision_manager<O>(
state: &AppState,
merchant_account: &domain::MerchantAccount,
payment_data: &mut PaymentData<O>,
) -> RouterResult<()>
where
O: Send + Clone,
{
let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account
.routing_algorithm
.clone()
.map(|val| val.parse_value("routing algorithm"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not decode the routing algorithm")?
.unwrap_or_default();
let output = perform_decision_management(
state,
algorithm_ref,
merchant_account.merchant_id.as_str(),
payment_data,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not decode the conditional config")?;
payment_data.payment_attempt.authentication_type = payment_data
.payment_attempt
.authentication_type
.or(output.override_3ds.map(ForeignInto::foreign_into))
.or(Some(storage_enums::AuthenticationType::NoThreeDs));
Ok(())
}
#[instrument(skip_all)]
async fn populate_surcharge_details<F>(
state: &AppState,
payment_data: &mut PaymentData<F>,
) -> RouterResult<()>
where
F: Send + Clone,
{
if payment_data
.payment_intent
.surcharge_applicable
.unwrap_or(false)
{
if let Some(surcharge_details) = payment_data.payment_attempt.get_surcharge_details() {
// if retry payment, surcharge would have been populated from the previous attempt. Use the same surcharge
let surcharge_details =
types::SurchargeDetails::from((&surcharge_details, &payment_data.payment_attempt));
payment_data.surcharge_details = Some(surcharge_details);
return Ok(());
}
let raw_card_key = payment_data
.payment_method_data
.as_ref()
.and_then(get_key_params_for_surcharge_details)
.map(|(payment_method, payment_method_type, card_network)| {
types::SurchargeKey::PaymentMethodData(
payment_method,
payment_method_type,
card_network,
)
});
let saved_card_key = payment_data.token.clone().map(types::SurchargeKey::Token);
let surcharge_key = raw_card_key
.or(saved_card_key)
.get_required_value("payment_method_data or payment_token")?;
logger::debug!(surcharge_key_confirm =? surcharge_key);
let calculated_surcharge_details =
match types::SurchargeMetadata::get_individual_surcharge_detail_from_redis(
state,
surcharge_key,
&payment_data.payment_attempt.attempt_id,
)
.await
{
Ok(surcharge_details) => Some(surcharge_details),
Err(err) if err.current_context() == &RedisError::NotFound => None,
Err(err) => {
Err(err).change_context(errors::ApiErrorResponse::InternalServerError)?
}
};
payment_data.surcharge_details = calculated_surcharge_details;
} else {
let surcharge_details =
payment_data
.payment_attempt
.get_surcharge_details()
.map(|surcharge_details| {
types::SurchargeDetails::from((
&surcharge_details,
&payment_data.payment_attempt,
))
});
payment_data.surcharge_details = surcharge_details;
}
Ok(())
}
#[inline]
pub fn get_connector_data(
connectors: &mut IntoIter<api::ConnectorData>,
) -> RouterResult<api::ConnectorData> {
connectors
.next()
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Connector not found in connectors iterator")
}
#[instrument(skip_all)]
pub async fn call_surcharge_decision_management_for_session_flow<O>(
state: &AppState,
merchant_account: &domain::MerchantAccount,
payment_data: &mut PaymentData<O>,
session_connector_data: &[api::SessionConnectorData],
) -> RouterResult<Option<api::SessionSurchargeDetails>>
where
O: Send + Clone + Sync,
{
if let Some(surcharge_amount) = payment_data.payment_attempt.surcharge_amount {
let tax_on_surcharge_amount = payment_data.payment_attempt.tax_amount.unwrap_or(0);
let final_amount =
payment_data.payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount;
Ok(Some(api::SessionSurchargeDetails::PreDetermined(
types::SurchargeDetails {
original_amount: payment_data.payment_attempt.amount,
surcharge: Surcharge::Fixed(surcharge_amount),
tax_on_surcharge: None,
surcharge_amount,
tax_on_surcharge_amount,
final_amount,
},
)))
} else {
let payment_method_type_list = session_connector_data
.iter()
.map(|session_connector_data| session_connector_data.payment_method_type)
.collect();
let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account
.routing_algorithm
.clone()
.map(|val| val.parse_value("routing algorithm"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not decode the routing algorithm")?
.unwrap_or_default();
let surcharge_results =
surcharge_decision_configs::perform_surcharge_decision_management_for_session_flow(
state,
algorithm_ref,
payment_data,
&payment_method_type_list,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("error performing surcharge decision operation")?;
Ok(if surcharge_results.is_empty_result() {
None
} else {
Some(api::SessionSurchargeDetails::Calculated(surcharge_results))
})
}
}
#[allow(clippy::too_many_arguments)]
pub async fn payments_core<F, Res, Req, Op, FData, Ctx>(
state: AppState,
req_state: ReqState,
merchant_account: domain::MerchantAccount,
key_store: domain::MerchantKeyStore,
operation: Op,
req: Req,
auth_flow: services::AuthFlow,
call_connector_action: CallConnectorAction,
eligible_connectors: Option<Vec<api_models::enums::Connector>>,
header_payload: HeaderPayload,
) -> RouterResponse<Res>
where
F: Send + Clone + Sync,
FData: Send + Sync,
Op: Operation<F, Req, Ctx> + Send + Sync + Clone,
Req: Debug + Authenticate + Clone,
Res: transformers::ToResponse<PaymentData<F>, Op>,
// To create connector flow specific interface data
PaymentData<F>: ConstructFlowSpecificData<F, FData, router_types::PaymentsResponseData>,
router_types::RouterData<F, FData, router_types::PaymentsResponseData>: Feature<F, FData>,
Ctx: PaymentMethodRetrieve,
// To construct connector flow specific api
dyn router_types::api::Connector:
services::api::ConnectorIntegration<F, FData, router_types::PaymentsResponseData>,
// To perform router related operation for PaymentResponse
PaymentResponse: Operation<F, FData, Ctx>,
{
let eligible_routable_connectors = eligible_connectors.map(|connectors| {
connectors
.into_iter()
.flat_map(|c| c.foreign_try_into())
.collect()
});
let (payment_data, _req, customer, connector_http_status_code, external_latency) =
payments_operation_core::<_, _, _, _, Ctx>(
&state,
req_state,
merchant_account,
key_store,
operation.clone(),
req,
call_connector_action,
auth_flow,
eligible_routable_connectors,
header_payload,
)
.await?;
Res::generate_response(
payment_data,
customer,
auth_flow,
&state.conf.server,
operation,
&state.conf.connector_request_reference_id_config,
connector_http_status_code,
external_latency,
header_payload.x_hs_latency,
)
}
fn is_start_pay<Op: Debug>(operation: &Op) -> bool {
format!("{operation:?}").eq("PaymentStart")
}
#[derive(Clone, Debug, serde::Serialize)]
pub struct PaymentsRedirectResponseData {
pub connector: Option<String>,
pub param: Option<String>,
pub merchant_id: Option<String>,
pub json_payload: Option<serde_json::Value>,
pub resource_id: api::PaymentIdType,
pub force_sync: bool,
pub creds_identifier: Option<String>,
}
#[async_trait::async_trait]
pub trait PaymentRedirectFlow<Ctx: PaymentMethodRetrieve>: Sync {
async fn call_payment_flow(
&self,
state: &AppState,
req_state: ReqState,
merchant_account: domain::MerchantAccount,
merchant_key_store: domain::MerchantKeyStore,
req: PaymentsRedirectResponseData,
connector_action: CallConnectorAction,
) -> RouterResponse<api::PaymentsResponse>;
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_id: String,
connector: String,
) -> RouterResult<services::ApplicationResponse<api::RedirectionResponse>>;
#[allow(clippy::too_many_arguments)]
async fn handle_payments_redirect_response(
&self,
state: AppState,
req_state: ReqState,
merchant_account: domain::MerchantAccount,
key_store: domain::MerchantKeyStore,
req: PaymentsRedirectResponseData,
) -> RouterResponse<api::RedirectionResponse> {
metrics::REDIRECTION_TRIGGERED.add(
&metrics::CONTEXT,
1,
&[
metrics::request::add_attributes(
"connector",
req.connector.to_owned().unwrap_or("null".to_string()),
),
metrics::request::add_attributes(
"merchant_id",
merchant_account.merchant_id.to_owned(),
),
],
);
let connector = req.connector.clone().get_required_value("connector")?;
let query_params = req.param.clone().get_required_value("param")?;
let resource_id = api::PaymentIdTypeExt::get_payment_intent_id(&req.resource_id)
.change_context(errors::ApiErrorResponse::MissingRequiredField {
field_name: "payment_id",
})?;
// This connector data is ephemeral, the call payment flow will get new connector data
// with merchant account details, so the connector_id can be safely set to None here
let connector_data = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
&connector,
api::GetToken::Connector,
None,
)?;
let flow_type = connector_data
.connector
.get_flow_type(
&query_params,
req.json_payload.clone(),
self.get_payment_action(),
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to decide the response flow")?;
let response = self
.call_payment_flow(
&state,
req_state,
merchant_account.clone(),
key_store,
req.clone(),
flow_type,
)
.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)
}
}
#[derive(Clone, Debug)]
pub struct PaymentRedirectCompleteAuthorize;
#[async_trait::async_trait]
impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentRedirectCompleteAuthorize {
async fn call_payment_flow(
&self,
state: &AppState,
req_state: ReqState,
merchant_account: domain::MerchantAccount,
merchant_key_store: domain::MerchantKeyStore,
req: PaymentsRedirectResponseData,
connector_action: CallConnectorAction,
) -> RouterResponse<api::PaymentsResponse> {
let payment_confirm_req = api::PaymentsRequest {
payment_id: Some(req.resource_id.clone()),
merchant_id: req.merchant_id.clone(),
feature_metadata: Some(api_models::payments::FeatureMetadata {
redirect_response: Some(api_models::payments::RedirectResponse {
param: req.param.map(Secret::new),
json_payload: Some(req.json_payload.unwrap_or(serde_json::json!({})).into()),
}),
}),
..Default::default()
};
Box::pin(payments_core::<
api::CompleteAuthorize,
api::PaymentsResponse,
_,
_,
_,
Ctx,
>(
state.clone(),
req_state,
merchant_account,
merchant_key_store,
payment_complete_authorize::CompleteAuthorize,
payment_confirm_req,
services::api::AuthFlow::Merchant,
connector_action,
None,
HeaderPayload::default(),
))
.await
}
fn get_payment_action(&self) -> services::PaymentAction {
services::PaymentAction::CompleteAuthorize
}
fn generate_response(
&self,
payments_response: &api_models::payments::PaymentsResponse,
business_profile: diesel_models::business_profile::BusinessProfile,
payment_id: String,
connector: String,
) -> RouterResult<services::ApplicationResponse<api::RedirectionResponse>> {
// 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
let redirection_response = match payments_response.status {
api_models::enums::IntentStatus::RequiresCustomerAction => {
let startpay_url = payments_response
.next_action
.clone()
.and_then(|next_action_data| match next_action_data {
api_models::payments::NextActionData::RedirectToUrl { redirect_to_url } => Some(redirect_to_url),
api_models::payments::NextActionData::DisplayBankTransferInformation { .. } => None,
api_models::payments::NextActionData::ThirdPartySdkSessionToken { .. } => None,
api_models::payments::NextActionData::QrCodeInformation{..} => None,
api_models::payments::NextActionData::DisplayVoucherInformation{ .. } => None,
api_models::payments::NextActionData::WaitScreenInformation{..} => None,
api_models::payments::NextActionData::ThreeDsInvoke{..} => None,
})
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"did not receive redirect to url when status is requires customer action",
)?;
Ok(api::RedirectionResponse {
return_url: String::new(),
params: vec![],
return_url_with_query_params: startpay_url,
http_method: "GET".to_string(),
headers: vec![],
})
}
// If the status is terminal status, then redirect to merchant return url to provide status
api_models::enums::IntentStatus::Succeeded
| 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,
payments_response,
connector,
),
_ => Err(errors::ApiErrorResponse::InternalServerError).attach_printable_lazy(|| format!("Could not proceed with payment as payment status {} cannot be handled during redirection",payments_response.status))?
}?;
Ok(services::ApplicationResponse::JsonForRedirection(
redirection_response,
))
}
}
#[derive(Clone, Debug)]
pub struct PaymentRedirectSync;
#[async_trait::async_trait]
impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentRedirectSync {
async fn call_payment_flow(
&self,
state: &AppState,
req_state: ReqState,
merchant_account: domain::MerchantAccount,
merchant_key_store: domain::MerchantKeyStore,
req: PaymentsRedirectResponseData,
connector_action: CallConnectorAction,
) -> RouterResponse<api::PaymentsResponse> {
let payment_sync_req = api::PaymentsRetrieveRequest {
resource_id: req.resource_id,
merchant_id: req.merchant_id,
param: req.param,
force_sync: req.force_sync,
connector: req.connector,
merchant_connector_details: req.creds_identifier.map(|creds_id| {
api::MerchantConnectorDetailsWrap {
creds_identifier: creds_id,
encoded_data: None,
}
}),
client_secret: None,
expand_attempts: None,
expand_captures: None,
};
Box::pin(payments_core::<
api::PSync,
api::PaymentsResponse,
_,
_,
_,
Ctx,
>(
state.clone(),
req_state,
merchant_account,
merchant_key_store,
PaymentStatus,
payment_sync_req,
services::api::AuthFlow::Merchant,
connector_action,
None,
HeaderPayload::default(),
))
.await
}
fn generate_response(
&self,
payments_response: &api_models::payments::PaymentsResponse,
business_profile: diesel_models::business_profile::BusinessProfile,
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,
connector,
)?,
))
}
fn get_payment_action(&self) -> services::PaymentAction {
services::PaymentAction::PSync
}
}
#[derive(Clone, Debug)]
pub struct PaymentAuthenticateCompleteAuthorize;
#[async_trait::async_trait]
impl<Ctx: PaymentMethodRetrieve> PaymentRedirectFlow<Ctx> for PaymentAuthenticateCompleteAuthorize {
async fn call_payment_flow(
&self,
state: &AppState,
req_state: ReqState,
merchant_account: domain::MerchantAccount,
merchant_key_store: domain::MerchantKeyStore,
req: PaymentsRedirectResponseData,
connector_action: CallConnectorAction,
) -> RouterResponse<api::PaymentsResponse> {
let payment_confirm_req = api::PaymentsRequest {
payment_id: Some(req.resource_id.clone()),
merchant_id: req.merchant_id.clone(),
feature_metadata: Some(api_models::payments::FeatureMetadata {
redirect_response: Some(api_models::payments::RedirectResponse {
param: req.param.map(Secret::new),
json_payload: Some(req.json_payload.unwrap_or(serde_json::json!({})).into()),
}),
}),
..Default::default()
};
Box::pin(payments_core::<
api::Authorize,
api::PaymentsResponse,
_,
_,
_,
Ctx,
>(
state.clone(),
req_state,
merchant_account,
merchant_key_store,
PaymentConfirm,
payment_confirm_req,
services::api::AuthFlow::Merchant,
connector_action,
None,
HeaderPayload::with_source(enums::PaymentSource::ExternalAuthenticator),
))
.await
}
fn generate_response(
&self,
payments_response: &api_models::payments::PaymentsResponse,
business_profile: diesel_models::business_profile::BusinessProfile,
payment_id: String,
connector: String,
) -> RouterResult<services::ApplicationResponse<api::RedirectionResponse>> {
let redirect_response = helpers::get_handle_response_url(
payment_id,
&business_profile,
payments_response,
connector,
)?;
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();
Ok(services::ApplicationResponse::Form(Box::new(
services::RedirectionFormData {
redirect_form: services::RedirectForm::Html { html_data: html },
payment_method_data: None,
amount: payments_response.amount.to_string(),
currency: payments_response.currency.clone(),
},
)))
}
fn get_payment_action(&self) -> services::PaymentAction {
services::PaymentAction::CompleteAuthorize
}
}
#[allow(clippy::too_many_arguments)]
#[instrument(skip_all)]
pub async fn call_connector_service<F, RouterDReq, ApiRequest, Ctx>(
state: &AppState,
req_state: ReqState,
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
connector: api::ConnectorData,
operation: &BoxedOperation<'_, F, ApiRequest, Ctx>,
payment_data: &mut PaymentData<F>,
customer: &Option<domain::Customer>,
call_connector_action: CallConnectorAction,
validate_result: &operations::ValidateResult<'_>,
schedule_time: Option<time::PrimitiveDateTime>,
header_payload: HeaderPayload,
frm_suggestion: Option<storage_enums::FrmSuggestion>,
) -> RouterResult<router_types::RouterData<F, RouterDReq, router_types::PaymentsResponseData>>
where
F: Send + Clone + Sync,
RouterDReq: Send + Sync,
// To create connector flow specific interface data
PaymentData<F>: ConstructFlowSpecificData<F, RouterDReq, router_types::PaymentsResponseData>,
router_types::RouterData<F, RouterDReq, router_types::PaymentsResponseData>:
Feature<F, RouterDReq> + Send,
Ctx: PaymentMethodRetrieve,
// To construct connector flow specific api
dyn api::Connector:
services::api::ConnectorIntegration<F, RouterDReq, router_types::PaymentsResponseData>,
{
let stime_connector = Instant::now();
let merchant_connector_account = construct_profile_id_and_get_mca(
state,
merchant_account,
payment_data,
&connector.connector_name.to_string(),
connector.merchant_connector_id.as_ref(),
key_store,
false,
)
.await?;
if payment_data.payment_attempt.merchant_connector_id.is_none() {
payment_data.payment_attempt.merchant_connector_id =
merchant_connector_account.get_mca_id();
}
operation
.to_domain()?
.populate_payment_data(state, payment_data, merchant_account)
.await?;
let (pd, tokenization_action) = get_connector_tokenization_action_when_confirm_true(
state,
operation,
payment_data,
validate_result,
&merchant_connector_account,
key_store,
customer,
)
.await?;
*payment_data = pd;
// Validating the blocklist guard and generate the fingerprint
blocklist_guard(state, merchant_account, operation, payment_data).await?;
let updated_customer = call_create_connector_customer_if_required(
state,
customer,
merchant_account,
key_store,
&merchant_connector_account,
payment_data,
)
.await?;
let mut router_data = payment_data
.construct_router_data(
state,
connector.connector.id(),
merchant_account,
key_store,
customer,
&merchant_connector_account,
)
.await?;
let add_access_token_result = router_data
.add_access_token(state, &connector, merchant_account)
.await?;
let mut should_continue_further = access_token::update_router_data_with_access_token_result(
&add_access_token_result,
&mut router_data,
&call_connector_action,
);
// Tokenization Action will be DecryptApplePayToken, only when payment method type is Apple Pay
// and the connector supports Apple Pay predecrypt
if matches!(
tokenization_action,
TokenizationAction::DecryptApplePayToken
| TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt
) {
let apple_pay_data = match payment_data.payment_method_data.clone() {
Some(payment_data) => {
let domain_data = domain::PaymentMethodData::from(payment_data);
match domain_data {
domain::PaymentMethodData::Wallet(domain::WalletData::ApplePay(
wallet_data,
)) => Some(
ApplePayData::token_json(domain::WalletData::ApplePay(wallet_data))
.change_context(errors::ApiErrorResponse::InternalServerError)?
.decrypt(state)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?,
),
_ => None,
}
}
_ => None,
};
let apple_pay_predecrypt = apple_pay_data
.parse_value::<router_types::ApplePayPredecryptData>("ApplePayPredecryptData")
.change_context(errors::ApiErrorResponse::InternalServerError)?;
logger::debug!(?apple_pay_predecrypt);
router_data.payment_method_token = Some(router_types::PaymentMethodToken::ApplePayDecrypt(
Box::new(apple_pay_predecrypt),
));
}
let pm_token = router_data
.add_payment_method_token(state, &connector, &tokenization_action)
.await?;
if let Some(payment_method_token) = pm_token.clone() {
router_data.payment_method_token = Some(router_types::PaymentMethodToken::Token(
payment_method_token,
));
};
(router_data, should_continue_further) = complete_preprocessing_steps_if_required(
state,
&connector,
payment_data,
router_data,
operation,
should_continue_further,
)
.await?;
if let Ok(router_types::PaymentsResponseData::PreProcessingResponse {
session_token: Some(session_token),
..
}) = router_data.response.to_owned()
{
payment_data.sessions_token.push(session_token);
};
// In case of authorize flow, pre-task and post-tasks are being called in build request
// if we do not want to proceed further, then the function will return Ok(None, false)
let (connector_request, should_continue_further) = if should_continue_further {
// Check if the actual flow specific request can be built with available data
router_data
.build_flow_specific_connector_request(state, &connector, call_connector_action.clone())
.await?
} else {
(None, false)
};
if should_add_task_to_process_tracker(payment_data) {
operation
.to_domain()?
.add_task_to_process_tracker(
state,
&payment_data.payment_attempt,
validate_result.requeue,
schedule_time,
)
.await
.map_err(|error| logger::error!(process_tracker_error=?error))
.ok();
}
// Update the payment trackers just before calling the connector
// Since the request is already built in the previous step,
// there should be no error in request construction from hyperswitch end
(_, *payment_data) = operation
.to_update_tracker()?
.update_trackers(
state,
req_state,
payment_data.clone(),
customer.clone(),
merchant_account.storage_scheme,
updated_customer,
key_store,
frm_suggestion,
header_payload,
)
.await?;
let router_data_res = if should_continue_further {
// The status of payment_attempt and intent will be updated in the previous step
// update this in router_data.
// This is added because few connector integrations do not update the status,
// and rely on previous status set in router_data
router_data.status = payment_data.payment_attempt.status;
router_data
.decide_flows(
state,
&connector,
customer,
call_connector_action,
merchant_account,
connector_request,
key_store,
payment_data.payment_intent.profile_id.clone(),
)
.await
} else {
Ok(router_data)
};
let etime_connector = Instant::now();
let duration_connector = etime_connector.saturating_duration_since(stime_connector);
tracing::info!(duration = format!("Duration taken: {}", duration_connector.as_millis()));
router_data_res
}
async fn blocklist_guard<F, ApiRequest, Ctx>(
state: &AppState,
merchant_account: &domain::MerchantAccount,
operation: &BoxedOperation<'_, F, ApiRequest, Ctx>,
payment_data: &mut PaymentData<F>,
) -> CustomResult<bool, errors::ApiErrorResponse>
where
F: Send + Clone + Sync,
Ctx: PaymentMethodRetrieve,
{
let merchant_id = &payment_data.payment_attempt.merchant_id;
let blocklist_enabled_key = format!("guard_blocklist_for_{merchant_id}");
let blocklist_guard_enabled = state
.store
.find_config_by_key_unwrap_or(&blocklist_enabled_key, Some("false".to_string()))
.await;
let blocklist_guard_enabled: bool = match blocklist_guard_enabled {
Ok(config) => serde_json::from_str(&config.config).unwrap_or(false),
// If it is not present in db we are defaulting it to false
Err(inner) => {
if !inner.current_context().is_db_not_found() {
logger::error!("Error fetching guard blocklist enabled config {:?}", inner);
}
false
}
};
if blocklist_guard_enabled {
Ok(operation
.to_domain()?
.guard_payment_against_blocklist(state, merchant_account, payment_data)
.await?)
} else {
Ok(false)
}
}
#[allow(clippy::too_many_arguments)]
pub async fn call_multiple_connectors_service<F, Op, Req, Ctx>(
state: &AppState,
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
connectors: Vec<api::SessionConnectorData>,
_operation: &Op,
mut payment_data: PaymentData<F>,
customer: &Option<domain::Customer>,
session_surcharge_details: Option<api::SessionSurchargeDetails>,
) -> RouterResult<PaymentData<F>>
where
Op: Debug,
F: Send + Clone,
// To create connector flow specific interface data
PaymentData<F>: ConstructFlowSpecificData<F, Req, router_types::PaymentsResponseData>,
router_types::RouterData<F, Req, router_types::PaymentsResponseData>: Feature<F, Req>,
// To construct connector flow specific api
dyn api::Connector:
services::api::ConnectorIntegration<F, Req, router_types::PaymentsResponseData>,
Ctx: PaymentMethodRetrieve,
// To perform router related operation for PaymentResponse
PaymentResponse: Operation<F, Req, Ctx>,
{
let call_connectors_start_time = Instant::now();
let mut join_handlers = Vec::with_capacity(connectors.len());
for session_connector_data in connectors.iter() {
let connector_id = session_connector_data.connector.connector.id();
let merchant_connector_account = construct_profile_id_and_get_mca(
state,
merchant_account,
&mut payment_data,
&session_connector_data.connector.connector_name.to_string(),
session_connector_data
.connector
.merchant_connector_id
.as_ref(),
key_store,
false,
)
.await?;
payment_data.surcharge_details =
session_surcharge_details
.as_ref()
.and_then(|session_surcharge_details| {
session_surcharge_details.fetch_surcharge_details(
&session_connector_data.payment_method_type.into(),
&session_connector_data.payment_method_type,
None,
)
});
let router_data = payment_data
.construct_router_data(
state,
connector_id,
merchant_account,
key_store,
customer,
&merchant_connector_account,
)
.await?;
let res = router_data.decide_flows(
state,
&session_connector_data.connector,
customer,
CallConnectorAction::Trigger,
merchant_account,
None,
key_store,
payment_data.payment_intent.profile_id.clone(),
);
join_handlers.push(res);
}
let result = join_all(join_handlers).await;
for (connector_res, session_connector) in result.into_iter().zip(connectors) {
let connector_name = session_connector.connector.connector_name.to_string();
match connector_res {
Ok(connector_response) => {
if let Ok(router_types::PaymentsResponseData::SessionResponse {
session_token,
..
}) = connector_response.response
{
// If session token is NoSessionTokenReceived, it is not pushed into the sessions_token as there is no response or there can be some error
// In case of error, that error is already logged
if !matches!(
session_token,
api_models::payments::SessionToken::NoSessionTokenReceived,
) {
payment_data.sessions_token.push(session_token);
}
}
}
Err(connector_error) => {
logger::error!(
"sessions_connector_error {} {:?}",
connector_name,
connector_error
);
}
}
}
let call_connectors_end_time = Instant::now();
let call_connectors_duration =
call_connectors_end_time.saturating_duration_since(call_connectors_start_time);
tracing::info!(duration = format!("Duration taken: {}", call_connectors_duration.as_millis()));
Ok(payment_data)
}
pub async fn call_create_connector_customer_if_required<F, Req>(
state: &AppState,
customer: &Option<domain::Customer>,
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
merchant_connector_account: &helpers::MerchantConnectorAccountType,
payment_data: &mut PaymentData<F>,
) -> RouterResult<Option<storage::CustomerUpdate>>
where
F: Send + Clone + Sync,
Req: Send + Sync,
// To create connector flow specific interface data
PaymentData<F>: ConstructFlowSpecificData<F, Req, router_types::PaymentsResponseData>,
router_types::RouterData<F, Req, router_types::PaymentsResponseData>: Feature<F, Req> + Send,
// To construct connector flow specific api
dyn api::Connector:
services::api::ConnectorIntegration<F, Req, router_types::PaymentsResponseData>,
{
let connector_name = payment_data.payment_attempt.connector.clone();
match connector_name {
Some(connector_name) => {
let connector = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
&connector_name,
api::GetToken::Connector,
merchant_connector_account.get_mca_id(),
)?;
let connector_label = super::utils::get_connector_label(
payment_data.payment_intent.business_country,
payment_data.payment_intent.business_label.as_ref(),
payment_data.payment_attempt.business_sub_label.as_ref(),
&connector_name,
);
let connector_label = if let Some(connector_label) =
merchant_connector_account.get_mca_id().or(connector_label)
{
connector_label
} else {
let profile_id = utils::get_profile_id_from_business_details(
payment_data.payment_intent.business_country,
payment_data.payment_intent.business_label.as_ref(),
merchant_account,
payment_data.payment_intent.profile_id.as_ref(),
&*state.store,
false,
)
.await
.attach_printable("Could not find profile id from business details")?;
format!("{connector_name}_{profile_id}")
};
let (should_call_connector, existing_connector_customer_id) =
customers::should_call_connector_create_customer(
state,
&connector,
customer,
&connector_label,
);
if should_call_connector {
// Create customer at connector and update the customer table to store this data
let router_data = payment_data
.construct_router_data(
state,
connector.connector.id(),
merchant_account,
key_store,
customer,
merchant_connector_account,
)
.await?;
let connector_customer_id = router_data
.create_connector_customer(state, &connector)
.await?;
let customer_update = customers::update_connector_customer_in_customers(
&connector_label,
customer.as_ref(),
&connector_customer_id,
)
.await;
payment_data.connector_customer_id = connector_customer_id;
Ok(customer_update)
} else {
// Customer already created in previous calls use the same value, no need to update
payment_data.connector_customer_id =
existing_connector_customer_id.map(ToOwned::to_owned);
Ok(None)
}
}
None => Ok(None),
}
}
async fn complete_preprocessing_steps_if_required<F, Req, Q, Ctx>(
state: &AppState,
connector: &api::ConnectorData,
payment_data: &PaymentData<F>,
mut router_data: router_types::RouterData<F, Req, router_types::PaymentsResponseData>,
operation: &BoxedOperation<'_, F, Q, Ctx>,
should_continue_payment: bool,
) -> RouterResult<(
router_types::RouterData<F, Req, router_types::PaymentsResponseData>,
bool,
)>
where
F: Send + Clone + Sync,
Req: Send + Sync,
router_types::RouterData<F, Req, router_types::PaymentsResponseData>: Feature<F, Req> + Send,
dyn api::Connector:
services::api::ConnectorIntegration<F, Req, router_types::PaymentsResponseData>,
{
//TODO: For ACH transfers, if preprocessing_step is not required for connectors encountered in future, add the check
let router_data_and_should_continue_payment = match payment_data.payment_method_data.clone() {
Some(api_models::payments::PaymentMethodData::BankTransfer(data)) => match data.deref() {
api_models::payments::BankTransferData::AchBankTransfer { .. }
| api_models::payments::BankTransferData::MultibancoBankTransfer { .. }
if connector.connector_name == router_types::Connector::Stripe =>
{
if payment_data.payment_attempt.preprocessing_step_id.is_none() {
(
router_data.preprocessing_steps(state, connector).await?,
false,
)
} else {
(router_data, should_continue_payment)
}
}
_ => (router_data, should_continue_payment),
},
Some(api_models::payments::PaymentMethodData::Wallet(_)) => {
if is_preprocessing_required_for_wallets(connector.connector_name.to_string()) {
(
router_data.preprocessing_steps(state, connector).await?,
false,
)
} else {
(router_data, should_continue_payment)
}
}
Some(api_models::payments::PaymentMethodData::Card(_)) => {
if connector.connector_name == router_types::Connector::Payme
&& !matches!(format!("{operation:?}").as_str(), "CompleteAuthorize")
{
router_data = router_data.preprocessing_steps(state, connector).await?;
let is_error_in_response = router_data.response.is_err();
// If is_error_in_response is true, should_continue_payment should be false, we should throw the error
(router_data, !is_error_in_response)
} else if connector.connector_name == router_types::Connector::Nmi
&& !matches!(format!("{operation:?}").as_str(), "CompleteAuthorize")
&& router_data.auth_type == storage_enums::AuthenticationType::ThreeDs
{
router_data = router_data.preprocessing_steps(state, connector).await?;
(router_data, false)
} else if (connector.connector_name == router_types::Connector::Cybersource
|| connector.connector_name == router_types::Connector::Bankofamerica)
&& is_operation_complete_authorize(&operation)
&& router_data.auth_type == storage_enums::AuthenticationType::ThreeDs
{
router_data = router_data.preprocessing_steps(state, connector).await?;
// Should continue the flow only if no redirection_data is returned else a response with redirection form shall be returned
let should_continue = matches!(
router_data.response,
Ok(router_types::PaymentsResponseData::TransactionResponse {
redirection_data: None,
..
})
) && router_data.status
!= common_enums::AttemptStatus::AuthenticationFailed;
(router_data, should_continue)
} else {
(router_data, should_continue_payment)
}
}
Some(api_models::payments::PaymentMethodData::GiftCard(_)) => {
if connector.connector_name == router_types::Connector::Adyen {
router_data = router_data.preprocessing_steps(state, connector).await?;
let is_error_in_response = router_data.response.is_err();
// If is_error_in_response is true, should_continue_payment should be false, we should throw the error
(router_data, !is_error_in_response)
} else {
(router_data, should_continue_payment)
}
}
Some(api_models::payments::PaymentMethodData::BankDebit(_)) => {
if connector.connector_name == router_types::Connector::Gocardless {
router_data = router_data.preprocessing_steps(state, connector).await?;
let is_error_in_response = router_data.response.is_err();
// If is_error_in_response is true, should_continue_payment should be false, we should throw the error
(router_data, !is_error_in_response)
} else {
(router_data, should_continue_payment)
}
}
_ => {
// 3DS validation for paypal cards after verification (authorize call)
if connector.connector_name == router_types::Connector::Paypal
&& payment_data.payment_attempt.payment_method
== Some(storage_enums::PaymentMethod::Card)
&& matches!(format!("{operation:?}").as_str(), "CompleteAuthorize")
{
router_data = router_data.preprocessing_steps(state, connector).await?;
let is_error_in_response = router_data.response.is_err();
// If is_error_in_response is true, should_continue_payment should be false, we should throw the error
(router_data, !is_error_in_response)
} else {
(router_data, should_continue_payment)
}
}
};
Ok(router_data_and_should_continue_payment)
}
pub fn is_preprocessing_required_for_wallets(connector_name: String) -> bool {
connector_name == *"trustpay" || connector_name == *"payme"
}
#[instrument(skip_all)]
pub async fn construct_profile_id_and_get_mca<'a, F>(
state: &'a AppState,
merchant_account: &domain::MerchantAccount,
payment_data: &mut PaymentData<F>,
connector_name: &str,
merchant_connector_id: Option<&String>,
key_store: &domain::MerchantKeyStore,
should_validate: bool,
) -> RouterResult<helpers::MerchantConnectorAccountType>
where
F: Clone,
{
let profile_id = utils::get_profile_id_from_business_details(
payment_data.payment_intent.business_country,
payment_data.payment_intent.business_label.as_ref(),
merchant_account,
payment_data.payment_intent.profile_id.as_ref(),
&*state.store,
should_validate,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("profile_id is not set in payment_intent")?;
let merchant_connector_account = helpers::get_merchant_connector_account(
state,
merchant_account.merchant_id.as_str(),
payment_data.creds_identifier.to_owned(),
key_store,
&profile_id,
connector_name,
merchant_connector_id,
)
.await?;
Ok(merchant_connector_account)
}
fn is_payment_method_tokenization_enabled_for_connector(
state: &AppState,
connector_name: &str,
payment_method: &storage::enums::PaymentMethod,
payment_method_type: &Option<storage::enums::PaymentMethodType>,
apple_pay_flow: &Option<enums::ApplePayFlow>,
) -> RouterResult<bool> {
let connector_tokenization_filter = state.conf.tokenization.0.get(connector_name);
Ok(connector_tokenization_filter
.map(|connector_filter| {
connector_filter
.payment_method
.clone()
.contains(payment_method)
&& is_payment_method_type_allowed_for_connector(
payment_method_type,
connector_filter.payment_method_type.clone(),
)
&& is_apple_pay_pre_decrypt_type_connector_tokenization(
payment_method_type,
apple_pay_flow,
connector_filter.apple_pay_pre_decrypt_flow.clone(),
)
})
.unwrap_or(false))
}
fn is_apple_pay_pre_decrypt_type_connector_tokenization(
payment_method_type: &Option<storage::enums::PaymentMethodType>,
apple_pay_flow: &Option<enums::ApplePayFlow>,
apple_pay_pre_decrypt_flow_filter: Option<ApplePayPreDecryptFlow>,
) -> bool {
match (payment_method_type, apple_pay_flow) {
(
Some(storage::enums::PaymentMethodType::ApplePay),
Some(enums::ApplePayFlow::Simplified),
) => !matches!(
apple_pay_pre_decrypt_flow_filter,
Some(ApplePayPreDecryptFlow::NetworkTokenization)
),
_ => true,
}
}
fn decide_apple_pay_flow(
payment_method_type: &Option<api_models::enums::PaymentMethodType>,
merchant_connector_account: Option<&helpers::MerchantConnectorAccountType>,
) -> Option<enums::ApplePayFlow> {
payment_method_type.and_then(|pmt| match pmt {
api_models::enums::PaymentMethodType::ApplePay => {
check_apple_pay_metadata(merchant_connector_account)
}
_ => None,
})
}
fn check_apple_pay_metadata(
merchant_connector_account: Option<&helpers::MerchantConnectorAccountType>,
) -> Option<enums::ApplePayFlow> {
merchant_connector_account.and_then(|mca| {
let metadata = mca.get_metadata();
metadata.and_then(|apple_pay_metadata| {
let parsed_metadata = apple_pay_metadata
.clone()
.parse_value::<api_models::payments::ApplepayCombinedSessionTokenData>(
"ApplepayCombinedSessionTokenData",
)
.map(|combined_metadata| {
api_models::payments::ApplepaySessionTokenMetadata::ApplePayCombined(
combined_metadata.apple_pay_combined,
)
})
.or_else(|_| {
apple_pay_metadata
.parse_value::<api_models::payments::ApplepaySessionTokenData>(
"ApplepaySessionTokenData",
)
.map(|old_metadata| {
api_models::payments::ApplepaySessionTokenMetadata::ApplePay(
old_metadata.apple_pay,
)
})
})
.map_err(
|error| logger::warn!(%error, "Failed to Parse Value to ApplepaySessionTokenData"),
);
parsed_metadata.ok().map(|metadata| match metadata {
api_models::payments::ApplepaySessionTokenMetadata::ApplePayCombined(
apple_pay_combined,
) => match apple_pay_combined {
api_models::payments::ApplePayCombinedMetadata::Simplified { .. } => {
enums::ApplePayFlow::Simplified
}
api_models::payments::ApplePayCombinedMetadata::Manual { .. } => {
enums::ApplePayFlow::Manual
}
},
api_models::payments::ApplepaySessionTokenMetadata::ApplePay(_) => {
enums::ApplePayFlow::Manual
}
})
})
})
}
fn is_payment_method_type_allowed_for_connector(
current_pm_type: &Option<storage::enums::PaymentMethodType>,
pm_type_filter: Option<PaymentMethodTypeTokenFilter>,
) -> bool {
match (*current_pm_type).zip(pm_type_filter) {
Some((pm_type, type_filter)) => match type_filter {
PaymentMethodTypeTokenFilter::AllAccepted => true,
PaymentMethodTypeTokenFilter::EnableOnly(enabled) => enabled.contains(&pm_type),
PaymentMethodTypeTokenFilter::DisableOnly(disabled) => !disabled.contains(&pm_type),
},
None => true, // Allow all types if payment_method_type is not present
}
}
async fn decide_payment_method_tokenize_action(
state: &AppState,
connector_name: &str,
payment_method: &storage::enums::PaymentMethod,
pm_parent_token: Option<&String>,
is_connector_tokenization_enabled: bool,
apple_pay_flow: Option<enums::ApplePayFlow>,
) -> RouterResult<TokenizationAction> {
let is_apple_pay_predecrypt_supported =
matches!(apple_pay_flow, Some(enums::ApplePayFlow::Simplified));
match pm_parent_token {
None => {
if is_connector_tokenization_enabled && is_apple_pay_predecrypt_supported {
Ok(TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt)
} else if is_connector_tokenization_enabled {
Ok(TokenizationAction::TokenizeInConnectorAndRouter)
} else if is_apple_pay_predecrypt_supported {
Ok(TokenizationAction::DecryptApplePayToken)
} else {
Ok(TokenizationAction::TokenizeInRouter)
}
}
Some(token) => {
let redis_conn = state
.store
.get_redis_conn()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to get redis connection")?;
let key = format!(
"pm_token_{}_{}_{}",
token.to_owned(),
payment_method,
connector_name
);
let connector_token_option = redis_conn
.get_key::<Option<String>>(&key)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to fetch the token from redis")?;
match connector_token_option {
Some(connector_token) => Ok(TokenizationAction::ConnectorToken(connector_token)),
None => {
if is_connector_tokenization_enabled && is_apple_pay_predecrypt_supported {
Ok(TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt)
} else if is_connector_tokenization_enabled {
Ok(TokenizationAction::TokenizeInConnectorAndRouter)
} else if is_apple_pay_predecrypt_supported {
Ok(TokenizationAction::DecryptApplePayToken)
} else {
Ok(TokenizationAction::TokenizeInRouter)
}
}
}
}
}
}
#[derive(Clone, Debug)]
pub enum TokenizationAction {
TokenizeInRouter,
TokenizeInConnector,
TokenizeInConnectorAndRouter,
ConnectorToken(String),
SkipConnectorTokenization,
DecryptApplePayToken,
TokenizeInConnectorAndApplepayPreDecrypt,
}
#[allow(clippy::too_many_arguments)]
pub async fn get_connector_tokenization_action_when_confirm_true<F, Req, Ctx>(
state: &AppState,
operation: &BoxedOperation<'_, F, Req, Ctx>,
payment_data: &mut PaymentData<F>,
validate_result: &operations::ValidateResult<'_>,
merchant_connector_account: &helpers::MerchantConnectorAccountType,
merchant_key_store: &domain::MerchantKeyStore,
customer: &Option<domain::Customer>,
) -> RouterResult<(PaymentData<F>, TokenizationAction)>
where
F: Send + Clone,
Ctx: PaymentMethodRetrieve,
{
let connector = payment_data.payment_attempt.connector.to_owned();
let is_mandate = payment_data
.mandate_id
.as_ref()
.and_then(|inner| inner.mandate_reference_id.as_ref())
.map(|mandate_reference| match mandate_reference {
api_models::payments::MandateReferenceId::ConnectorMandateId(_) => true,
api_models::payments::MandateReferenceId::NetworkMandateId(_) => false,
})
.unwrap_or(false);
let payment_data_and_tokenization_action = match connector {
Some(_) if is_mandate => (
payment_data.to_owned(),
TokenizationAction::SkipConnectorTokenization,
),
Some(connector) if is_operation_confirm(&operation) => {
let payment_method = &payment_data
.payment_attempt
.payment_method
.get_required_value("payment_method")?;
let payment_method_type = &payment_data.payment_attempt.payment_method_type;
let apple_pay_flow =
decide_apple_pay_flow(payment_method_type, Some(merchant_connector_account));
let is_connector_tokenization_enabled =
is_payment_method_tokenization_enabled_for_connector(
state,
&connector,
payment_method,
payment_method_type,
&apple_pay_flow,
)?;
add_apple_pay_flow_metrics(
&apple_pay_flow,
payment_data.payment_attempt.connector.clone(),
payment_data.payment_attempt.merchant_id.clone(),
);
let payment_method_action = decide_payment_method_tokenize_action(
state,
&connector,
payment_method,
payment_data.token.as_ref(),
is_connector_tokenization_enabled,
apple_pay_flow,
)
.await?;
let connector_tokenization_action = match payment_method_action {
TokenizationAction::TokenizeInRouter => {
let (_operation, payment_method_data, pm_id) = operation
.to_domain()?
.make_pm_data(
state,
payment_data,
validate_result.storage_scheme,
merchant_key_store,
customer,
)
.await?;
payment_data.payment_method_data = payment_method_data;
payment_data.payment_attempt.payment_method_id = pm_id;
TokenizationAction::SkipConnectorTokenization
}
TokenizationAction::TokenizeInConnector => TokenizationAction::TokenizeInConnector,
TokenizationAction::TokenizeInConnectorAndRouter => {
let (_operation, payment_method_data, pm_id) = operation
.to_domain()?
.make_pm_data(
state,
payment_data,
validate_result.storage_scheme,
merchant_key_store,
customer,
)
.await?;
payment_data.payment_method_data = payment_method_data;
payment_data.payment_attempt.payment_method_id = pm_id;
TokenizationAction::TokenizeInConnector
}
TokenizationAction::ConnectorToken(token) => {
payment_data.pm_token = Some(token);
TokenizationAction::SkipConnectorTokenization
}
TokenizationAction::SkipConnectorTokenization => {
TokenizationAction::SkipConnectorTokenization
}
TokenizationAction::DecryptApplePayToken => {
TokenizationAction::DecryptApplePayToken
}
TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt => {
TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt
}
};
(payment_data.to_owned(), connector_tokenization_action)
}
_ => (
payment_data.to_owned(),
TokenizationAction::SkipConnectorTokenization,
),
};
Ok(payment_data_and_tokenization_action)
}
pub async fn tokenize_in_router_when_confirm_false_or_external_authentication<F, Req, Ctx>(
state: &AppState,
operation: &BoxedOperation<'_, F, Req, Ctx>,
payment_data: &mut PaymentData<F>,
validate_result: &operations::ValidateResult<'_>,
merchant_key_store: &domain::MerchantKeyStore,
customer: &Option<domain::Customer>,
) -> RouterResult<PaymentData<F>>
where
F: Send + Clone,
Ctx: PaymentMethodRetrieve,
{
// On confirm is false and only router related
let is_external_authentication_requested = payment_data
.payment_intent
.request_external_three_ds_authentication;
let payment_data =
if !is_operation_confirm(operation) || is_external_authentication_requested == Some(true) {
let (_operation, payment_method_data, pm_id) = operation
.to_domain()?
.make_pm_data(
state,
payment_data,
validate_result.storage_scheme,
merchant_key_store,
customer,
)
.await?;
payment_data.payment_method_data = payment_method_data;
if let Some(payment_method_id) = pm_id {
payment_data.payment_attempt.payment_method_id = Some(payment_method_id);
}
payment_data
} else {
payment_data
};
Ok(payment_data.to_owned())
}
#[derive(Clone, PartialEq)]
pub enum CallConnectorAction {
Trigger,
Avoid,
StatusUpdate {
status: storage_enums::AttemptStatus,
error_code: Option<String>,
error_message: Option<String>,
},
HandleResponse(Vec<u8>),
}
pub mod payment_address {
use super::*;
#[derive(Clone, Default, Debug)]
pub struct PaymentAddress {
shipping: Option<api::Address>,
billing: Option<api::Address>,
unified_payment_method_billing: Option<api::Address>,
payment_method_billing: Option<api::Address>,
}
impl PaymentAddress {
pub fn new(
shipping: Option<api::Address>,
billing: Option<api::Address>,
payment_method_billing: Option<api::Address>,
) -> Self {
// billing -> .billing, this is the billing details passed in the root of payments request
// payment_method_billing -> .payment_method_data.billing
// Merge the billing details field from both `payment.billing` and `payment.payment_method_data.billing`
// The unified payment_method_billing will be used as billing address and passed to the connector module
// This unification is required in order to provide backwards compatibility
// so that if `payment.billing` is passed it should be sent to the connector module
// Unify the billing details with `payment_method_data.billing`
let unified_payment_method_billing = payment_method_billing
.as_ref()
.map(|payment_method_billing| {
payment_method_billing
.clone()
.unify_address(billing.as_ref())
})
.or(billing.clone());
Self {
shipping,
billing,
unified_payment_method_billing,
payment_method_billing,
}
}
pub fn get_shipping(&self) -> Option<&api::Address> {
self.shipping.as_ref()
}
pub fn get_payment_method_billing(&self) -> Option<&api::Address> {
self.unified_payment_method_billing.as_ref()
}
/// Unify the billing details from `payment_method_data.[payment_method_data].billing details`.
pub fn unify_with_payment_method_data_billing(
self,
payment_method_data_billing: Option<api::Address>,
) -> Self {
// Unify the billing details with `payment_method_data.billing_details`
let unified_payment_method_billing = payment_method_data_billing
.map(|payment_method_data_billing| {
payment_method_data_billing.unify_address(self.get_payment_method_billing())
})
.or(self.get_payment_method_billing().cloned());
Self {
shipping: self.shipping,
billing: self.billing,
unified_payment_method_billing,
payment_method_billing: self.payment_method_billing,
}
}
pub fn get_request_payment_method_billing(&self) -> Option<&api::Address> {
self.payment_method_billing.as_ref()
}
pub fn get_payment_billing(&self) -> Option<&api::Address> {
self.billing.as_ref()
}
}
}
#[derive(Clone)]
pub struct MandateConnectorDetails {
pub connector: String,
pub merchant_connector_id: Option<String>,
}
#[derive(Clone)]
pub struct PaymentData<F>
where
F: Clone,
{
pub flow: PhantomData<F>,
pub payment_intent: storage::PaymentIntent,
pub payment_attempt: storage::PaymentAttempt,
pub multiple_capture_data: Option<types::MultipleCaptureData>,
pub amount: api::Amount,
pub mandate_id: Option<api_models::payments::MandateIds>,
pub mandate_connector: Option<MandateConnectorDetails>,
pub currency: storage_enums::Currency,
pub setup_mandate: Option<MandateData>,
pub customer_acceptance: Option<CustomerAcceptance>,
pub address: PaymentAddress,
pub token: Option<String>,
pub token_data: Option<storage::PaymentTokenData>,
pub confirm: Option<bool>,
pub force_sync: Option<bool>,
pub payment_method_data: Option<api::PaymentMethodData>,
pub payment_method_info: Option<storage::PaymentMethod>,
pub refunds: Vec<storage::Refund>,
pub disputes: Vec<storage::Dispute>,
pub attempts: Option<Vec<storage::PaymentAttempt>>,
pub sessions_token: Vec<api::SessionToken>,
pub card_cvc: Option<Secret<String>>,
pub email: Option<pii::Email>,
pub creds_identifier: Option<String>,
pub pm_token: Option<String>,
pub connector_customer_id: Option<String>,
pub recurring_mandate_payment_data: Option<RecurringMandatePaymentData>,
pub ephemeral_key: Option<ephemeral_key::EphemeralKey>,
pub redirect_response: Option<api_models::payments::RedirectResponse>,
pub surcharge_details: Option<types::SurchargeDetails>,
pub frm_message: Option<FraudCheck>,
pub payment_link_data: Option<api_models::payments::PaymentLinkResponse>,
pub incremental_authorization_details: Option<IncrementalAuthorizationDetails>,
pub authorizations: Vec<diesel_models::authorization::Authorization>,
pub authentication: Option<storage::Authentication>,
pub frm_metadata: Option<serde_json::Value>,
pub recurring_details: Option<RecurringDetails>,
}
#[derive(Clone, serde::Serialize, Debug)]
pub struct PaymentEvent {
payment_intent: storage::PaymentIntent,
payment_attempt: storage::PaymentAttempt,
}
impl<F: Clone> PaymentData<F> {
fn to_event(&self) -> PaymentEvent {
PaymentEvent {
payment_intent: self.payment_intent.clone(),
payment_attempt: self.payment_attempt.clone(),
}
}
}
impl EventInfo for PaymentEvent {
type Data = Self;
fn data(&self) -> error_stack::Result<Self::Data, events::EventsError> {
Ok(self.clone())
}
fn key(&self) -> String {
"payment".to_string()
}
}
#[derive(Debug, Default, Clone)]
pub struct IncrementalAuthorizationDetails {
pub additional_amount: i64,
pub total_amount: i64,
pub reason: Option<String>,
pub authorization_id: Option<String>,
}
#[derive(Debug, Default, Clone)]
pub struct RecurringMandatePaymentData {
pub payment_method_type: Option<storage_enums::PaymentMethodType>, //required for making recurring payment using saved payment method through stripe
pub original_payment_authorized_amount: Option<i64>,
pub original_payment_authorized_currency: Option<storage_enums::Currency>,
}
#[derive(Debug, Default, Clone)]
pub struct CustomerDetails {
pub customer_id: Option<String>,
pub name: Option<Secret<String, masking::WithType>>,
pub email: Option<pii::Email>,
pub phone: Option<Secret<String, masking::WithType>>,
pub phone_country_code: Option<String>,
}
pub fn if_not_create_change_operation<'a, Op, F, Ctx>(
status: storage_enums::IntentStatus,
confirm: Option<bool>,
current: &'a Op,
) -> BoxedOperation<'_, F, api::PaymentsRequest, Ctx>
where
F: Send + Clone,
Op: Operation<F, api::PaymentsRequest, Ctx> + Send + Sync,
&'a Op: Operation<F, api::PaymentsRequest, Ctx>,
Ctx: PaymentMethodRetrieve,
{
if confirm.unwrap_or(false) {
Box::new(PaymentConfirm)
} else {
match status {
storage_enums::IntentStatus::RequiresConfirmation
| storage_enums::IntentStatus::RequiresCustomerAction
| storage_enums::IntentStatus::RequiresPaymentMethod => Box::new(current),
_ => Box::new(&PaymentStatus),
}
}
}
pub fn is_confirm<'a, F: Clone + Send, R, Op, Ctx>(
operation: &'a Op,
confirm: Option<bool>,
) -> BoxedOperation<'_, F, R, Ctx>
where
PaymentConfirm: Operation<F, R, Ctx>,
&'a PaymentConfirm: Operation<F, R, Ctx>,
Op: Operation<F, R, Ctx> + Send + Sync,
&'a Op: Operation<F, R, Ctx>,
Ctx: PaymentMethodRetrieve,
{
if confirm.unwrap_or(false) {
Box::new(&PaymentConfirm)
} else {
Box::new(operation)
}
}
pub fn should_call_connector<Op: Debug, F: Clone>(
operation: &Op,
payment_data: &PaymentData<F>,
) -> bool {
match format!("{operation:?}").as_str() {
"PaymentConfirm" => true,
"PaymentStart" => {
!matches!(
payment_data.payment_intent.status,
storage_enums::IntentStatus::Failed | storage_enums::IntentStatus::Succeeded
) && payment_data.payment_attempt.authentication_data.is_none()
}
"PaymentStatus" => {
matches!(
payment_data.payment_intent.status,
storage_enums::IntentStatus::Processing
| storage_enums::IntentStatus::RequiresCustomerAction
| storage_enums::IntentStatus::RequiresMerchantAction
| storage_enums::IntentStatus::RequiresCapture
| storage_enums::IntentStatus::PartiallyCapturedAndCapturable
) && payment_data.force_sync.unwrap_or(false)
}
"PaymentCancel" => matches!(
payment_data.payment_intent.status,
storage_enums::IntentStatus::RequiresCapture
| storage_enums::IntentStatus::PartiallyCapturedAndCapturable
),
"PaymentCapture" => {
matches!(
payment_data.payment_intent.status,
storage_enums::IntentStatus::RequiresCapture
| storage_enums::IntentStatus::PartiallyCapturedAndCapturable
) || (matches!(
payment_data.payment_intent.status,
storage_enums::IntentStatus::Processing
) && matches!(
payment_data.payment_attempt.capture_method,
Some(storage_enums::CaptureMethod::ManualMultiple)
))
}
"CompleteAuthorize" => true,
"PaymentApprove" => true,
"PaymentReject" => true,
"PaymentSession" => true,
"PaymentIncrementalAuthorization" => matches!(
payment_data.payment_intent.status,
storage_enums::IntentStatus::RequiresCapture
),
_ => false,
}
}
pub fn is_operation_confirm<Op: Debug>(operation: &Op) -> bool {
matches!(format!("{operation:?}").as_str(), "PaymentConfirm")
}
pub fn is_operation_complete_authorize<Op: Debug>(operation: &Op) -> bool {
matches!(format!("{operation:?}").as_str(), "CompleteAuthorize")
}
#[cfg(feature = "olap")]
pub async fn list_payments(
state: AppState,
merchant: domain::MerchantAccount,
constraints: api::PaymentListConstraints,
) -> RouterResponse<api::PaymentListResponse> {
use data_models::errors::StorageError;
helpers::validate_payment_list_request(&constraints)?;
let merchant_id = &merchant.merchant_id;
let db = state.store.as_ref();
let payment_intents =
helpers::filter_by_constraints(db, &constraints, merchant_id, merchant.storage_scheme)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
let collected_futures = payment_intents.into_iter().map(|pi| {
async {
match db
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
&pi.payment_id,
merchant_id,
&pi.active_attempt.get_id(),
// since OLAP doesn't have KV. Force to get the data from PSQL.
storage_enums::MerchantStorageScheme::PostgresOnly,
)
.await
{
Ok(pa) => Some(Ok((pi, pa))),
Err(error) => {
if matches!(error.current_context(), StorageError::ValueNotFound(_)) {
logger::warn!(
?error,
"payment_attempts missing for payment_id : {}",
pi.payment_id,
);
return None;
}
Some(Err(error))
}
}
}
});
//If any of the response are Err, we will get Result<Err(_)>
let pi_pa_tuple_vec: Result<Vec<(storage::PaymentIntent, storage::PaymentAttempt)>, _> =
join_all(collected_futures)
.await
.into_iter()
.flatten() //Will ignore `None`, will only flatten 1 level
.collect::<Result<Vec<(storage::PaymentIntent, storage::PaymentAttempt)>, _>>();
//Will collect responses in same order async, leading to sorted responses
//Converting Intent-Attempt array to Response if no error
let data: Vec<api::PaymentsResponse> = pi_pa_tuple_vec
.change_context(errors::ApiErrorResponse::InternalServerError)?
.into_iter()
.map(ForeignFrom::foreign_from)
.collect();
Ok(services::ApplicationResponse::Json(
api::PaymentListResponse {
size: data.len(),
data,
},
))
}
#[cfg(feature = "olap")]
pub async fn apply_filters_on_payments(
state: AppState,
merchant: domain::MerchantAccount,
constraints: api::PaymentListFilterConstraints,
) -> RouterResponse<api::PaymentListResponseV2> {
let limit = &constraints.limit;
helpers::validate_payment_list_request_for_joins(*limit)?;
let db = state.store.as_ref();
let list: Vec<(storage::PaymentIntent, storage::PaymentAttempt)> = db
.get_filtered_payment_intents_attempt(
&merchant.merchant_id,
&constraints.clone().into(),
merchant.storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
let data: Vec<api::PaymentsResponse> =
list.into_iter().map(ForeignFrom::foreign_from).collect();
let active_attempt_ids = db
.get_filtered_active_attempt_ids_for_total_count(
&merchant.merchant_id,
&constraints.clone().into(),
merchant.storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::InternalServerError)?;
let total_count = db
.get_total_count_of_filtered_payment_attempts(
&merchant.merchant_id,
&active_attempt_ids,
constraints.connector,
constraints.payment_method,
constraints.payment_method_type,
constraints.authentication_type,
merchant.storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;
Ok(services::ApplicationResponse::Json(
api::PaymentListResponseV2 {
count: data.len(),
total_count,
data,
},
))
}
#[cfg(feature = "olap")]
pub async fn get_filters_for_payments(
state: AppState,
merchant: domain::MerchantAccount,
time_range: api::TimeRange,
) -> RouterResponse<api::PaymentListFilters> {
let db = state.store.as_ref();
let pi = db
.filter_payment_intents_by_time_range_constraints(
&merchant.merchant_id,
&time_range,
merchant.storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
let filters = db
.get_filters_for_payments(
pi.as_slice(),
&merchant.merchant_id,
// since OLAP doesn't have KV. Force to get the data from PSQL.
storage_enums::MerchantStorageScheme::PostgresOnly,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
Ok(services::ApplicationResponse::Json(
api::PaymentListFilters {
connector: filters.connector,
currency: filters.currency,
status: filters.status,
payment_method: filters.payment_method,
payment_method_type: filters.payment_method_type,
authentication_type: filters.authentication_type,
},
))
}
#[cfg(feature = "olap")]
pub async fn get_payment_filters(
state: AppState,
merchant: domain::MerchantAccount,
) -> RouterResponse<api::PaymentListFiltersV2> {
let merchant_connector_accounts = if let services::ApplicationResponse::Json(data) =
super::admin::list_payment_connectors(state, merchant.merchant_id).await?
{
data
} else {
return Err(errors::ApiErrorResponse::InternalServerError.into());
};
let mut connector_map: HashMap<String, Vec<MerchantConnectorInfo>> = HashMap::new();
let mut payment_method_types_map: HashMap<
enums::PaymentMethod,
HashSet<enums::PaymentMethodType>,
> = HashMap::new();
// populate connector map
merchant_connector_accounts
.iter()
.filter_map(|merchant_connector_account| {
merchant_connector_account
.connector_label
.as_ref()
.map(|label| {
let info = MerchantConnectorInfo {
connector_label: label.clone(),
merchant_connector_id: merchant_connector_account
.merchant_connector_id
.clone(),
};
(merchant_connector_account.connector_name.clone(), info)
})
})
.for_each(|(connector_name, info)| {
connector_map
.entry(connector_name.clone())
.or_default()
.push(info);
});
// populate payment method type map
merchant_connector_accounts
.iter()
.flat_map(|merchant_connector_account| {
merchant_connector_account.payment_methods_enabled.as_ref()
})
.map(|payment_methods_enabled| {
payment_methods_enabled
.iter()
.filter_map(|payment_method_enabled| {
payment_method_enabled
.payment_method_types
.as_ref()
.map(|types_vec| (payment_method_enabled.payment_method, types_vec.clone()))
})
})
.for_each(|payment_methods_enabled| {
payment_methods_enabled.for_each(|(payment_method, payment_method_types_vec)| {
payment_method_types_map
.entry(payment_method)
.or_default()
.extend(
payment_method_types_vec
.iter()
.map(|p| p.payment_method_type),
);
});
});
Ok(services::ApplicationResponse::Json(
api::PaymentListFiltersV2 {
connector: connector_map,
currency: enums::Currency::iter().collect(),
status: enums::IntentStatus::iter().collect(),
payment_method: payment_method_types_map,
authentication_type: enums::AuthenticationType::iter().collect(),
},
))
}
pub async fn add_process_sync_task(
db: &dyn StorageInterface,
payment_attempt: &storage::PaymentAttempt,
schedule_time: time::PrimitiveDateTime,
) -> CustomResult<(), errors::StorageError> {
let tracking_data = api::PaymentsRetrieveRequest {
force_sync: true,
merchant_id: Some(payment_attempt.merchant_id.clone()),
resource_id: api::PaymentIdType::PaymentAttemptId(payment_attempt.attempt_id.clone()),
..Default::default()
};
let runner = storage::ProcessTrackerRunner::PaymentsSyncWorkflow;
let task = "PAYMENTS_SYNC";
let tag = ["SYNC", "PAYMENT"];
let process_tracker_id = pt_utils::get_process_tracker_id(
runner,
task,
&payment_attempt.attempt_id,
&payment_attempt.merchant_id,
);
let process_tracker_entry = storage::ProcessTrackerNew::new(
process_tracker_id,
task,
runner,
tag,
tracking_data,
schedule_time,
)
.map_err(errors::StorageError::from)?;
db.insert_process(process_tracker_entry).await?;
Ok(())
}
pub async fn reset_process_sync_task(
db: &dyn StorageInterface,
payment_attempt: &storage::PaymentAttempt,
schedule_time: time::PrimitiveDateTime,
) -> Result<(), errors::ProcessTrackerError> {
let runner = storage::ProcessTrackerRunner::PaymentsSyncWorkflow;
let task = "PAYMENTS_SYNC";
let process_tracker_id = pt_utils::get_process_tracker_id(
runner,
task,
&payment_attempt.attempt_id,
&payment_attempt.merchant_id,
);
let psync_process = db
.find_process_by_id(&process_tracker_id)
.await?
.ok_or(errors::ProcessTrackerError::ProcessFetchingFailed)?;
db.as_scheduler()
.reset_process(psync_process, schedule_time)
.await?;
Ok(())
}
pub fn update_straight_through_routing<F>(
payment_data: &mut PaymentData<F>,
request_straight_through: serde_json::Value,
) -> CustomResult<(), errors::ParsingError>
where
F: Send + Clone,
{
let _: api_models::routing::RoutingAlgorithm = request_straight_through
.clone()
.parse_value("RoutingAlgorithm")
.attach_printable("Invalid straight through routing rules format")?;
payment_data.payment_attempt.straight_through_algorithm = Some(request_straight_through);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn get_connector_choice<F, Req, Ctx>(
operation: &BoxedOperation<'_, F, Req, Ctx>,
state: &AppState,
req: &Req,
merchant_account: &domain::MerchantAccount,
business_profile: &storage::business_profile::BusinessProfile,
key_store: &domain::MerchantKeyStore,
payment_data: &mut PaymentData<F>,
eligible_connectors: Option<Vec<api_models::enums::RoutableConnectors>>,
mandate_type: Option<api::MandateTransactionType>,
) -> RouterResult<Option<ConnectorCallType>>
where
F: Send + Clone,
Ctx: PaymentMethodRetrieve,
{
let connector_choice = operation
.to_domain()?
.get_connector(
merchant_account,
&state.clone(),
req,
&payment_data.payment_intent,
key_store,
)
.await?;
let connector = if should_call_connector(operation, payment_data) {
Some(match connector_choice {
api::ConnectorChoice::SessionMultiple(connectors) => {
let routing_output = perform_session_token_routing(
state.clone(),
merchant_account,
key_store,
payment_data,
connectors,
)
.await?;
api::ConnectorCallType::SessionMultiple(routing_output)
}
api::ConnectorChoice::StraightThrough(straight_through) => {
connector_selection(
state,
merchant_account,
business_profile,
key_store,
payment_data,
Some(straight_through),
eligible_connectors,
mandate_type,
)
.await?
}
api::ConnectorChoice::Decide => {
connector_selection(
state,
merchant_account,
business_profile,
key_store,
payment_data,
None,
eligible_connectors,
mandate_type,
)
.await?
}
})
} else if let api::ConnectorChoice::StraightThrough(algorithm) = connector_choice {
update_straight_through_routing(payment_data, algorithm)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update straight through routing algorithm")?;
None
} else {
None
};
Ok(connector)
}
#[allow(clippy::too_many_arguments)]
pub async fn connector_selection<F>(
state: &AppState,
merchant_account: &domain::MerchantAccount,
business_profile: &storage::business_profile::BusinessProfile,
key_store: &domain::MerchantKeyStore,
payment_data: &mut PaymentData<F>,
request_straight_through: Option<serde_json::Value>,
eligible_connectors: Option<Vec<api_models::enums::RoutableConnectors>>,
mandate_type: Option<api::MandateTransactionType>,
) -> RouterResult<ConnectorCallType>
where
F: Send + Clone,
{
let request_straight_through: Option<api::routing::StraightThroughAlgorithm> =
request_straight_through
.map(|val| val.parse_value("RoutingAlgorithm"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid straight through routing rules format")?;
let mut routing_data = storage::RoutingData {
routed_through: payment_data.payment_attempt.connector.clone(),
#[cfg(feature = "connector_choice_mca_id")]
merchant_connector_id: payment_data.payment_attempt.merchant_connector_id.clone(),
#[cfg(not(feature = "connector_choice_mca_id"))]
business_sub_label: payment_data.payment_attempt.business_sub_label.clone(),
algorithm: request_straight_through.clone(),
routing_info: payment_data
.payment_attempt
.straight_through_algorithm
.clone()
.map(|val| val.parse_value("PaymentRoutingInfo"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid straight through algorithm format found in payment attempt")?
.unwrap_or_else(|| storage::PaymentRoutingInfo {
algorithm: None,
pre_routing_results: None,
}),
};
let decided_connector = decide_connector(
state.clone(),
merchant_account,
business_profile,
key_store,
payment_data,
request_straight_through,
&mut routing_data,
eligible_connectors,
mandate_type,
)
.await?;
let encoded_info = routing_data
.routing_info
.encode_to_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("error serializing payment routing info to serde value")?;
payment_data.payment_attempt.connector = routing_data.routed_through;
#[cfg(feature = "connector_choice_mca_id")]
{
payment_data.payment_attempt.merchant_connector_id = routing_data.merchant_connector_id;
}
#[cfg(not(feature = "connector_choice_mca_id"))]
{
payment_data.payment_attempt.business_sub_label = routing_data.business_sub_label;
}
payment_data.payment_attempt.straight_through_algorithm = Some(encoded_info);
Ok(decided_connector)
}
#[allow(clippy::too_many_arguments)]
pub async fn decide_connector<F>(
state: AppState,
merchant_account: &domain::MerchantAccount,
business_profile: &storage::business_profile::BusinessProfile,
key_store: &domain::MerchantKeyStore,
payment_data: &mut PaymentData<F>,
request_straight_through: Option<api::routing::StraightThroughAlgorithm>,
routing_data: &mut storage::RoutingData,
eligible_connectors: Option<Vec<api_models::enums::RoutableConnectors>>,
mandate_type: Option<api::MandateTransactionType>,
) -> RouterResult<ConnectorCallType>
where
F: Send + Clone,
{
// If the connector was already decided previously, use the same connector
// This is in case of flows like payments_sync, payments_cancel where the successive operations
// with the connector have to be made using the same connector account.
if let Some(ref connector_name) = payment_data.payment_attempt.connector {
// Connector was already decided previously, use the same connector
let connector_data = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
connector_name,
api::GetToken::Connector,
payment_data.payment_attempt.merchant_connector_id.clone(),
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid connector name received in 'routed_through'")?;
routing_data.routed_through = Some(connector_name.clone());
return Ok(api::ConnectorCallType::PreDetermined(connector_data));
}
if let Some(mandate_connector_details) = payment_data.mandate_connector.as_ref() {
let connector_data = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
&mandate_connector_details.connector,
api::GetToken::Connector,
#[cfg(feature = "connector_choice_mca_id")]
mandate_connector_details.merchant_connector_id.clone(),
#[cfg(not(feature = "connector_choice_mca_id"))]
None,
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid connector name received in 'routed_through'")?;
routing_data.routed_through = Some(mandate_connector_details.connector.clone());
#[cfg(feature = "connector_choice_mca_id")]
{
routing_data.merchant_connector_id =
mandate_connector_details.merchant_connector_id.clone();
}
return Ok(api::ConnectorCallType::PreDetermined(connector_data));
}
if let Some((pre_routing_results, storage_pm_type)) = routing_data
.routing_info
.pre_routing_results
.as_ref()
.zip(payment_data.payment_attempt.payment_method_type.as_ref())
{
if let (Some(choice), None) = (
pre_routing_results.get(storage_pm_type),
&payment_data.token_data,
) {
let connector_data = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
&choice.connector.to_string(),
api::GetToken::Connector,
#[cfg(feature = "connector_choice_mca_id")]
choice.merchant_connector_id.clone(),
#[cfg(not(feature = "connector_choice_mca_id"))]
None,
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid connector name received")?;
routing_data.routed_through = Some(choice.connector.to_string());
#[cfg(feature = "connector_choice_mca_id")]
{
routing_data.merchant_connector_id = choice.merchant_connector_id.clone();
}
#[cfg(not(feature = "connector_choice_mca_id"))]
{
routing_data.business_sub_label = choice.sub_label.clone();
}
return Ok(api::ConnectorCallType::PreDetermined(connector_data));
}
}
if let Some(routing_algorithm) = request_straight_through {
let (mut connectors, check_eligibility) = routing::perform_straight_through_routing(
&routing_algorithm,
payment_data.creds_identifier.clone(),
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed execution of straight through routing")?;
if check_eligibility {
#[cfg(feature = "business_profile_routing")]
let profile_id = payment_data.payment_intent.profile_id.clone();
#[cfg(not(feature = "business_profile_routing"))]
let _profile_id: Option<String> = None;
connectors = routing::perform_eligibility_analysis_with_fallback(
&state.clone(),
key_store,
merchant_account.modified_at.assume_utc().unix_timestamp(),
connectors,
&TransactionData::Payment(payment_data),
eligible_connectors,
#[cfg(feature = "business_profile_routing")]
profile_id,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed eligibility analysis and fallback")?;
}
let connector_data = connectors
.into_iter()
.map(|conn| {
api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
&conn.connector.to_string(),
api::GetToken::Connector,
#[cfg(feature = "connector_choice_mca_id")]
conn.merchant_connector_id.clone(),
#[cfg(not(feature = "connector_choice_mca_id"))]
None,
)
})
.collect::<CustomResult<Vec<_>, _>>()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid connector name received")?;
return decide_multiplex_connector_for_normal_or_recurring_payment(
&state,
payment_data,
routing_data,
connector_data,
mandate_type,
)
.await;
}
if let Some(ref routing_algorithm) = routing_data.routing_info.algorithm {
let (mut connectors, check_eligibility) = routing::perform_straight_through_routing(
routing_algorithm,
payment_data.creds_identifier.clone(),
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed execution of straight through routing")?;
if check_eligibility {
#[cfg(feature = "business_profile_routing")]
let profile_id = payment_data.payment_intent.profile_id.clone();
#[cfg(not(feature = "business_profile_routing"))]
let _profile_id: Option<String> = None;
connectors = routing::perform_eligibility_analysis_with_fallback(
&state,
key_store,
merchant_account.modified_at.assume_utc().unix_timestamp(),
connectors,
&TransactionData::Payment(payment_data),
eligible_connectors,
#[cfg(feature = "business_profile_routing")]
profile_id,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed eligibility analysis and fallback")?;
}
let connector_data = connectors
.into_iter()
.map(|conn| {
api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
&conn.connector.to_string(),
api::GetToken::Connector,
#[cfg(feature = "connector_choice_mca_id")]
conn.merchant_connector_id,
#[cfg(not(feature = "connector_choice_mca_id"))]
None,
)
})
.collect::<CustomResult<Vec<_>, _>>()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid connector name received")?;
return decide_multiplex_connector_for_normal_or_recurring_payment(
&state,
payment_data,
routing_data,
connector_data,
mandate_type,
)
.await;
}
route_connector_v1(
&state,
merchant_account,
business_profile,
key_store,
TransactionData::Payment(payment_data),
routing_data,
eligible_connectors,
mandate_type,
)
.await
}
pub async fn decide_multiplex_connector_for_normal_or_recurring_payment<F: Clone>(
state: &AppState,
payment_data: &mut PaymentData<F>,
routing_data: &mut storage::RoutingData,
connectors: Vec<api::ConnectorData>,
mandate_type: Option<api::MandateTransactionType>,
) -> RouterResult<ConnectorCallType> {
match (
payment_data.payment_intent.setup_future_usage,
payment_data.token_data.as_ref(),
payment_data.recurring_details.as_ref(),
payment_data.payment_intent.off_session,
mandate_type,
) {
(
Some(storage_enums::FutureUsage::OffSession),
Some(_),
None,
None,
Some(api::MandateTransactionType::RecurringMandateTransaction),
)
| (
None,
None,
Some(RecurringDetails::PaymentMethodId(_)),
Some(true),
Some(api::MandateTransactionType::RecurringMandateTransaction),
)
| (None, Some(_), None, Some(true), _) => {
logger::debug!("performing routing for token-based MIT flow");
let payment_method_info = payment_data
.payment_method_info
.as_ref()
.get_required_value("payment_method_info")?;
let connector_mandate_details = &payment_method_info
.connector_mandate_details
.clone()
.map(|details| {
details.parse_value::<storage::PaymentsMandateReference>(
"connector_mandate_details",
)
})
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to deserialize connector mandate details")?;
let profile_id = payment_data
.payment_intent
.profile_id
.as_ref()
.ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?;
let pg_agnostic = state
.store
.find_config_by_key_unwrap_or(
&format!("pg_agnostic_mandate_{}", profile_id),
Some("false".to_string()),
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("The pg_agnostic config was not found in the DB")?;
let mut connector_choice = None;
for connector_data in connectors {
let merchant_connector_id = connector_data
.merchant_connector_id
.as_ref()
.ok_or(errors::ApiErrorResponse::InternalServerError)?;
if is_network_transaction_id_flow(
state,
&pg_agnostic.config,
connector_data.connector_name,
payment_method_info,
) {
logger::info!("using network_transaction_id for MIT flow");
let network_transaction_id = payment_method_info
.network_transaction_id
.as_ref()
.ok_or(errors::ApiErrorResponse::InternalServerError)?;
let mandate_reference_id =
Some(payments_api::MandateReferenceId::NetworkMandateId(
network_transaction_id.to_string(),
));
connector_choice = Some((connector_data, mandate_reference_id.clone()));
break;
} else if connector_mandate_details
.clone()
.map(|connector_mandate_details| {
connector_mandate_details.contains_key(merchant_connector_id)
})
.unwrap_or(false)
{
if let Some(merchant_connector_id) =
connector_data.merchant_connector_id.as_ref()
{
if let Some(mandate_reference_record) = connector_mandate_details.clone()
.get_required_value("connector_mandate_details")
.change_context(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
.attach_printable("no eligible connector found for token-based MIT flow since there were no connector mandate details")?
.get(merchant_connector_id)
{
common_utils::fp_utils::when(
mandate_reference_record
.original_payment_authorized_currency
.map(|mandate_currency| mandate_currency != payment_data.currency)
.unwrap_or(false),
|| {
Err(report!(errors::ApiErrorResponse::MandateValidationFailed {
reason: "cross currency mandates not supported".into()
}))
},
)?;
let mandate_reference_id =
Some(payments_api::MandateReferenceId::ConnectorMandateId(
payments_api::ConnectorMandateReferenceId {
connector_mandate_id: Some(
mandate_reference_record.connector_mandate_id.clone(),
),
payment_method_id: Some(
payment_method_info.payment_method_id.clone(),
),
update_history: None,
},
));
payment_data.recurring_mandate_payment_data =
Some(RecurringMandatePaymentData {
payment_method_type: mandate_reference_record
.payment_method_type,
original_payment_authorized_amount: mandate_reference_record
.original_payment_authorized_amount,
original_payment_authorized_currency: mandate_reference_record
.original_payment_authorized_currency,
});
connector_choice = Some((connector_data, mandate_reference_id.clone()));
break;
}
}
} else {
continue;
}
}
let (chosen_connector_data, mandate_reference_id) = connector_choice
.get_required_value("connector_choice")
.change_context(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
.attach_printable("no eligible connector found for token-based MIT payment")?;
routing_data.routed_through = Some(chosen_connector_data.connector_name.to_string());
#[cfg(feature = "connector_choice_mca_id")]
{
routing_data.merchant_connector_id =
chosen_connector_data.merchant_connector_id.clone();
}
routing_data.routed_through = Some(chosen_connector_data.connector_name.to_string());
#[cfg(feature = "connector_choice_mca_id")]
{
routing_data.merchant_connector_id =
chosen_connector_data.merchant_connector_id.clone();
}
payment_data.mandate_id = Some(payments_api::MandateIds {
mandate_id: None,
mandate_reference_id,
});
Ok(api::ConnectorCallType::PreDetermined(chosen_connector_data))
}
_ => {
let first_choice = connectors
.first()
.ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
.attach_printable("no eligible connector found for payment")?
.clone();
routing_data.routed_through = Some(first_choice.connector_name.to_string());
#[cfg(feature = "connector_choice_mca_id")]
{
routing_data.merchant_connector_id = first_choice.merchant_connector_id;
}
Ok(api::ConnectorCallType::Retryable(connectors))
}
}
}
pub fn is_network_transaction_id_flow(
state: &AppState,
pg_agnostic: &String,
connector: enums::Connector,
payment_method_info: &storage::PaymentMethod,
) -> bool {
let ntid_supported_connectors = &state
.conf
.network_transaction_id_supported_connectors
.connector_list;
pg_agnostic == "true"
&& payment_method_info.payment_method == storage_enums::PaymentMethod::Card
&& ntid_supported_connectors.contains(&connector)
&& payment_method_info.network_transaction_id.is_some()
}
pub fn should_add_task_to_process_tracker<F: Clone>(payment_data: &PaymentData<F>) -> bool {
let connector = payment_data.payment_attempt.connector.as_deref();
!matches!(
(payment_data.payment_attempt.payment_method, connector),
(
Some(storage_enums::PaymentMethod::BankTransfer),
Some("stripe")
)
)
}
pub async fn perform_session_token_routing<F>(
state: AppState,
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
payment_data: &mut PaymentData<F>,
connectors: Vec<api::SessionConnectorData>,
) -> RouterResult<Vec<api::SessionConnectorData>>
where
F: Clone,
{
let routing_info: Option<storage::PaymentRoutingInfo> = payment_data
.payment_attempt
.straight_through_algorithm
.clone()
.map(|val| val.parse_value("PaymentRoutingInfo"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("invalid payment routing info format found in payment attempt")?;
if let Some(storage::PaymentRoutingInfo {
pre_routing_results: Some(pre_routing_results),
..
}) = routing_info
{
let mut payment_methods: rustc_hash::FxHashMap<
(String, enums::PaymentMethodType),
api::SessionConnectorData,
> = rustc_hash::FxHashMap::from_iter(connectors.iter().map(|c| {
(
(
c.connector.connector_name.to_string(),
c.payment_method_type,
),
c.clone(),
)
}));
let mut final_list: Vec<api::SessionConnectorData> = Vec::new();
for (routed_pm_type, choice) in pre_routing_results.into_iter() {
if let Some(session_connector_data) =
payment_methods.remove(&(choice.to_string(), routed_pm_type))
{
final_list.push(session_connector_data);
}
}
if !final_list.is_empty() {
return Ok(final_list);
}
}
let routing_enabled_pms = std::collections::HashSet::from([
enums::PaymentMethodType::GooglePay,
enums::PaymentMethodType::ApplePay,
enums::PaymentMethodType::Klarna,
enums::PaymentMethodType::Paypal,
]);
let mut chosen = Vec::<api::SessionConnectorData>::new();
for connector_data in &connectors {
if routing_enabled_pms.contains(&connector_data.payment_method_type) {
chosen.push(connector_data.clone());
}
}
let sfr = SessionFlowRoutingInput {
state: &state,
country: payment_data
.address
.get_payment_method_billing()
.and_then(|address| address.address.as_ref())
.and_then(|details| details.country),
key_store,
merchant_account,
payment_attempt: &payment_data.payment_attempt,
payment_intent: &payment_data.payment_intent,
chosen,
};
let result = self_routing::perform_session_flow_routing(sfr, &enums::TransactionType::Payment)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("error performing session flow routing")?;
let mut final_list: Vec<api::SessionConnectorData> = Vec::new();
#[cfg(not(feature = "connector_choice_mca_id"))]
for mut connector_data in connectors {
if !routing_enabled_pms.contains(&connector_data.payment_method_type) {
final_list.push(connector_data);
} else if let Some(choice) = result.get(&connector_data.payment_method_type) {
if connector_data.connector.connector_name == choice.connector.connector_name {
connector_data.business_sub_label = choice.sub_label.clone();
final_list.push(connector_data);
}
}
}
#[cfg(feature = "connector_choice_mca_id")]
for connector_data in connectors {
if !routing_enabled_pms.contains(&connector_data.payment_method_type) {
final_list.push(connector_data);
} else if let Some(choice) = result.get(&connector_data.payment_method_type) {
if connector_data.connector.connector_name == choice.connector.connector_name {
final_list.push(connector_data);
}
}
}
Ok(final_list)
}
#[allow(clippy::too_many_arguments)]
pub async fn route_connector_v1<F>(
state: &AppState,
merchant_account: &domain::MerchantAccount,
business_profile: &storage::business_profile::BusinessProfile,
key_store: &domain::MerchantKeyStore,
transaction_data: TransactionData<'_, F>,
routing_data: &mut storage::RoutingData,
eligible_connectors: Option<Vec<api_models::enums::RoutableConnectors>>,
mandate_type: Option<api::MandateTransactionType>,
) -> RouterResult<ConnectorCallType>
where
F: Send + Clone,
{
#[allow(unused_variables)]
let (profile_id, routing_algorithm) = match &transaction_data {
TransactionData::Payment(payment_data) => {
if cfg!(feature = "business_profile_routing") {
(
payment_data.payment_intent.profile_id.clone(),
business_profile.routing_algorithm.clone(),
)
} else {
(None, merchant_account.routing_algorithm.clone())
}
}
#[cfg(feature = "payouts")]
TransactionData::Payout(payout_data) => {
if cfg!(feature = "business_profile_routing") {
(
Some(payout_data.payout_attempt.profile_id.clone()),
business_profile.payout_routing_algorithm.clone(),
)
} else {
(None, merchant_account.payout_routing_algorithm.clone())
}
}
};
let algorithm_ref = routing_algorithm
.map(|ra| ra.parse_value::<api::routing::RoutingAlgorithmRef>("RoutingAlgorithmRef"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not decode merchant routing algorithm ref")?
.unwrap_or_default();
let connectors = routing::perform_static_routing_v1(
state,
&merchant_account.merchant_id,
algorithm_ref,
&transaction_data,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;
let connectors = routing::perform_eligibility_analysis_with_fallback(
&state.clone(),
key_store,
merchant_account.modified_at.assume_utc().unix_timestamp(),
connectors,
&transaction_data,
eligible_connectors,
#[cfg(feature = "business_profile_routing")]
profile_id,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("failed eligibility analysis and fallback")?;
#[cfg(feature = "payouts")]
let first_connector_choice = connectors
.first()
.ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
.attach_printable("Empty connector list returned")?
.clone();
let connector_data = connectors
.into_iter()
.map(|conn| {
api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
&conn.connector.to_string(),
api::GetToken::Connector,
#[cfg(feature = "connector_choice_mca_id")]
conn.merchant_connector_id,
#[cfg(not(feature = "connector_choice_mca_id"))]
None,
)
})
.collect::<CustomResult<Vec<_>, _>>()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Invalid connector name received")?;
match transaction_data {
TransactionData::Payment(payment_data) => {
decide_multiplex_connector_for_normal_or_recurring_payment(
state,
payment_data,
routing_data,
connector_data,
mandate_type,
)
.await
}
#[cfg(feature = "payouts")]
TransactionData::Payout(_) => {
routing_data.routed_through = Some(first_connector_choice.connector.to_string());
#[cfg(feature = "connector_choice_mca_id")]
{
routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id;
}
#[cfg(not(feature = "connector_choice_mca_id"))]
{
routing_data.business_sub_label = first_connector_choice.sub_label;
}
Ok(ConnectorCallType::Retryable(connector_data))
}
}
}
#[instrument(skip_all)]
pub async fn payment_external_authentication(
state: AppState,
merchant_account: domain::MerchantAccount,
key_store: domain::MerchantKeyStore,
req: api_models::payments::PaymentsExternalAuthenticationRequest,
) -> RouterResponse<api_models::payments::PaymentsExternalAuthenticationResponse> {
let db = &*state.store;
let merchant_id = &merchant_account.merchant_id;
let storage_scheme = merchant_account.storage_scheme;
let payment_id = req.payment_id;
let payment_intent = db
.find_payment_intent_by_payment_id_merchant_id(&payment_id, merchant_id, storage_scheme)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
let attempt_id = payment_intent.active_attempt.get_id().clone();
let payment_attempt = db
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
&payment_intent.payment_id,
merchant_id,
&attempt_id.clone(),
storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
if payment_attempt.external_three_ds_authentication_attempted != Some(true) {
Err(errors::ApiErrorResponse::PreconditionFailed {
message:
"You cannot authenticate this payment because payment_attempt.external_three_ds_authentication_attempted is false".to_owned(),
})?
}
helpers::validate_payment_status_against_allowed_statuses(
&payment_intent.status,
&[storage_enums::IntentStatus::RequiresCustomerAction],
"authenticate",
)?;
let optional_customer = match &payment_intent.customer_id {
Some(customer_id) => Some(
state
.store
.find_customer_by_customer_id_merchant_id(
customer_id,
&merchant_account.merchant_id,
&key_store,
storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable_lazy(|| {
format!("error while finding customer with customer_id {customer_id}")
})?,
),
None => None,
};
let profile_id = payment_intent
.profile_id
.as_ref()
.get_required_value("profile_id")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("'profile_id' not set in payment intent")?;
let currency = payment_attempt.currency.get_required_value("currency")?;
let amount = payment_attempt.get_total_amount().into();
let shipping_address = helpers::create_or_find_address_for_payment_by_request(
db,
None,
payment_intent.shipping_address_id.as_deref(),
merchant_id,
payment_intent.customer_id.as_ref(),
&key_store,
&payment_intent.payment_id,
storage_scheme,
)
.await?;
let billing_address = helpers::create_or_find_address_for_payment_by_request(
db,
None,
payment_intent.billing_address_id.as_deref(),
merchant_id,
payment_intent.customer_id.as_ref(),
&key_store,
&payment_intent.payment_id,
storage_scheme,
)
.await?;
let authentication_connector = payment_attempt
.authentication_connector
.clone()
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable("authentication_connector not found in payment_attempt")?;
let merchant_connector_account = helpers::get_merchant_connector_account(
&state,
merchant_id,
None,
&key_store,
profile_id,
authentication_connector.as_str(),
None,
)
.await?;
let authentication = db
.find_authentication_by_merchant_id_authentication_id(
merchant_id.to_string(),
payment_attempt
.authentication_id
.clone()
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable("missing authentication_id in payment_attempt")?,
)
.await
.to_not_found_response(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error while fetching authentication record")?;
let payment_method_details = helpers::get_payment_method_details_from_payment_token(
&state,
&payment_attempt,
&payment_intent,
&key_store,
storage_scheme,
)
.await?
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable("missing payment_method_details")?;
let browser_info: Option<BrowserInformation> = payment_attempt
.browser_info
.clone()
.map(|browser_information| browser_information.parse_value("BrowserInformation"))
.transpose()
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "browser_info",
})?;
let payment_connector_name = payment_attempt
.connector
.as_ref()
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable("missing connector in payment_attempt")?;
let return_url = Some(helpers::create_authorize_url(
&state.conf.server.base_url,
&payment_attempt.clone(),
payment_connector_name,
));
let business_profile = state
.store
.find_business_profile_by_profile_id(profile_id)
.await
.change_context(errors::ApiErrorResponse::BusinessProfileNotFound {
id: profile_id.to_string(),
})?;
let authentication_response = Box::pin(authentication_core::perform_authentication(
&state,
authentication_connector,
payment_method_details.0,
payment_method_details.1,
billing_address
.as_ref()
.map(|address| address.into())
.ok_or(errors::ApiErrorResponse::MissingRequiredField {
field_name: "billing_address",
})?,
shipping_address.as_ref().map(|address| address.into()),
browser_info,
business_profile,
merchant_connector_account,
amount,
Some(currency),
authentication::MessageCategory::Payment,
req.device_channel,
authentication,
return_url,
req.sdk_information,
req.threeds_method_comp_ind,
optional_customer.and_then(|customer| customer.email.map(common_utils::pii::Email::from)),
))
.await?;
Ok(services::ApplicationResponse::Json(
api_models::payments::PaymentsExternalAuthenticationResponse {
transaction_status: authentication_response.trans_status,
acs_url: authentication_response
.acs_url
.as_ref()
.map(ToString::to_string),
challenge_request: authentication_response.challenge_request,
acs_reference_number: authentication_response.acs_reference_number,
acs_trans_id: authentication_response.acs_trans_id,
three_dsserver_trans_id: authentication_response.three_dsserver_trans_id,
acs_signed_content: authentication_response.acs_signed_content,
},
))
}