mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 04:04:43 +08:00
8567 lines
309 KiB
Rust
8567 lines
309 KiB
Rust
pub mod access_token;
|
|
pub mod conditional_configs;
|
|
pub mod connector_integration_v2_impls;
|
|
pub mod customers;
|
|
pub mod flows;
|
|
pub mod helpers;
|
|
pub mod operations;
|
|
|
|
#[cfg(feature = "retry")]
|
|
pub mod retry;
|
|
pub mod routing;
|
|
#[cfg(feature = "v2")]
|
|
pub mod session_operation;
|
|
pub mod tokenization;
|
|
pub mod transformers;
|
|
pub mod types;
|
|
#[cfg(feature = "olap")]
|
|
use std::collections::HashMap;
|
|
use std::{
|
|
collections::HashSet, fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoIter,
|
|
};
|
|
|
|
#[cfg(feature = "v2")]
|
|
pub mod payment_methods;
|
|
|
|
#[cfg(feature = "olap")]
|
|
use api_models::admin::MerchantConnectorInfo;
|
|
use api_models::{
|
|
self, enums,
|
|
mandates::RecurringDetails,
|
|
payments::{self as payments_api},
|
|
};
|
|
pub use common_enums::enums::CallConnectorAction;
|
|
use common_utils::{
|
|
ext_traits::{AsyncExt, StringExt},
|
|
id_type, pii,
|
|
types::{AmountConvertor, MinorUnit, Surcharge},
|
|
};
|
|
use diesel_models::{ephemeral_key, fraud_check::FraudCheck};
|
|
use error_stack::{report, ResultExt};
|
|
use events::EventInfo;
|
|
use futures::future::join_all;
|
|
use helpers::{decrypt_paze_token, ApplePayData};
|
|
use hyperswitch_domain_models::payments::{payment_intent::CustomerData, ClickToPayMetaData};
|
|
#[cfg(feature = "v2")]
|
|
use hyperswitch_domain_models::payments::{
|
|
PaymentCaptureData, PaymentConfirmData, PaymentIntentData, PaymentStatusData,
|
|
};
|
|
#[cfg(feature = "v2")]
|
|
use hyperswitch_domain_models::router_response_types::RedirectForm;
|
|
pub use hyperswitch_domain_models::{
|
|
mandates::{CustomerAcceptance, MandateData},
|
|
payment_address::PaymentAddress,
|
|
payments::HeaderPayload,
|
|
router_data::{PaymentMethodToken, RouterData},
|
|
router_request_types::CustomerDetails,
|
|
};
|
|
use masking::{ExposeInterface, PeekInterface, Secret};
|
|
#[cfg(feature = "v2")]
|
|
use operations::ValidateStatusForOperation;
|
|
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 = "v2")]
|
|
pub use session_operation::payments_session_core;
|
|
#[cfg(feature = "olap")]
|
|
use strum::IntoEnumIterator;
|
|
use time;
|
|
|
|
#[cfg(feature = "v1")]
|
|
pub use self::operations::{
|
|
PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate,
|
|
PaymentIncrementalAuthorization, PaymentPostSessionTokens, PaymentReject, PaymentSession,
|
|
PaymentSessionUpdate, PaymentStatus, PaymentUpdate,
|
|
};
|
|
use self::{
|
|
conditional_configs::perform_decision_management,
|
|
flows::{ConstructFlowSpecificData, Feature},
|
|
operations::{BoxedOperation, Operation, PaymentResponse},
|
|
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;
|
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
|
use crate::core::routing::helpers as routing_helpers;
|
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
|
use crate::types::api::convert_connector_data_to_routable_connectors;
|
|
use crate::{
|
|
configs::settings::{ApplePayPreDecryptFlow, PaymentMethodTypeTokenFilter},
|
|
connector::utils::missing_field_err,
|
|
core::{
|
|
errors::{self, CustomResult, RouterResponse, RouterResult},
|
|
payment_methods::{cards, network_tokenization},
|
|
payouts,
|
|
routing::{self as core_routing},
|
|
utils::{self as core_utils},
|
|
},
|
|
db::StorageInterface,
|
|
logger,
|
|
routes::{app::ReqState, metrics, payment_methods::ParentPaymentMethodToken, SessionState},
|
|
services::{self, api::Authenticate, ConnectorRedirectResponse},
|
|
types::{
|
|
self as router_types,
|
|
api::{self, ConnectorCallType, ConnectorCommon},
|
|
domain,
|
|
storage::{self, enums as storage_enums, payment_attempt::PaymentAttemptExt},
|
|
transformers::ForeignTryInto,
|
|
},
|
|
utils::{
|
|
self, add_apple_pay_flow_metrics, add_connector_http_status_code_metrics, Encode,
|
|
OptionExt, ValueExt,
|
|
},
|
|
workflows::payment_sync,
|
|
};
|
|
#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))]
|
|
use crate::{
|
|
core::authentication as authentication_core,
|
|
types::{api::authentication, BrowserInformation},
|
|
};
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[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, D>(
|
|
state: &SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
key_store: domain::MerchantKeyStore,
|
|
profile: domain::Profile,
|
|
operation: Op,
|
|
req: Req,
|
|
get_tracker_response: operations::GetTrackerResponse<D>,
|
|
call_connector_action: CallConnectorAction,
|
|
header_payload: HeaderPayload,
|
|
) -> RouterResult<(D, Req, Option<domain::Customer>, Option<u16>, Option<u128>)>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
Req: Send + Sync,
|
|
Op: Operation<F, Req, Data = D> + Send + Sync,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
|
|
// To create connector flow specific interface data
|
|
D: ConstructFlowSpecificData<F, FData, router_types::PaymentsResponseData>,
|
|
RouterData<F, FData, router_types::PaymentsResponseData>: Feature<F, FData>,
|
|
|
|
// To construct connector flow specific api
|
|
dyn api::Connector:
|
|
services::api::ConnectorIntegration<F, FData, router_types::PaymentsResponseData>,
|
|
|
|
RouterData<F, FData, router_types::PaymentsResponseData>:
|
|
hyperswitch_domain_models::router_data::TrackerPostUpdateObjects<F, FData, D>,
|
|
|
|
// To perform router related operation for PaymentResponse
|
|
PaymentResponse: Operation<F, FData, Data = D>,
|
|
FData: Send + Sync + Clone,
|
|
{
|
|
let operation: BoxedOperation<'_, F, Req, D> = Box::new(operation);
|
|
|
|
// Get the trackers related to track the state of the payment
|
|
let operations::GetTrackerResponse { mut payment_data } = get_tracker_response;
|
|
|
|
let (_operation, customer) = operation
|
|
.to_domain()?
|
|
.get_customer_details(
|
|
state,
|
|
&mut payment_data,
|
|
&key_store,
|
|
merchant_account.storage_scheme,
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)
|
|
.attach_printable("Failed while fetching/creating customer")?;
|
|
|
|
operation
|
|
.to_domain()?
|
|
.run_decision_manager(state, &mut payment_data, &profile)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to run decision manager")?;
|
|
|
|
let connector = operation
|
|
.to_domain()?
|
|
.perform_routing(
|
|
&merchant_account,
|
|
&profile,
|
|
state,
|
|
&mut payment_data,
|
|
&key_store,
|
|
)
|
|
.await?;
|
|
|
|
let payment_data = match connector {
|
|
ConnectorCallType::PreDetermined(connector_data) => {
|
|
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.clone(),
|
|
None,
|
|
header_payload.clone(),
|
|
#[cfg(feature = "frm")]
|
|
None,
|
|
#[cfg(not(feature = "frm"))]
|
|
None,
|
|
&profile,
|
|
false,
|
|
)
|
|
.await?;
|
|
|
|
let payments_response_operation = Box::new(PaymentResponse);
|
|
|
|
payments_response_operation
|
|
.to_post_update_tracker()?
|
|
.update_tracker(
|
|
state,
|
|
payment_data,
|
|
router_data,
|
|
&key_store,
|
|
merchant_account.storage_scheme,
|
|
)
|
|
.await?
|
|
}
|
|
ConnectorCallType::Retryable(vec) => todo!(),
|
|
ConnectorCallType::SessionMultiple(vec) => todo!(),
|
|
ConnectorCallType::Skip => payment_data,
|
|
};
|
|
|
|
Ok((payment_data, req, customer, None, None))
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[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, D>(
|
|
state: &SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
profile_id_from_auth_layer: Option<id_type::ProfileId>,
|
|
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,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResult<(D, Req, Option<domain::Customer>, Option<u16>, Option<u128>)>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
Req: Authenticate + Clone,
|
|
Op: Operation<F, Req, Data = D> + Send + Sync,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
|
|
// To create connector flow specific interface data
|
|
D: ConstructFlowSpecificData<F, FData, router_types::PaymentsResponseData>,
|
|
RouterData<F, FData, router_types::PaymentsResponseData>: Feature<F, FData>,
|
|
|
|
// To construct connector flow specific api
|
|
dyn api::Connector:
|
|
services::api::ConnectorIntegration<F, FData, router_types::PaymentsResponseData>,
|
|
|
|
// To perform router related operation for PaymentResponse
|
|
PaymentResponse: Operation<F, FData, Data = D>,
|
|
FData: Send + Sync + Clone,
|
|
{
|
|
let operation: BoxedOperation<'_, F, Req, D> = Box::new(operation);
|
|
|
|
tracing::Span::current().record("merchant_id", merchant_account.get_id().get_string_repr());
|
|
let (operation, validate_result) = operation
|
|
.to_validate_request()?
|
|
.validate_request(&req, &merchant_account)?;
|
|
|
|
tracing::Span::current().record("payment_id", format!("{}", validate_result.payment_id));
|
|
// get profile from headers
|
|
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,
|
|
platform_merchant_account.as_ref(),
|
|
)
|
|
.await?;
|
|
core_utils::validate_profile_id_from_auth_layer(
|
|
profile_id_from_auth_layer,
|
|
&payment_data.get_payment_intent().clone(),
|
|
)?;
|
|
|
|
let (operation, customer) = operation
|
|
.to_domain()?
|
|
// get_customer_details
|
|
.get_or_create_customer_details(
|
|
state,
|
|
&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")?;
|
|
|
|
let authentication_type =
|
|
call_decision_manager(state, &merchant_account, &business_profile, &payment_data).await?;
|
|
|
|
payment_data.set_authentication_type_in_attempt(authentication_type);
|
|
|
|
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);
|
|
|
|
let locale = header_payload.locale.clone();
|
|
|
|
payment_data = tokenize_in_router_when_confirm_false_or_external_authentication(
|
|
state,
|
|
&operation,
|
|
&mut payment_data,
|
|
&validate_result,
|
|
&key_store,
|
|
&customer,
|
|
&business_profile,
|
|
)
|
|
.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;
|
|
#[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(
|
|
&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_continue_transaction: {:?}\nshould_continue_capture: {:?}",
|
|
frm_configs,
|
|
should_continue_transaction,
|
|
should_continue_capture,
|
|
);
|
|
|
|
if helpers::is_merchant_eligible_authentication_service(merchant_account.get_id(), state)
|
|
.await?
|
|
{
|
|
operation
|
|
.to_domain()?
|
|
.call_unified_authentication_service_if_eligible(
|
|
state,
|
|
&mut payment_data,
|
|
&mut should_continue_transaction,
|
|
&connector_details,
|
|
&business_profile,
|
|
&key_store,
|
|
mandate_type,
|
|
)
|
|
.await?;
|
|
} else {
|
|
logger::info!(
|
|
"skipping authentication service call since the merchant is not eligible."
|
|
);
|
|
|
|
operation
|
|
.to_domain()?
|
|
.call_external_three_ds_authentication_if_eligible(
|
|
state,
|
|
&mut payment_data,
|
|
&mut should_continue_transaction,
|
|
&connector_details,
|
|
&business_profile,
|
|
&key_store,
|
|
mandate_type,
|
|
)
|
|
.await?;
|
|
};
|
|
|
|
operation
|
|
.to_domain()?
|
|
.payments_dynamic_tax_calculation(
|
|
state,
|
|
&mut payment_data,
|
|
&connector_details,
|
|
&business_profile,
|
|
&key_store,
|
|
&merchant_account,
|
|
)
|
|
.await?;
|
|
|
|
if should_continue_transaction {
|
|
#[cfg(feature = "frm")]
|
|
match (
|
|
should_continue_capture,
|
|
payment_data.get_payment_attempt().capture_method,
|
|
) {
|
|
(
|
|
false,
|
|
Some(storage_enums::CaptureMethod::Automatic)
|
|
| Some(storage_enums::CaptureMethod::SequentialAutomatic),
|
|
)
|
|
| (false, Some(storage_enums::CaptureMethod::Scheduled)) => {
|
|
if let Some(info) = &mut frm_info {
|
|
if let Some(frm_data) = &mut info.frm_data {
|
|
frm_data.fraud_check.payment_capture_method =
|
|
payment_data.get_payment_attempt().capture_method;
|
|
}
|
|
}
|
|
payment_data
|
|
.set_capture_method_in_attempt(storage_enums::CaptureMethod::Manual);
|
|
logger::debug!("payment_id : {:?} capture method has been changed to manual, since it has configured Post FRM flow",payment_data.get_payment_attempt().payment_id);
|
|
}
|
|
_ => (),
|
|
};
|
|
payment_data = match connector_details {
|
|
ConnectorCallType::PreDetermined(connector) => {
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
let routable_connectors =
|
|
convert_connector_data_to_routable_connectors(&[connector.clone()])
|
|
.map_err(|e| logger::error!(routable_connector_error=?e))
|
|
.unwrap_or_default();
|
|
let schedule_time = if should_add_task_to_process_tracker {
|
|
payment_sync::get_sync_process_schedule_time(
|
|
&*state.store,
|
|
connector.connector.id(),
|
|
merchant_account.get_id(),
|
|
0,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed while getting process schedule time")?
|
|
} else {
|
|
None
|
|
};
|
|
let (router_data, mca) = call_connector_service(
|
|
state,
|
|
req_state.clone(),
|
|
&merchant_account,
|
|
&key_store,
|
|
connector.clone(),
|
|
&operation,
|
|
&mut payment_data,
|
|
&customer,
|
|
call_connector_action.clone(),
|
|
&validate_result,
|
|
schedule_time,
|
|
header_payload.clone(),
|
|
#[cfg(feature = "frm")]
|
|
frm_info.as_ref().and_then(|fi| fi.suggested_action),
|
|
#[cfg(not(feature = "frm"))]
|
|
None,
|
|
&business_profile,
|
|
false,
|
|
)
|
|
.await?;
|
|
|
|
let op_ref = &operation;
|
|
let should_trigger_post_processing_flows = is_operation_confirm(&operation);
|
|
|
|
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()?
|
|
.save_pm_and_mandate(
|
|
state,
|
|
&router_data,
|
|
&merchant_account,
|
|
&key_store,
|
|
&mut payment_data,
|
|
&business_profile,
|
|
)
|
|
.await?;
|
|
|
|
let mut payment_data = operation
|
|
.to_post_update_tracker()?
|
|
.update_tracker(
|
|
state,
|
|
payment_data,
|
|
router_data,
|
|
&key_store,
|
|
merchant_account.storage_scheme,
|
|
&locale,
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
routable_connectors,
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
&business_profile,
|
|
)
|
|
.await?;
|
|
|
|
if should_trigger_post_processing_flows {
|
|
complete_postprocessing_steps_if_required(
|
|
state,
|
|
&merchant_account,
|
|
&key_store,
|
|
&customer,
|
|
&mca,
|
|
&connector,
|
|
&mut payment_data,
|
|
op_ref,
|
|
Some(header_payload.clone()),
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
payment_data
|
|
}
|
|
|
|
ConnectorCallType::Retryable(connectors) => {
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
let routable_connectors =
|
|
convert_connector_data_to_routable_connectors(&connectors)
|
|
.map_err(|e| logger::error!(routable_connector_error=?e))
|
|
.unwrap_or_default();
|
|
|
|
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.get_id(),
|
|
0,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed while getting process schedule time")?
|
|
} else {
|
|
None
|
|
};
|
|
let (router_data, mca) = call_connector_service(
|
|
state,
|
|
req_state.clone(),
|
|
&merchant_account,
|
|
&key_store,
|
|
connector_data.clone(),
|
|
&operation,
|
|
&mut payment_data,
|
|
&customer,
|
|
call_connector_action.clone(),
|
|
&validate_result,
|
|
schedule_time,
|
|
header_payload.clone(),
|
|
#[cfg(feature = "frm")]
|
|
frm_info.as_ref().and_then(|fi| fi.suggested_action),
|
|
#[cfg(not(feature = "frm"))]
|
|
None,
|
|
&business_profile,
|
|
false,
|
|
)
|
|
.await?;
|
|
|
|
#[cfg(all(feature = "retry", feature = "v1"))]
|
|
let mut router_data = router_data;
|
|
#[cfg(all(feature = "retry", feature = "v1"))]
|
|
{
|
|
use crate::core::payments::retry::{self, GsmValidation};
|
|
let config_bool = retry::config_should_call_gsm(
|
|
&*state.store,
|
|
merchant_account.get_id(),
|
|
&business_profile,
|
|
)
|
|
.await;
|
|
|
|
if config_bool && router_data.should_call_gsm() {
|
|
router_data = retry::do_gsm_actions(
|
|
state,
|
|
req_state.clone(),
|
|
&mut payment_data,
|
|
connectors,
|
|
connector_data.clone(),
|
|
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,
|
|
&business_profile,
|
|
)
|
|
.await?;
|
|
};
|
|
}
|
|
|
|
let op_ref = &operation;
|
|
let should_trigger_post_processing_flows = is_operation_confirm(&operation);
|
|
|
|
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()?
|
|
.save_pm_and_mandate(
|
|
state,
|
|
&router_data,
|
|
&merchant_account,
|
|
&key_store,
|
|
&mut payment_data,
|
|
&business_profile,
|
|
)
|
|
.await?;
|
|
|
|
let mut payment_data = operation
|
|
.to_post_update_tracker()?
|
|
.update_tracker(
|
|
state,
|
|
payment_data,
|
|
router_data,
|
|
&key_store,
|
|
merchant_account.storage_scheme,
|
|
&locale,
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
routable_connectors,
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
&business_profile,
|
|
)
|
|
.await?;
|
|
|
|
if should_trigger_post_processing_flows {
|
|
complete_postprocessing_steps_if_required(
|
|
state,
|
|
&merchant_account,
|
|
&key_store,
|
|
&customer,
|
|
&mca,
|
|
&connector_data,
|
|
&mut payment_data,
|
|
op_ref,
|
|
Some(header_payload.clone()),
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
payment_data
|
|
}
|
|
|
|
ConnectorCallType::SessionMultiple(connectors) => {
|
|
let session_surcharge_details =
|
|
call_surcharge_decision_management_for_session_flow(
|
|
state,
|
|
&merchant_account,
|
|
&business_profile,
|
|
payment_data.get_payment_attempt(),
|
|
payment_data.get_payment_intent(),
|
|
payment_data.get_billing_address(),
|
|
&connectors,
|
|
)
|
|
.await?;
|
|
Box::pin(call_multiple_connectors_service(
|
|
state,
|
|
&merchant_account,
|
|
&key_store,
|
|
connectors,
|
|
&operation,
|
|
payment_data,
|
|
&customer,
|
|
session_surcharge_details,
|
|
&business_profile,
|
|
header_payload.clone(),
|
|
))
|
|
.await?
|
|
}
|
|
};
|
|
|
|
#[cfg(feature = "frm")]
|
|
if let Some(fraud_info) = &mut frm_info {
|
|
#[cfg(feature = "v1")]
|
|
Box::pin(frm_core::post_payment_frm_core(
|
|
state,
|
|
req_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(),
|
|
&mut should_continue_capture,
|
|
platform_merchant_account.as_ref(),
|
|
))
|
|
.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.clone(),
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
let payment_intent_status = payment_data.get_payment_intent().status;
|
|
|
|
payment_data
|
|
.get_payment_attempt()
|
|
.payment_token
|
|
.as_ref()
|
|
.zip(payment_data.get_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_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.clone(),
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
let cloned_payment_data = payment_data.clone();
|
|
let cloned_customer = customer.clone();
|
|
|
|
#[cfg(feature = "v1")]
|
|
operation
|
|
.to_domain()?
|
|
.store_extended_card_info_temporarily(
|
|
state,
|
|
payment_data.get_payment_intent().get_id(),
|
|
&business_profile,
|
|
payment_data.get_payment_method_data(),
|
|
)
|
|
.await?;
|
|
|
|
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,
|
|
))
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
// This function is intended for use when the feature being implemented is not aligned with the
|
|
// core payment operations.
|
|
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
|
|
#[instrument(skip_all, fields(payment_id, merchant_id))]
|
|
pub async fn proxy_for_payments_operation_core<F, Req, Op, FData, D>(
|
|
state: &SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
profile_id_from_auth_layer: Option<id_type::ProfileId>,
|
|
key_store: domain::MerchantKeyStore,
|
|
operation: Op,
|
|
req: Req,
|
|
call_connector_action: CallConnectorAction,
|
|
auth_flow: services::AuthFlow,
|
|
header_payload: HeaderPayload,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResult<(D, Req, Option<domain::Customer>, Option<u16>, Option<u128>)>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
Req: Authenticate + Clone,
|
|
Op: Operation<F, Req, Data = D> + Send + Sync,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
|
|
// To create connector flow specific interface data
|
|
D: ConstructFlowSpecificData<F, FData, router_types::PaymentsResponseData>,
|
|
RouterData<F, FData, router_types::PaymentsResponseData>: Feature<F, FData>,
|
|
|
|
// To construct connector flow specific api
|
|
dyn api::Connector:
|
|
services::api::ConnectorIntegration<F, FData, router_types::PaymentsResponseData>,
|
|
|
|
// To perform router related operation for PaymentResponse
|
|
PaymentResponse: Operation<F, FData, Data = D>,
|
|
FData: Send + Sync + Clone,
|
|
{
|
|
let operation: BoxedOperation<'_, F, Req, D> = Box::new(operation);
|
|
|
|
tracing::Span::current().record("merchant_id", merchant_account.get_id().get_string_repr());
|
|
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,
|
|
platform_merchant_account.as_ref(),
|
|
)
|
|
.await?;
|
|
|
|
core_utils::validate_profile_id_from_auth_layer(
|
|
profile_id_from_auth_layer,
|
|
&payment_data.get_payment_intent().clone(),
|
|
)?;
|
|
|
|
common_utils::fp_utils::when(!should_call_connector(&operation, &payment_data), || {
|
|
Err(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration).attach_printable(format!(
|
|
"Nti and card details based mit flow is not support for this {operation:?} payment operation"
|
|
))
|
|
})?;
|
|
|
|
let connector_choice = operation
|
|
.to_domain()?
|
|
.get_connector(
|
|
&merchant_account,
|
|
&state.clone(),
|
|
&req,
|
|
payment_data.get_payment_intent(),
|
|
&key_store,
|
|
)
|
|
.await?;
|
|
|
|
let connector = set_eligible_connector_for_nti_in_payment_data(
|
|
state,
|
|
&business_profile,
|
|
&key_store,
|
|
&mut payment_data,
|
|
connector_choice,
|
|
)
|
|
.await?;
|
|
|
|
let should_add_task_to_process_tracker = should_add_task_to_process_tracker(&payment_data);
|
|
|
|
let locale = header_payload.locale.clone();
|
|
|
|
let schedule_time = if should_add_task_to_process_tracker {
|
|
payment_sync::get_sync_process_schedule_time(
|
|
&*state.store,
|
|
connector.connector.id(),
|
|
merchant_account.get_id(),
|
|
0,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed while getting process schedule time")?
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let (router_data, mca) = proxy_for_call_connector_service(
|
|
state,
|
|
req_state.clone(),
|
|
&merchant_account,
|
|
&key_store,
|
|
connector.clone(),
|
|
&operation,
|
|
&mut payment_data,
|
|
&None,
|
|
call_connector_action.clone(),
|
|
&validate_result,
|
|
schedule_time,
|
|
header_payload.clone(),
|
|
&business_profile,
|
|
)
|
|
.await?;
|
|
|
|
let op_ref = &operation;
|
|
let should_trigger_post_processing_flows = is_operation_confirm(&operation);
|
|
|
|
let operation = Box::new(PaymentResponse);
|
|
|
|
let connector_http_status_code = router_data.connector_http_status_code;
|
|
let external_latency = router_data.external_latency;
|
|
|
|
add_connector_http_status_code_metrics(connector_http_status_code);
|
|
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
let routable_connectors = convert_connector_data_to_routable_connectors(&[connector.clone()])
|
|
.map_err(|e| logger::error!(routable_connector_error=?e))
|
|
.unwrap_or_default();
|
|
|
|
let mut payment_data = operation
|
|
.to_post_update_tracker()?
|
|
.update_tracker(
|
|
state,
|
|
payment_data,
|
|
router_data,
|
|
&key_store,
|
|
merchant_account.storage_scheme,
|
|
&locale,
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
routable_connectors,
|
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
|
&business_profile,
|
|
)
|
|
.await?;
|
|
|
|
if should_trigger_post_processing_flows {
|
|
complete_postprocessing_steps_if_required(
|
|
state,
|
|
&merchant_account,
|
|
&key_store,
|
|
&None,
|
|
&mca,
|
|
&connector,
|
|
&mut payment_data,
|
|
op_ref,
|
|
Some(header_payload.clone()),
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
let cloned_payment_data = payment_data.clone();
|
|
|
|
utils::trigger_payments_webhook(
|
|
merchant_account,
|
|
business_profile,
|
|
&key_store,
|
|
cloned_payment_data,
|
|
None,
|
|
state,
|
|
operation,
|
|
)
|
|
.await
|
|
.map_err(|error| logger::warn!(payments_outgoing_webhook_error=?error))
|
|
.ok();
|
|
|
|
Ok((
|
|
payment_data,
|
|
req,
|
|
None,
|
|
connector_http_status_code,
|
|
external_latency,
|
|
))
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
|
|
#[instrument(skip_all, fields(payment_id, merchant_id))]
|
|
pub async fn payments_intent_operation_core<F, Req, Op, D>(
|
|
state: &SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
profile: domain::Profile,
|
|
key_store: domain::MerchantKeyStore,
|
|
operation: Op,
|
|
req: Req,
|
|
payment_id: id_type::GlobalPaymentId,
|
|
header_payload: HeaderPayload,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResult<(D, Req, Option<domain::Customer>)>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
Req: Clone,
|
|
Op: Operation<F, Req, Data = D> + Send + Sync,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
let operation: BoxedOperation<'_, F, Req, D> = Box::new(operation);
|
|
|
|
tracing::Span::current().record("merchant_id", merchant_account.get_id().get_string_repr());
|
|
|
|
let _validate_result = operation
|
|
.to_validate_request()?
|
|
.validate_request(&req, &merchant_account)?;
|
|
|
|
tracing::Span::current().record("global_payment_id", payment_id.get_string_repr());
|
|
|
|
let operations::GetTrackerResponse { mut payment_data } = operation
|
|
.to_get_tracker()?
|
|
.get_trackers(
|
|
state,
|
|
&payment_id,
|
|
&req,
|
|
&merchant_account,
|
|
&profile,
|
|
&key_store,
|
|
&header_payload,
|
|
platform_merchant_account.as_ref(),
|
|
)
|
|
.await?;
|
|
|
|
let (_operation, customer) = operation
|
|
.to_domain()?
|
|
.get_customer_details(
|
|
state,
|
|
&mut payment_data,
|
|
&key_store,
|
|
merchant_account.storage_scheme,
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)
|
|
.attach_printable("Failed while fetching/creating customer")?;
|
|
|
|
let (_operation, payment_data) = operation
|
|
.to_update_tracker()?
|
|
.update_trackers(
|
|
state,
|
|
req_state,
|
|
payment_data,
|
|
customer.clone(),
|
|
merchant_account.storage_scheme,
|
|
None,
|
|
&key_store,
|
|
None,
|
|
header_payload,
|
|
)
|
|
.await?;
|
|
|
|
Ok((payment_data, req, customer))
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
#[cfg(feature = "v1")]
|
|
pub async fn call_decision_manager<F, D>(
|
|
state: &SessionState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
_business_profile: &domain::Profile,
|
|
payment_data: &D,
|
|
) -> RouterResult<Option<enums::AuthenticationType>>
|
|
where
|
|
F: Clone,
|
|
D: OperationSessionGetters<F>,
|
|
{
|
|
let setup_mandate = payment_data.get_setup_mandate();
|
|
let payment_method_data = payment_data.get_payment_method_data();
|
|
let payment_dsl_data = core_routing::PaymentsDslInput::new(
|
|
setup_mandate,
|
|
payment_data.get_payment_attempt(),
|
|
payment_data.get_payment_intent(),
|
|
payment_method_data,
|
|
payment_data.get_address(),
|
|
payment_data.get_recurring_details(),
|
|
payment_data.get_currency(),
|
|
);
|
|
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.get_id(),
|
|
&payment_dsl_data,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Could not decode the conditional config")?;
|
|
Ok(payment_dsl_data
|
|
.payment_attempt
|
|
.authentication_type
|
|
.or(output.override_3ds)
|
|
.or(Some(storage_enums::AuthenticationType::NoThreeDs)))
|
|
}
|
|
|
|
// TODO: Move to business profile surcharge column
|
|
#[instrument(skip_all)]
|
|
#[cfg(feature = "v2")]
|
|
pub fn call_decision_manager<F>(
|
|
state: &SessionState,
|
|
record: common_types::payments::DecisionManagerRecord,
|
|
payment_data: &PaymentConfirmData<F>,
|
|
) -> RouterResult<Option<enums::AuthenticationType>>
|
|
where
|
|
F: Clone,
|
|
{
|
|
let payment_method_data = payment_data.get_payment_method_data();
|
|
let payment_dsl_data = core_routing::PaymentsDslInput::new(
|
|
None,
|
|
payment_data.get_payment_attempt(),
|
|
payment_data.get_payment_intent(),
|
|
payment_method_data,
|
|
payment_data.get_address(),
|
|
None,
|
|
payment_data.get_currency(),
|
|
);
|
|
|
|
let output = perform_decision_management(record, &payment_dsl_data)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Could not decode the conditional config")?;
|
|
|
|
Ok(output.override_3ds)
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[instrument(skip_all)]
|
|
async fn populate_surcharge_details<F>(
|
|
state: &SessionState,
|
|
payment_data: &mut PaymentData<F>,
|
|
) -> RouterResult<()>
|
|
where
|
|
F: Send + Clone,
|
|
{
|
|
todo!()
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[instrument(skip_all)]
|
|
async fn populate_surcharge_details<F>(
|
|
state: &SessionState,
|
|
payment_data: &mut PaymentData<F>,
|
|
) -> RouterResult<()>
|
|
where
|
|
F: Send + Clone,
|
|
{
|
|
if payment_data
|
|
.payment_intent
|
|
.surcharge_applicable
|
|
.unwrap_or(false)
|
|
{
|
|
logger::debug!("payment_intent.surcharge_applicable = true");
|
|
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(helpers::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.clone();
|
|
|
|
//Update payment_attempt net_amount with surcharge details
|
|
payment_data
|
|
.payment_attempt
|
|
.net_amount
|
|
.set_surcharge_details(calculated_surcharge_details);
|
|
} else {
|
|
let surcharge_details =
|
|
payment_data
|
|
.payment_attempt
|
|
.get_surcharge_details()
|
|
.map(|surcharge_details| {
|
|
logger::debug!("surcharge sent in payments create request");
|
|
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")
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[instrument(skip_all)]
|
|
pub async fn call_surcharge_decision_management_for_session_flow(
|
|
_state: &SessionState,
|
|
_merchant_account: &domain::MerchantAccount,
|
|
_business_profile: &domain::Profile,
|
|
_payment_attempt: &storage::PaymentAttempt,
|
|
_payment_intent: &storage::PaymentIntent,
|
|
_billing_address: Option<hyperswitch_domain_models::address::Address>,
|
|
_session_connector_data: &[api::SessionConnectorData],
|
|
) -> RouterResult<Option<api::SessionSurchargeDetails>> {
|
|
todo!()
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[instrument(skip_all)]
|
|
pub async fn call_surcharge_decision_management_for_session_flow(
|
|
state: &SessionState,
|
|
_merchant_account: &domain::MerchantAccount,
|
|
_business_profile: &domain::Profile,
|
|
payment_attempt: &storage::PaymentAttempt,
|
|
payment_intent: &storage::PaymentIntent,
|
|
billing_address: Option<hyperswitch_domain_models::address::Address>,
|
|
session_connector_data: &[api::SessionConnectorData],
|
|
) -> RouterResult<Option<api::SessionSurchargeDetails>> {
|
|
if let Some(surcharge_amount) = payment_attempt.net_amount.get_surcharge_amount() {
|
|
Ok(Some(api::SessionSurchargeDetails::PreDetermined(
|
|
types::SurchargeDetails {
|
|
original_amount: payment_attempt.net_amount.get_order_amount(),
|
|
surcharge: Surcharge::Fixed(surcharge_amount),
|
|
tax_on_surcharge: None,
|
|
surcharge_amount,
|
|
tax_on_surcharge_amount: payment_attempt
|
|
.net_amount
|
|
.get_tax_on_surcharge()
|
|
.unwrap_or_default(),
|
|
},
|
|
)))
|
|
} else {
|
|
let payment_method_type_list = session_connector_data
|
|
.iter()
|
|
.map(|session_connector_data| session_connector_data.payment_method_type)
|
|
.collect();
|
|
|
|
#[cfg(feature = "v1")]
|
|
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();
|
|
|
|
// TODO: Move to business profile surcharge column
|
|
#[cfg(feature = "v2")]
|
|
let algorithm_ref: api::routing::RoutingAlgorithmRef = todo!();
|
|
|
|
let surcharge_results =
|
|
surcharge_decision_configs::perform_surcharge_decision_management_for_session_flow(
|
|
state,
|
|
algorithm_ref,
|
|
payment_attempt,
|
|
payment_intent,
|
|
billing_address,
|
|
&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))
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn payments_core<F, Res, Req, Op, FData, D>(
|
|
state: SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
profile_id: Option<id_type::ProfileId>,
|
|
key_store: domain::MerchantKeyStore,
|
|
operation: Op,
|
|
req: Req,
|
|
auth_flow: services::AuthFlow,
|
|
call_connector_action: CallConnectorAction,
|
|
eligible_connectors: Option<Vec<enums::Connector>>,
|
|
header_payload: HeaderPayload,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResponse<Res>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
FData: Send + Sync + Clone,
|
|
Op: Operation<F, Req, Data = D> + Send + Sync + Clone,
|
|
Req: Debug + Authenticate + Clone,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
Res: transformers::ToResponse<F, D, Op>,
|
|
// To create connector flow specific interface data
|
|
D: ConstructFlowSpecificData<F, FData, router_types::PaymentsResponseData>,
|
|
RouterData<F, FData, router_types::PaymentsResponseData>: Feature<F, FData>,
|
|
|
|
// To construct connector flow specific api
|
|
dyn api::Connector:
|
|
services::api::ConnectorIntegration<F, FData, router_types::PaymentsResponseData>,
|
|
|
|
// To perform router related operation for PaymentResponse
|
|
PaymentResponse: Operation<F, FData, Data = D>,
|
|
{
|
|
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::<_, _, _, _, _>(
|
|
&state,
|
|
req_state,
|
|
merchant_account,
|
|
profile_id,
|
|
key_store,
|
|
operation.clone(),
|
|
req,
|
|
call_connector_action,
|
|
auth_flow,
|
|
eligible_routable_connectors,
|
|
header_payload.clone(),
|
|
platform_merchant_account,
|
|
)
|
|
.await?;
|
|
|
|
Res::generate_response(
|
|
payment_data,
|
|
customer,
|
|
auth_flow,
|
|
&state.base_url,
|
|
operation,
|
|
&state.conf.connector_request_reference_id_config,
|
|
connector_http_status_code,
|
|
external_latency,
|
|
header_payload.x_hs_latency,
|
|
)
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn proxy_for_payments_core<F, Res, Req, Op, FData, D>(
|
|
state: SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
profile_id: Option<id_type::ProfileId>,
|
|
key_store: domain::MerchantKeyStore,
|
|
operation: Op,
|
|
req: Req,
|
|
auth_flow: services::AuthFlow,
|
|
call_connector_action: CallConnectorAction,
|
|
header_payload: HeaderPayload,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResponse<Res>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
FData: Send + Sync + Clone,
|
|
Op: Operation<F, Req, Data = D> + Send + Sync + Clone,
|
|
Req: Debug + Authenticate + Clone,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
Res: transformers::ToResponse<F, D, Op>,
|
|
// To create connector flow specific interface data
|
|
D: ConstructFlowSpecificData<F, FData, router_types::PaymentsResponseData>,
|
|
RouterData<F, FData, router_types::PaymentsResponseData>: Feature<F, FData>,
|
|
|
|
// To construct connector flow specific api
|
|
dyn api::Connector:
|
|
services::api::ConnectorIntegration<F, FData, router_types::PaymentsResponseData>,
|
|
|
|
// To perform router related operation for PaymentResponse
|
|
PaymentResponse: Operation<F, FData, Data = D>,
|
|
{
|
|
let (payment_data, _req, customer, connector_http_status_code, external_latency) =
|
|
proxy_for_payments_operation_core::<_, _, _, _, _>(
|
|
&state,
|
|
req_state,
|
|
merchant_account,
|
|
profile_id,
|
|
key_store,
|
|
operation.clone(),
|
|
req,
|
|
call_connector_action,
|
|
auth_flow,
|
|
header_payload.clone(),
|
|
platform_merchant_account,
|
|
)
|
|
.await?;
|
|
|
|
Res::generate_response(
|
|
payment_data,
|
|
customer,
|
|
auth_flow,
|
|
&state.base_url,
|
|
operation,
|
|
&state.conf.connector_request_reference_id_config,
|
|
connector_http_status_code,
|
|
external_latency,
|
|
header_payload.x_hs_latency,
|
|
)
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn payments_intent_core<F, Res, Req, Op, D>(
|
|
state: SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
profile: domain::Profile,
|
|
key_store: domain::MerchantKeyStore,
|
|
operation: Op,
|
|
req: Req,
|
|
payment_id: id_type::GlobalPaymentId,
|
|
header_payload: HeaderPayload,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResponse<Res>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
Op: Operation<F, Req, Data = D> + Send + Sync + Clone,
|
|
Req: Debug + Clone,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
Res: transformers::ToResponse<F, D, Op>,
|
|
{
|
|
let (payment_data, _req, customer) = payments_intent_operation_core::<_, _, _, _>(
|
|
&state,
|
|
req_state,
|
|
merchant_account.clone(),
|
|
profile,
|
|
key_store,
|
|
operation.clone(),
|
|
req,
|
|
payment_id,
|
|
header_payload.clone(),
|
|
platform_merchant_account,
|
|
)
|
|
.await?;
|
|
|
|
Res::generate_response(
|
|
payment_data,
|
|
customer,
|
|
&state.base_url,
|
|
operation,
|
|
&state.conf.connector_request_reference_id_config,
|
|
None,
|
|
None,
|
|
header_payload.x_hs_latency,
|
|
&merchant_account,
|
|
)
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn payments_get_intent_using_merchant_reference(
|
|
state: SessionState,
|
|
merchant_account: domain::MerchantAccount,
|
|
profile: domain::Profile,
|
|
key_store: domain::MerchantKeyStore,
|
|
req_state: ReqState,
|
|
merchant_reference_id: &id_type::PaymentReferenceId,
|
|
header_payload: HeaderPayload,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResponse<api::PaymentsIntentResponse> {
|
|
let db = state.store.as_ref();
|
|
let storage_scheme = merchant_account.storage_scheme;
|
|
let key_manager_state = &(&state).into();
|
|
let payment_intent = db
|
|
.find_payment_intent_by_merchant_reference_id_profile_id(
|
|
key_manager_state,
|
|
merchant_reference_id,
|
|
profile.get_id(),
|
|
&key_store,
|
|
&storage_scheme,
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
|
|
|
let (payment_data, _req, customer) = Box::pin(payments_intent_operation_core::<
|
|
api::PaymentGetIntent,
|
|
_,
|
|
_,
|
|
PaymentIntentData<api::PaymentGetIntent>,
|
|
>(
|
|
&state,
|
|
req_state,
|
|
merchant_account.clone(),
|
|
profile.clone(),
|
|
key_store.clone(),
|
|
operations::PaymentGetIntent,
|
|
api_models::payments::PaymentsGetIntentRequest {
|
|
id: payment_intent.get_id().clone(),
|
|
},
|
|
payment_intent.get_id().clone(),
|
|
header_payload.clone(),
|
|
platform_merchant_account,
|
|
))
|
|
.await?;
|
|
|
|
transformers::ToResponse::<
|
|
api::PaymentGetIntent,
|
|
PaymentIntentData<api::PaymentGetIntent>,
|
|
operations::PaymentGetIntent,
|
|
>::generate_response(
|
|
payment_data,
|
|
customer,
|
|
&state.base_url,
|
|
operations::PaymentGetIntent,
|
|
&state.conf.connector_request_reference_id_config,
|
|
None,
|
|
None,
|
|
header_payload.x_hs_latency,
|
|
&merchant_account,
|
|
)
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn payments_core<F, Res, Req, Op, FData, D>(
|
|
state: SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
profile: domain::Profile,
|
|
key_store: domain::MerchantKeyStore,
|
|
operation: Op,
|
|
req: Req,
|
|
payment_id: id_type::GlobalPaymentId,
|
|
call_connector_action: CallConnectorAction,
|
|
header_payload: HeaderPayload,
|
|
) -> RouterResponse<Res>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
Req: Send + Sync,
|
|
FData: Send + Sync + Clone,
|
|
Op: Operation<F, Req, Data = D> + ValidateStatusForOperation + Send + Sync + Clone,
|
|
Req: Debug,
|
|
D: OperationSessionGetters<F>
|
|
+ OperationSessionSetters<F>
|
|
+ transformers::GenerateResponse<Res>
|
|
+ Send
|
|
+ Sync
|
|
+ Clone,
|
|
// To create connector flow specific interface data
|
|
D: ConstructFlowSpecificData<F, FData, router_types::PaymentsResponseData>,
|
|
RouterData<F, FData, router_types::PaymentsResponseData>: Feature<F, FData>,
|
|
|
|
// To construct connector flow specific api
|
|
dyn api::Connector:
|
|
services::api::ConnectorIntegration<F, FData, router_types::PaymentsResponseData>,
|
|
|
|
// To perform router related operation for PaymentResponse
|
|
PaymentResponse: Operation<F, FData, Data = D>,
|
|
|
|
// To create updatable objects in post update tracker
|
|
RouterData<F, FData, router_types::PaymentsResponseData>:
|
|
hyperswitch_domain_models::router_data::TrackerPostUpdateObjects<F, FData, D>,
|
|
{
|
|
// Validate the request fields
|
|
operation
|
|
.to_validate_request()?
|
|
.validate_request(&req, &merchant_account)?;
|
|
|
|
// Get the tracker related information. This includes payment intent and payment attempt
|
|
let get_tracker_response = operation
|
|
.to_get_tracker()?
|
|
.get_trackers(
|
|
&state,
|
|
&payment_id,
|
|
&req,
|
|
&merchant_account,
|
|
&profile,
|
|
&key_store,
|
|
&header_payload,
|
|
None,
|
|
)
|
|
.await?;
|
|
|
|
let (payment_data, _req, customer, connector_http_status_code, external_latency) =
|
|
payments_operation_core::<_, _, _, _, _>(
|
|
&state,
|
|
req_state,
|
|
merchant_account.clone(),
|
|
key_store,
|
|
profile,
|
|
operation.clone(),
|
|
req,
|
|
get_tracker_response,
|
|
call_connector_action,
|
|
header_payload.clone(),
|
|
)
|
|
.await?;
|
|
|
|
payment_data.generate_response(
|
|
&state,
|
|
connector_http_status_code,
|
|
external_latency,
|
|
header_payload.x_hs_latency,
|
|
&merchant_account,
|
|
)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
#[cfg(feature = "v2")]
|
|
pub(crate) async fn payments_create_and_confirm_intent(
|
|
state: SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
profile: domain::Profile,
|
|
key_store: domain::MerchantKeyStore,
|
|
request: payments_api::PaymentsRequest,
|
|
payment_id: id_type::GlobalPaymentId,
|
|
mut header_payload: HeaderPayload,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResponse<payments_api::PaymentsResponse> {
|
|
use actix_http::body::MessageBody;
|
|
use common_utils::ext_traits::BytesExt;
|
|
use hyperswitch_domain_models::{
|
|
payments::{PaymentConfirmData, PaymentIntentData},
|
|
router_flow_types::{Authorize, PaymentCreateIntent, SetupMandate},
|
|
};
|
|
|
|
let payload = payments_api::PaymentsCreateIntentRequest::from(&request);
|
|
|
|
let create_intent_response = Box::pin(payments_intent_core::<
|
|
PaymentCreateIntent,
|
|
payments_api::PaymentsIntentResponse,
|
|
_,
|
|
_,
|
|
PaymentIntentData<PaymentCreateIntent>,
|
|
>(
|
|
state.clone(),
|
|
req_state.clone(),
|
|
merchant_account.clone(),
|
|
profile.clone(),
|
|
key_store.clone(),
|
|
operations::PaymentIntentCreate,
|
|
payload,
|
|
payment_id.clone(),
|
|
header_payload.clone(),
|
|
platform_merchant_account,
|
|
))
|
|
.await?;
|
|
|
|
logger::info!(?create_intent_response);
|
|
let create_intent_response = handle_payments_intent_response(create_intent_response)?;
|
|
|
|
// Adding client secret to ensure client secret validation passes during confirm intent step
|
|
header_payload.client_secret = Some(create_intent_response.client_secret.clone());
|
|
|
|
let payload = payments_api::PaymentsConfirmIntentRequest::from(&request);
|
|
|
|
let confirm_intent_response = decide_authorize_or_setup_intent_flow(
|
|
state,
|
|
req_state,
|
|
merchant_account,
|
|
profile,
|
|
key_store,
|
|
&create_intent_response,
|
|
payload,
|
|
payment_id,
|
|
header_payload,
|
|
)
|
|
.await?;
|
|
|
|
logger::info!(?confirm_intent_response);
|
|
let confirm_intent_response = handle_payments_intent_response(confirm_intent_response)?;
|
|
|
|
construct_payments_response(create_intent_response, confirm_intent_response)
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[inline]
|
|
fn handle_payments_intent_response<T>(
|
|
response: hyperswitch_domain_models::api::ApplicationResponse<T>,
|
|
) -> CustomResult<T, errors::ApiErrorResponse> {
|
|
match response {
|
|
hyperswitch_domain_models::api::ApplicationResponse::Json(body)
|
|
| hyperswitch_domain_models::api::ApplicationResponse::JsonWithHeaders((body, _)) => {
|
|
Ok(body)
|
|
}
|
|
hyperswitch_domain_models::api::ApplicationResponse::StatusOk
|
|
| hyperswitch_domain_models::api::ApplicationResponse::TextPlain(_)
|
|
| hyperswitch_domain_models::api::ApplicationResponse::JsonForRedirection(_)
|
|
| hyperswitch_domain_models::api::ApplicationResponse::Form(_)
|
|
| hyperswitch_domain_models::api::ApplicationResponse::PaymentLinkForm(_)
|
|
| hyperswitch_domain_models::api::ApplicationResponse::FileData(_)
|
|
| hyperswitch_domain_models::api::ApplicationResponse::GenericLinkForm(_) => {
|
|
Err(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Unexpected response from payment intent core")
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[inline]
|
|
fn construct_payments_response(
|
|
create_intent_response: payments_api::PaymentsIntentResponse,
|
|
confirm_intent_response: payments_api::PaymentsConfirmIntentResponse,
|
|
) -> RouterResponse<payments_api::PaymentsResponse> {
|
|
let response = payments_api::PaymentsResponse {
|
|
id: confirm_intent_response.id,
|
|
status: confirm_intent_response.status,
|
|
amount: confirm_intent_response.amount,
|
|
customer_id: confirm_intent_response.customer_id,
|
|
connector: confirm_intent_response.connector,
|
|
client_secret: confirm_intent_response.client_secret,
|
|
created: confirm_intent_response.created,
|
|
payment_method_data: confirm_intent_response.payment_method_data,
|
|
payment_method_type: confirm_intent_response.payment_method_type,
|
|
payment_method_subtype: confirm_intent_response.payment_method_subtype,
|
|
next_action: confirm_intent_response.next_action,
|
|
connector_transaction_id: confirm_intent_response.connector_transaction_id,
|
|
connector_reference_id: confirm_intent_response.connector_reference_id,
|
|
connector_token_details: confirm_intent_response.connector_token_details,
|
|
merchant_connector_id: confirm_intent_response.merchant_connector_id,
|
|
browser_info: confirm_intent_response.browser_info,
|
|
error: confirm_intent_response.error,
|
|
};
|
|
|
|
Ok(hyperswitch_domain_models::api::ApplicationResponse::Json(
|
|
response,
|
|
))
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn decide_authorize_or_setup_intent_flow(
|
|
state: SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
profile: domain::Profile,
|
|
key_store: domain::MerchantKeyStore,
|
|
create_intent_response: &payments_api::PaymentsIntentResponse,
|
|
confirm_intent_request: payments_api::PaymentsConfirmIntentRequest,
|
|
payment_id: id_type::GlobalPaymentId,
|
|
header_payload: HeaderPayload,
|
|
) -> RouterResponse<payments_api::PaymentsConfirmIntentResponse> {
|
|
use hyperswitch_domain_models::{
|
|
payments::PaymentConfirmData,
|
|
router_flow_types::{Authorize, SetupMandate},
|
|
};
|
|
|
|
if create_intent_response.amount_details.order_amount == MinorUnit::zero() {
|
|
Box::pin(payments_core::<
|
|
SetupMandate,
|
|
api_models::payments::PaymentsConfirmIntentResponse,
|
|
_,
|
|
_,
|
|
_,
|
|
PaymentConfirmData<SetupMandate>,
|
|
>(
|
|
state,
|
|
req_state,
|
|
merchant_account,
|
|
profile,
|
|
key_store,
|
|
operations::PaymentIntentConfirm,
|
|
confirm_intent_request,
|
|
payment_id,
|
|
CallConnectorAction::Trigger,
|
|
header_payload,
|
|
))
|
|
.await
|
|
} else {
|
|
Box::pin(payments_core::<
|
|
Authorize,
|
|
api_models::payments::PaymentsConfirmIntentResponse,
|
|
_,
|
|
_,
|
|
_,
|
|
PaymentConfirmData<Authorize>,
|
|
>(
|
|
state,
|
|
req_state,
|
|
merchant_account,
|
|
profile,
|
|
key_store,
|
|
operations::PaymentIntentConfirm,
|
|
confirm_intent_request,
|
|
payment_id,
|
|
CallConnectorAction::Trigger,
|
|
header_payload,
|
|
))
|
|
.await
|
|
}
|
|
}
|
|
|
|
fn is_start_pay<Op: Debug>(operation: &Op) -> bool {
|
|
format!("{operation:?}").eq("PaymentStart")
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[derive(Clone, Debug, serde::Serialize)]
|
|
pub struct PaymentsRedirectResponseData {
|
|
pub connector: Option<String>,
|
|
pub param: Option<String>,
|
|
pub merchant_id: Option<id_type::MerchantId>,
|
|
pub json_payload: Option<serde_json::Value>,
|
|
pub resource_id: api::PaymentIdType,
|
|
pub force_sync: bool,
|
|
pub creds_identifier: Option<String>,
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[derive(Clone, Debug, serde::Serialize)]
|
|
pub struct PaymentsRedirectResponseData {
|
|
pub payment_id: id_type::GlobalPaymentId,
|
|
pub query_params: String,
|
|
pub json_payload: Option<serde_json::Value>,
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
pub trait PaymentRedirectFlow: Sync {
|
|
// Associated type for call_payment_flow response
|
|
type PaymentFlowResponse;
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn call_payment_flow(
|
|
&self,
|
|
state: &SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
merchant_key_store: domain::MerchantKeyStore,
|
|
req: PaymentsRedirectResponseData,
|
|
connector_action: CallConnectorAction,
|
|
connector: String,
|
|
payment_id: id_type::PaymentId,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResult<Self::PaymentFlowResponse>;
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn call_payment_flow(
|
|
&self,
|
|
state: &SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
merchant_key_store: domain::MerchantKeyStore,
|
|
profile: domain::Profile,
|
|
req: PaymentsRedirectResponseData,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResult<Self::PaymentFlowResponse>;
|
|
|
|
fn get_payment_action(&self) -> services::PaymentAction;
|
|
|
|
#[cfg(feature = "v1")]
|
|
fn generate_response(
|
|
&self,
|
|
payment_flow_response: &Self::PaymentFlowResponse,
|
|
payment_id: id_type::PaymentId,
|
|
connector: String,
|
|
) -> RouterResult<services::ApplicationResponse<api::RedirectionResponse>>;
|
|
|
|
#[cfg(feature = "v2")]
|
|
fn generate_response(
|
|
&self,
|
|
payment_flow_response: &Self::PaymentFlowResponse,
|
|
) -> RouterResult<services::ApplicationResponse<api::RedirectionResponse>>;
|
|
|
|
#[cfg(feature = "v1")]
|
|
async fn handle_payments_redirect_response(
|
|
&self,
|
|
state: SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
key_store: domain::MerchantKeyStore,
|
|
req: PaymentsRedirectResponseData,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResponse<api::RedirectionResponse> {
|
|
metrics::REDIRECTION_TRIGGERED.add(
|
|
1,
|
|
router_env::metric_attributes!(
|
|
(
|
|
"connector",
|
|
req.connector.to_owned().unwrap_or("null".to_string()),
|
|
),
|
|
("merchant_id", merchant_account.get_id().clone()),
|
|
),
|
|
);
|
|
let connector = req.connector.clone().get_required_value("connector")?;
|
|
|
|
let query_params = req.param.clone().get_required_value("param")?;
|
|
|
|
#[cfg(feature = "v1")]
|
|
let resource_id = api::PaymentIdTypeExt::get_payment_intent_id(&req.resource_id)
|
|
.change_context(errors::ApiErrorResponse::MissingRequiredField {
|
|
field_name: "payment_id",
|
|
})?;
|
|
|
|
#[cfg(feature = "v2")]
|
|
//TODO: Will get the global payment id from the resource id, we need to handle this in the further flow
|
|
let resource_id: id_type::PaymentId = todo!();
|
|
|
|
// 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 payment_flow_response = self
|
|
.call_payment_flow(
|
|
&state,
|
|
req_state,
|
|
merchant_account.clone(),
|
|
key_store,
|
|
req.clone(),
|
|
flow_type,
|
|
connector.clone(),
|
|
resource_id.clone(),
|
|
platform_merchant_account,
|
|
)
|
|
.await?;
|
|
|
|
self.generate_response(&payment_flow_response, resource_id, connector)
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn handle_payments_redirect_response(
|
|
&self,
|
|
state: SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
key_store: domain::MerchantKeyStore,
|
|
profile: domain::Profile,
|
|
request: PaymentsRedirectResponseData,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResponse<api::RedirectionResponse> {
|
|
metrics::REDIRECTION_TRIGGERED.add(
|
|
1,
|
|
router_env::metric_attributes!(("merchant_id", merchant_account.get_id().clone())),
|
|
);
|
|
|
|
let payment_flow_response = self
|
|
.call_payment_flow(
|
|
&state,
|
|
req_state,
|
|
merchant_account,
|
|
key_store,
|
|
profile,
|
|
request,
|
|
platform_merchant_account,
|
|
)
|
|
.await?;
|
|
|
|
self.generate_response(&payment_flow_response)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct PaymentRedirectCompleteAuthorize;
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[async_trait::async_trait]
|
|
impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize {
|
|
type PaymentFlowResponse = router_types::RedirectPaymentFlowResponse;
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn call_payment_flow(
|
|
&self,
|
|
state: &SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
merchant_key_store: domain::MerchantKeyStore,
|
|
req: PaymentsRedirectResponseData,
|
|
connector_action: CallConnectorAction,
|
|
_connector: String,
|
|
_payment_id: id_type::PaymentId,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResult<Self::PaymentFlowResponse> {
|
|
let key_manager_state = &state.into();
|
|
|
|
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()),
|
|
}),
|
|
search_tags: None,
|
|
apple_pay_recurring_details: None,
|
|
}),
|
|
..Default::default()
|
|
};
|
|
let response = Box::pin(payments_core::<
|
|
api::CompleteAuthorize,
|
|
api::PaymentsResponse,
|
|
_,
|
|
_,
|
|
_,
|
|
_,
|
|
>(
|
|
state.clone(),
|
|
req_state,
|
|
merchant_account,
|
|
None,
|
|
merchant_key_store.clone(),
|
|
operations::payment_complete_authorize::CompleteAuthorize,
|
|
payment_confirm_req,
|
|
services::api::AuthFlow::Merchant,
|
|
connector_action,
|
|
None,
|
|
HeaderPayload::default(),
|
|
platform_merchant_account,
|
|
))
|
|
.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(key_manager_state, &merchant_key_store, profile_id)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::ProfileNotFound {
|
|
id: profile_id.get_string_repr().to_owned(),
|
|
})?;
|
|
Ok(router_types::RedirectPaymentFlowResponse {
|
|
payments_response,
|
|
business_profile,
|
|
})
|
|
}
|
|
|
|
fn get_payment_action(&self) -> services::PaymentAction {
|
|
services::PaymentAction::CompleteAuthorize
|
|
}
|
|
|
|
fn generate_response(
|
|
&self,
|
|
payment_flow_response: &Self::PaymentFlowResponse,
|
|
payment_id: id_type::PaymentId,
|
|
connector: String,
|
|
) -> RouterResult<services::ApplicationResponse<api::RedirectionResponse>> {
|
|
let payments_response = &payment_flow_response.payments_response;
|
|
// There might be multiple redirections needed for some flows
|
|
// If the status is requires customer action, then send the startpay url again
|
|
// The redirection data must have been provided and updated by the connector
|
|
let redirection_response = match payments_response.status {
|
|
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::FetchQrCodeInformation{..} => None,
|
|
api_models::payments::NextActionData::DisplayVoucherInformation{ .. } => None,
|
|
api_models::payments::NextActionData::WaitScreenInformation{..} => None,
|
|
api_models::payments::NextActionData::ThreeDsInvoke{..} => None,
|
|
api_models::payments::NextActionData::InvokeSdkClient{..} => None,
|
|
api_models::payments::NextActionData::CollectOtp{ .. } => 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
|
|
enums::IntentStatus::Succeeded
|
|
| enums::IntentStatus::Failed
|
|
| enums::IntentStatus::Cancelled | enums::IntentStatus::RequiresCapture| enums::IntentStatus::Processing=> helpers::get_handle_response_url(
|
|
payment_id,
|
|
&payment_flow_response.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;
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[async_trait::async_trait]
|
|
impl PaymentRedirectFlow for PaymentRedirectSync {
|
|
type PaymentFlowResponse = router_types::RedirectPaymentFlowResponse;
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn call_payment_flow(
|
|
&self,
|
|
state: &SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
merchant_key_store: domain::MerchantKeyStore,
|
|
req: PaymentsRedirectResponseData,
|
|
connector_action: CallConnectorAction,
|
|
_connector: String,
|
|
_payment_id: id_type::PaymentId,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResult<Self::PaymentFlowResponse> {
|
|
let key_manager_state = &state.into();
|
|
|
|
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,
|
|
};
|
|
let response = Box::pin(
|
|
payments_core::<api::PSync, api::PaymentsResponse, _, _, _, _>(
|
|
state.clone(),
|
|
req_state,
|
|
merchant_account,
|
|
None,
|
|
merchant_key_store.clone(),
|
|
PaymentStatus,
|
|
payment_sync_req,
|
|
services::api::AuthFlow::Merchant,
|
|
connector_action,
|
|
None,
|
|
HeaderPayload::default(),
|
|
platform_merchant_account,
|
|
),
|
|
)
|
|
.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(key_manager_state, &merchant_key_store, profile_id)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::ProfileNotFound {
|
|
id: profile_id.get_string_repr().to_owned(),
|
|
})?;
|
|
Ok(router_types::RedirectPaymentFlowResponse {
|
|
payments_response,
|
|
business_profile,
|
|
})
|
|
}
|
|
fn generate_response(
|
|
&self,
|
|
payment_flow_response: &Self::PaymentFlowResponse,
|
|
payment_id: id_type::PaymentId,
|
|
connector: String,
|
|
) -> RouterResult<services::ApplicationResponse<api::RedirectionResponse>> {
|
|
Ok(services::ApplicationResponse::JsonForRedirection(
|
|
helpers::get_handle_response_url(
|
|
payment_id,
|
|
&payment_flow_response.business_profile,
|
|
&payment_flow_response.payments_response,
|
|
connector,
|
|
)?,
|
|
))
|
|
}
|
|
|
|
fn get_payment_action(&self) -> services::PaymentAction {
|
|
services::PaymentAction::PSync
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
impl ValidateStatusForOperation for &PaymentRedirectSync {
|
|
fn validate_status_for_operation(
|
|
&self,
|
|
intent_status: common_enums::IntentStatus,
|
|
) -> Result<(), errors::ApiErrorResponse> {
|
|
match intent_status {
|
|
common_enums::IntentStatus::RequiresCustomerAction => Ok(()),
|
|
common_enums::IntentStatus::Succeeded
|
|
| common_enums::IntentStatus::Failed
|
|
| common_enums::IntentStatus::Cancelled
|
|
| common_enums::IntentStatus::Processing
|
|
| common_enums::IntentStatus::RequiresPaymentMethod
|
|
| common_enums::IntentStatus::RequiresMerchantAction
|
|
| common_enums::IntentStatus::RequiresCapture
|
|
| common_enums::IntentStatus::PartiallyCaptured
|
|
| common_enums::IntentStatus::RequiresConfirmation
|
|
| common_enums::IntentStatus::PartiallyCapturedAndCapturable => {
|
|
Err(errors::ApiErrorResponse::PaymentUnexpectedState {
|
|
current_flow: format!("{self:?}"),
|
|
field_name: "status".to_string(),
|
|
current_value: intent_status.to_string(),
|
|
states: ["requires_customer_action".to_string()].join(", "),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[async_trait::async_trait]
|
|
impl PaymentRedirectFlow for PaymentRedirectSync {
|
|
type PaymentFlowResponse =
|
|
router_types::RedirectPaymentFlowResponse<PaymentStatusData<api::PSync>>;
|
|
|
|
async fn call_payment_flow(
|
|
&self,
|
|
state: &SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
merchant_key_store: domain::MerchantKeyStore,
|
|
profile: domain::Profile,
|
|
req: PaymentsRedirectResponseData,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResult<Self::PaymentFlowResponse> {
|
|
let payment_id = req.payment_id.clone();
|
|
|
|
let payment_sync_request = api::PaymentsRetrieveRequest {
|
|
param: Some(req.query_params.clone()),
|
|
force_sync: true,
|
|
expand_attempts: false,
|
|
};
|
|
|
|
let operation = operations::PaymentGet;
|
|
let boxed_operation: BoxedOperation<
|
|
'_,
|
|
api::PSync,
|
|
api::PaymentsRetrieveRequest,
|
|
PaymentStatusData<api::PSync>,
|
|
> = Box::new(operation);
|
|
|
|
let get_tracker_response = boxed_operation
|
|
.to_get_tracker()?
|
|
.get_trackers(
|
|
state,
|
|
&payment_id,
|
|
&payment_sync_request,
|
|
&merchant_account,
|
|
&profile,
|
|
&merchant_key_store,
|
|
&HeaderPayload::default(),
|
|
platform_merchant_account.as_ref(),
|
|
)
|
|
.await?;
|
|
|
|
let payment_data = &get_tracker_response.payment_data;
|
|
self.validate_status_for_operation(payment_data.payment_intent.status)?;
|
|
|
|
let payment_attempt = payment_data
|
|
.payment_attempt
|
|
.as_ref()
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("payment_attempt not found in get_tracker_response")?;
|
|
|
|
let connector = payment_attempt
|
|
.connector
|
|
.as_ref()
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(
|
|
"connector is not set in payment attempt in finish redirection flow",
|
|
)?;
|
|
|
|
// 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 call_connector_action = connector_data
|
|
.connector
|
|
.get_flow_type(
|
|
&req.query_params,
|
|
req.json_payload.clone(),
|
|
self.get_payment_action(),
|
|
)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to decide the response flow")?;
|
|
|
|
let (payment_data, _, _, _, _) =
|
|
Box::pin(payments_operation_core::<api::PSync, _, _, _, _>(
|
|
state,
|
|
req_state,
|
|
merchant_account,
|
|
merchant_key_store.clone(),
|
|
profile.clone(),
|
|
operation,
|
|
payment_sync_request,
|
|
get_tracker_response,
|
|
call_connector_action,
|
|
HeaderPayload::default(),
|
|
))
|
|
.await?;
|
|
|
|
Ok(router_types::RedirectPaymentFlowResponse {
|
|
payment_data,
|
|
profile,
|
|
})
|
|
}
|
|
fn generate_response(
|
|
&self,
|
|
payment_flow_response: &Self::PaymentFlowResponse,
|
|
) -> RouterResult<services::ApplicationResponse<api::RedirectionResponse>> {
|
|
let payment_intent = &payment_flow_response.payment_data.payment_intent;
|
|
let profile = &payment_flow_response.profile;
|
|
|
|
let return_url = payment_intent
|
|
.return_url
|
|
.as_ref()
|
|
.or(profile.return_url.as_ref())
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("return url not found in payment intent and profile")?
|
|
.to_owned();
|
|
|
|
let return_url = return_url
|
|
.add_query_params(("id", payment_intent.id.get_string_repr()))
|
|
.add_query_params(("status", &payment_intent.status.to_string()));
|
|
|
|
let return_url_str = return_url.into_inner().to_string();
|
|
|
|
Ok(services::ApplicationResponse::JsonForRedirection(
|
|
api::RedirectionResponse {
|
|
return_url_with_query_params: return_url_str,
|
|
},
|
|
))
|
|
}
|
|
|
|
fn get_payment_action(&self) -> services::PaymentAction {
|
|
services::PaymentAction::PSync
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct PaymentAuthenticateCompleteAuthorize;
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[async_trait::async_trait]
|
|
impl PaymentRedirectFlow for PaymentAuthenticateCompleteAuthorize {
|
|
type PaymentFlowResponse = router_types::AuthenticatePaymentFlowResponse;
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn call_payment_flow(
|
|
&self,
|
|
state: &SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: domain::MerchantAccount,
|
|
merchant_key_store: domain::MerchantKeyStore,
|
|
req: PaymentsRedirectResponseData,
|
|
connector_action: CallConnectorAction,
|
|
connector: String,
|
|
payment_id: id_type::PaymentId,
|
|
platform_merchant_account: Option<domain::MerchantAccount>,
|
|
) -> RouterResult<Self::PaymentFlowResponse> {
|
|
let merchant_id = merchant_account.get_id().clone();
|
|
let key_manager_state = &state.into();
|
|
|
|
let payment_intent = state
|
|
.store
|
|
.find_payment_intent_by_payment_id_merchant_id(
|
|
key_manager_state,
|
|
&payment_id,
|
|
&merchant_id,
|
|
&merchant_key_store,
|
|
merchant_account.storage_scheme,
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
|
let payment_attempt = state
|
|
.store
|
|
.find_payment_attempt_by_attempt_id_merchant_id(
|
|
&payment_intent.active_attempt.get_id(),
|
|
&merchant_id,
|
|
merchant_account.storage_scheme,
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
|
let authentication_id = payment_attempt
|
|
.authentication_id
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("missing authentication_id in payment_attempt")?;
|
|
let authentication = state
|
|
.store
|
|
.find_authentication_by_merchant_id_authentication_id(
|
|
&merchant_id,
|
|
authentication_id.clone(),
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::AuthenticationNotFound {
|
|
id: authentication_id,
|
|
})?;
|
|
// Fetching merchant_connector_account to check if pull_mechanism is enabled for 3ds connector
|
|
let authentication_merchant_connector_account = helpers::get_merchant_connector_account(
|
|
state,
|
|
&merchant_id,
|
|
None,
|
|
&merchant_key_store,
|
|
&payment_intent
|
|
.profile_id
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("missing profile_id in payment_intent")?,
|
|
&payment_attempt
|
|
.authentication_connector
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("missing authentication connector in payment_intent")?,
|
|
None,
|
|
)
|
|
.await?;
|
|
let is_pull_mechanism_enabled =
|
|
utils::check_if_pull_mechanism_for_external_3ds_enabled_from_connector_metadata(
|
|
authentication_merchant_connector_account
|
|
.get_metadata()
|
|
.map(|metadata| metadata.expose()),
|
|
);
|
|
let response = if is_pull_mechanism_enabled
|
|
|| authentication.authentication_type
|
|
!= Some(common_enums::DecoupledAuthenticationType::Challenge)
|
|
{
|
|
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(),
|
|
),
|
|
}),
|
|
search_tags: None,
|
|
apple_pay_recurring_details: None,
|
|
}),
|
|
..Default::default()
|
|
};
|
|
Box::pin(payments_core::<
|
|
api::Authorize,
|
|
api::PaymentsResponse,
|
|
_,
|
|
_,
|
|
_,
|
|
_,
|
|
>(
|
|
state.clone(),
|
|
req_state,
|
|
merchant_account,
|
|
None,
|
|
merchant_key_store.clone(),
|
|
PaymentConfirm,
|
|
payment_confirm_req,
|
|
services::api::AuthFlow::Merchant,
|
|
connector_action,
|
|
None,
|
|
HeaderPayload::with_source(enums::PaymentSource::ExternalAuthenticator),
|
|
platform_merchant_account,
|
|
))
|
|
.await?
|
|
} else {
|
|
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, _, _, _, _>(
|
|
state.clone(),
|
|
req_state,
|
|
merchant_account.clone(),
|
|
None,
|
|
merchant_key_store.clone(),
|
|
PaymentStatus,
|
|
payment_sync_req,
|
|
services::api::AuthFlow::Merchant,
|
|
connector_action,
|
|
None,
|
|
HeaderPayload::default(),
|
|
platform_merchant_account,
|
|
),
|
|
)
|
|
.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"),
|
|
}?;
|
|
// When intent status is RequiresCustomerAction, Set poll_id in redis to allow the fetch status of poll through retrieve_poll_status api from client
|
|
if payments_response.status == common_enums::IntentStatus::RequiresCustomerAction {
|
|
let req_poll_id = core_utils::get_external_authentication_request_poll_id(&payment_id);
|
|
let poll_id = core_utils::get_poll_id(&merchant_id, req_poll_id.clone());
|
|
let redis_conn = state
|
|
.store
|
|
.get_redis_conn()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to get redis connection")?;
|
|
redis_conn
|
|
.set_key_with_expiry(
|
|
&poll_id.into(),
|
|
api_models::poll::PollStatus::Pending.to_string(),
|
|
crate::consts::POLL_ID_TTL,
|
|
)
|
|
.await
|
|
.change_context(errors::StorageError::KVError)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to add poll_id in redis")?;
|
|
};
|
|
let default_poll_config = router_types::PollConfig::default();
|
|
let default_config_str = default_poll_config
|
|
.encode_to_string_of_json()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Error while stringifying default poll config")?;
|
|
let poll_config = state
|
|
.store
|
|
.find_config_by_key_unwrap_or(
|
|
&router_types::PollConfig::get_poll_config_key(connector),
|
|
Some(default_config_str),
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("The poll config was not found in the DB")?;
|
|
let poll_config: router_types::PollConfig = poll_config
|
|
.config
|
|
.parse_struct("PollConfig")
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Error while parsing PollConfig")?;
|
|
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(key_manager_state, &merchant_key_store, profile_id)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::ProfileNotFound {
|
|
id: profile_id.get_string_repr().to_owned(),
|
|
})?;
|
|
Ok(router_types::AuthenticatePaymentFlowResponse {
|
|
payments_response,
|
|
poll_config,
|
|
business_profile,
|
|
})
|
|
}
|
|
fn generate_response(
|
|
&self,
|
|
payment_flow_response: &Self::PaymentFlowResponse,
|
|
payment_id: id_type::PaymentId,
|
|
connector: String,
|
|
) -> RouterResult<services::ApplicationResponse<api::RedirectionResponse>> {
|
|
let payments_response = &payment_flow_response.payments_response;
|
|
let redirect_response = helpers::get_handle_response_url(
|
|
payment_id.clone(),
|
|
&payment_flow_response.business_profile,
|
|
payments_response,
|
|
connector.clone(),
|
|
)?;
|
|
// html script to check if inside iframe, then send post message to parent for redirection else redirect self to return_url
|
|
let html = core_utils::get_html_redirect_response_for_external_authentication(
|
|
redirect_response.return_url_with_query_params,
|
|
payments_response,
|
|
payment_id,
|
|
&payment_flow_response.poll_config,
|
|
)?;
|
|
Ok(services::ApplicationResponse::Form(Box::new(
|
|
services::RedirectionFormData {
|
|
redirect_form: services::RedirectForm::Html { html_data: html },
|
|
payment_method_data: None,
|
|
amount: payments_response.amount.to_string(),
|
|
currency: payments_response.currency.clone(),
|
|
},
|
|
)))
|
|
}
|
|
|
|
fn get_payment_action(&self) -> services::PaymentAction {
|
|
services::PaymentAction::PaymentAuthenticateCompleteAuthorize
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
#[instrument(skip_all)]
|
|
pub async fn call_connector_service<F, RouterDReq, ApiRequest, D>(
|
|
state: &SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
key_store: &domain::MerchantKeyStore,
|
|
connector: api::ConnectorData,
|
|
operation: &BoxedOperation<'_, F, ApiRequest, D>,
|
|
payment_data: &mut D,
|
|
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>,
|
|
business_profile: &domain::Profile,
|
|
is_retry_payment: bool,
|
|
) -> RouterResult<(
|
|
RouterData<F, RouterDReq, router_types::PaymentsResponseData>,
|
|
helpers::MerchantConnectorAccountType,
|
|
)>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
RouterDReq: Send + Sync,
|
|
|
|
// To create connector flow specific interface data
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
D: ConstructFlowSpecificData<F, RouterDReq, router_types::PaymentsResponseData>,
|
|
RouterData<F, RouterDReq, router_types::PaymentsResponseData>: Feature<F, RouterDReq> + Send,
|
|
// 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?;
|
|
|
|
#[cfg(feature = "v1")]
|
|
if payment_data
|
|
.get_payment_attempt()
|
|
.merchant_connector_id
|
|
.is_none()
|
|
{
|
|
payment_data.set_merchant_connector_id_in_attempt(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,
|
|
business_profile,
|
|
)
|
|
.await?;
|
|
*payment_data = pd;
|
|
|
|
// Validating the blocklist guard and generate the fingerprint
|
|
blocklist_guard(state, merchant_account, key_store, 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?;
|
|
|
|
#[cfg(feature = "v1")]
|
|
let merchant_recipient_data = if let Some(true) = payment_data
|
|
.get_payment_intent()
|
|
.is_payment_processor_token_flow
|
|
{
|
|
None
|
|
} else {
|
|
payment_data
|
|
.get_merchant_recipient_data(
|
|
state,
|
|
merchant_account,
|
|
key_store,
|
|
&merchant_connector_account,
|
|
&connector,
|
|
)
|
|
.await?
|
|
};
|
|
|
|
// TODO: handle how we read `is_processor_token_flow` in v2 and then call `get_merchant_recipient_data`
|
|
#[cfg(feature = "v2")]
|
|
let merchant_recipient_data = None;
|
|
|
|
let mut router_data = payment_data
|
|
.construct_router_data(
|
|
state,
|
|
connector.connector.id(),
|
|
merchant_account,
|
|
key_store,
|
|
customer,
|
|
&merchant_connector_account,
|
|
merchant_recipient_data,
|
|
None,
|
|
)
|
|
.await?;
|
|
|
|
let add_access_token_result = router_data
|
|
.add_access_token(
|
|
state,
|
|
&connector,
|
|
merchant_account,
|
|
payment_data.get_creds_identifier(),
|
|
)
|
|
.await?;
|
|
|
|
router_data = router_data.add_session_token(state, &connector).await?;
|
|
|
|
let should_continue_further = access_token::update_router_data_with_access_token_result(
|
|
&add_access_token_result,
|
|
&mut router_data,
|
|
&call_connector_action,
|
|
);
|
|
|
|
router_data.payment_method_token = if let Some(decrypted_token) =
|
|
add_decrypted_payment_method_token(tokenization_action.clone(), payment_data).await?
|
|
{
|
|
Some(decrypted_token)
|
|
} else {
|
|
router_data.payment_method_token
|
|
};
|
|
|
|
let payment_method_token_response = router_data
|
|
.add_payment_method_token(
|
|
state,
|
|
&connector,
|
|
&tokenization_action,
|
|
should_continue_further,
|
|
)
|
|
.await?;
|
|
|
|
let mut should_continue_further =
|
|
tokenization::update_router_data_with_payment_method_token_result(
|
|
payment_method_token_response,
|
|
&mut router_data,
|
|
is_retry_payment,
|
|
should_continue_further,
|
|
);
|
|
|
|
(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.push_sessions_token(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.get_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.clone(),
|
|
)
|
|
.await?;
|
|
|
|
let router_data = 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.get_payment_attempt().status;
|
|
router_data
|
|
.decide_flows(
|
|
state,
|
|
&connector,
|
|
call_connector_action,
|
|
connector_request,
|
|
business_profile,
|
|
header_payload.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()));
|
|
|
|
Ok((router_data, merchant_connector_account))
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
#[instrument(skip_all)]
|
|
pub async fn call_connector_service<F, RouterDReq, ApiRequest, D>(
|
|
state: &SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
key_store: &domain::MerchantKeyStore,
|
|
connector: api::ConnectorData,
|
|
operation: &BoxedOperation<'_, F, ApiRequest, D>,
|
|
payment_data: &mut D,
|
|
customer: &Option<domain::Customer>,
|
|
call_connector_action: CallConnectorAction,
|
|
schedule_time: Option<time::PrimitiveDateTime>,
|
|
header_payload: HeaderPayload,
|
|
frm_suggestion: Option<storage_enums::FrmSuggestion>,
|
|
business_profile: &domain::Profile,
|
|
is_retry_payment: bool,
|
|
) -> RouterResult<RouterData<F, RouterDReq, router_types::PaymentsResponseData>>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
RouterDReq: Send + Sync,
|
|
|
|
// To create connector flow specific interface data
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
D: ConstructFlowSpecificData<F, RouterDReq, router_types::PaymentsResponseData>,
|
|
RouterData<F, RouterDReq, router_types::PaymentsResponseData>: Feature<F, RouterDReq> + Send,
|
|
// 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_id = connector
|
|
.merchant_connector_id
|
|
.as_ref()
|
|
.get_required_value("merchant_connector_id")
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("connector id is not set")?;
|
|
|
|
let merchant_connector_account = state
|
|
.store
|
|
.find_merchant_connector_account_by_id(&state.into(), merchant_connector_id, key_store)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
|
|
id: merchant_connector_id.get_string_repr().to_owned(),
|
|
})?;
|
|
|
|
let mut router_data = payment_data
|
|
.construct_router_data(
|
|
state,
|
|
connector.connector.id(),
|
|
merchant_account,
|
|
key_store,
|
|
customer,
|
|
&merchant_connector_account,
|
|
None,
|
|
None,
|
|
)
|
|
.await?;
|
|
|
|
let add_access_token_result = router_data
|
|
.add_access_token(
|
|
state,
|
|
&connector,
|
|
merchant_account,
|
|
payment_data.get_creds_identifier(),
|
|
)
|
|
.await?;
|
|
|
|
router_data = router_data.add_session_token(state, &connector).await?;
|
|
|
|
let should_continue_further = access_token::update_router_data_with_access_token_result(
|
|
&add_access_token_result,
|
|
&mut router_data,
|
|
&call_connector_action,
|
|
);
|
|
|
|
// 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)
|
|
};
|
|
|
|
// 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,
|
|
// TODO: update the customer with connector customer id
|
|
None,
|
|
key_store,
|
|
frm_suggestion,
|
|
header_payload.clone(),
|
|
)
|
|
.await?;
|
|
|
|
let router_data = 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
|
|
// TODO: status is already set when constructing payment data, why should this be done again?
|
|
// router_data.status = payment_data.get_payment_attempt().status;
|
|
router_data
|
|
.decide_flows(
|
|
state,
|
|
&connector,
|
|
call_connector_action,
|
|
connector_request,
|
|
business_profile,
|
|
header_payload.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()));
|
|
|
|
Ok(router_data)
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
// This function does not perform the tokenization action, as the payment method is not saved in this flow.
|
|
#[allow(clippy::too_many_arguments)]
|
|
#[instrument(skip_all)]
|
|
pub async fn proxy_for_call_connector_service<F, RouterDReq, ApiRequest, D>(
|
|
state: &SessionState,
|
|
req_state: ReqState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
key_store: &domain::MerchantKeyStore,
|
|
connector: api::ConnectorData,
|
|
operation: &BoxedOperation<'_, F, ApiRequest, D>,
|
|
payment_data: &mut D,
|
|
customer: &Option<domain::Customer>,
|
|
call_connector_action: CallConnectorAction,
|
|
validate_result: &operations::ValidateResult,
|
|
schedule_time: Option<time::PrimitiveDateTime>,
|
|
header_payload: HeaderPayload,
|
|
|
|
business_profile: &domain::Profile,
|
|
) -> RouterResult<(
|
|
RouterData<F, RouterDReq, router_types::PaymentsResponseData>,
|
|
helpers::MerchantConnectorAccountType,
|
|
)>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
RouterDReq: Send + Sync,
|
|
|
|
// To create connector flow specific interface data
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
D: ConstructFlowSpecificData<F, RouterDReq, router_types::PaymentsResponseData>,
|
|
RouterData<F, RouterDReq, router_types::PaymentsResponseData>: Feature<F, RouterDReq> + Send,
|
|
// 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
|
|
.get_payment_attempt()
|
|
.merchant_connector_id
|
|
.is_none()
|
|
{
|
|
payment_data.set_merchant_connector_id_in_attempt(merchant_connector_account.get_mca_id());
|
|
}
|
|
|
|
let merchant_recipient_data = None;
|
|
|
|
let mut router_data = payment_data
|
|
.construct_router_data(
|
|
state,
|
|
connector.connector.id(),
|
|
merchant_account,
|
|
key_store,
|
|
customer,
|
|
&merchant_connector_account,
|
|
merchant_recipient_data,
|
|
None,
|
|
)
|
|
.await?;
|
|
|
|
let add_access_token_result = router_data
|
|
.add_access_token(
|
|
state,
|
|
&connector,
|
|
merchant_account,
|
|
payment_data.get_creds_identifier(),
|
|
)
|
|
.await?;
|
|
|
|
router_data = router_data.add_session_token(state, &connector).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,
|
|
);
|
|
|
|
(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.push_sessions_token(session_token);
|
|
};
|
|
|
|
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.get_payment_attempt(),
|
|
validate_result.requeue,
|
|
schedule_time,
|
|
)
|
|
.await
|
|
.map_err(|error| logger::error!(process_tracker_error=?error))
|
|
.ok();
|
|
}
|
|
|
|
let updated_customer = None;
|
|
let frm_suggestion = None;
|
|
|
|
(_, *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.clone(),
|
|
)
|
|
.await?;
|
|
|
|
let router_data = 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.get_payment_attempt().status;
|
|
router_data
|
|
.decide_flows(
|
|
state,
|
|
&connector,
|
|
call_connector_action,
|
|
connector_request,
|
|
business_profile,
|
|
header_payload.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()));
|
|
|
|
Ok((router_data, merchant_connector_account))
|
|
}
|
|
|
|
pub async fn add_decrypted_payment_method_token<F, D>(
|
|
tokenization_action: TokenizationAction,
|
|
payment_data: &D,
|
|
) -> CustomResult<Option<PaymentMethodToken>, errors::ApiErrorResponse>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
D: OperationSessionGetters<F> + Send + Sync + Clone,
|
|
{
|
|
// Tokenization Action will be DecryptApplePayToken, only when payment method type is Apple Pay
|
|
// and the connector supports Apple Pay predecrypt
|
|
match &tokenization_action {
|
|
TokenizationAction::DecryptApplePayToken(payment_processing_details)
|
|
| TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt(
|
|
payment_processing_details,
|
|
) => {
|
|
let apple_pay_data = match payment_data.get_payment_method_data() {
|
|
Some(domain::PaymentMethodData::Wallet(domain::WalletData::ApplePay(
|
|
wallet_data,
|
|
))) => Some(
|
|
ApplePayData::token_json(domain::WalletData::ApplePay(wallet_data.clone()))
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed to parse apple pay token to json")?
|
|
.decrypt(
|
|
&payment_processing_details.payment_processing_certificate,
|
|
&payment_processing_details.payment_processing_certificate_key,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed to decrypt apple pay token")?,
|
|
),
|
|
_ => None,
|
|
};
|
|
|
|
let apple_pay_predecrypt = apple_pay_data
|
|
.parse_value::<hyperswitch_domain_models::router_data::ApplePayPredecryptData>(
|
|
"ApplePayPredecryptData",
|
|
)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(
|
|
"failed to parse decrypted apple pay response to ApplePayPredecryptData",
|
|
)?;
|
|
|
|
Ok(Some(PaymentMethodToken::ApplePayDecrypt(Box::new(
|
|
apple_pay_predecrypt,
|
|
))))
|
|
}
|
|
TokenizationAction::DecryptPazeToken(payment_processing_details) => {
|
|
let paze_data = match payment_data.get_payment_method_data() {
|
|
Some(domain::PaymentMethodData::Wallet(domain::WalletData::Paze(wallet_data))) => {
|
|
Some(
|
|
decrypt_paze_token(
|
|
wallet_data.clone(),
|
|
payment_processing_details.paze_private_key.clone(),
|
|
payment_processing_details
|
|
.paze_private_key_passphrase
|
|
.clone(),
|
|
)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed to decrypt paze token")?,
|
|
)
|
|
}
|
|
_ => None,
|
|
};
|
|
let paze_decrypted_data = paze_data
|
|
.parse_value::<hyperswitch_domain_models::router_data::PazeDecryptedData>(
|
|
"PazeDecryptedData",
|
|
)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed to parse PazeDecryptedData")?;
|
|
Ok(Some(PaymentMethodToken::PazeDecrypt(Box::new(
|
|
paze_decrypted_data,
|
|
))))
|
|
}
|
|
TokenizationAction::DecryptGooglePayToken(payment_processing_details) => {
|
|
let google_pay_data = match payment_data.get_payment_method_data() {
|
|
Some(domain::PaymentMethodData::Wallet(domain::WalletData::GooglePay(
|
|
wallet_data,
|
|
))) => {
|
|
let decryptor = helpers::GooglePayTokenDecryptor::new(
|
|
payment_processing_details
|
|
.google_pay_root_signing_keys
|
|
.clone(),
|
|
payment_processing_details.google_pay_recipient_id.clone(),
|
|
payment_processing_details.google_pay_private_key.clone(),
|
|
)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed to create google pay token decryptor")?;
|
|
|
|
// should_verify_token is set to false to disable verification of token
|
|
Some(
|
|
decryptor
|
|
.decrypt_token(wallet_data.tokenization_data.token.clone(), false)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed to decrypt google pay token")?,
|
|
)
|
|
}
|
|
Some(payment_method_data) => {
|
|
logger::info!(
|
|
"Invalid payment_method_data found for Google Pay Decrypt Flow: {:?}",
|
|
payment_method_data.get_payment_method()
|
|
);
|
|
None
|
|
}
|
|
None => {
|
|
logger::info!("No payment_method_data found for Google Pay Decrypt Flow");
|
|
None
|
|
}
|
|
};
|
|
|
|
let google_pay_predecrypt = google_pay_data
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed to get GooglePayDecryptedData in response")?;
|
|
|
|
Ok(Some(PaymentMethodToken::GooglePayDecrypt(Box::new(
|
|
google_pay_predecrypt,
|
|
))))
|
|
}
|
|
TokenizationAction::ConnectorToken(_) => {
|
|
logger::info!("Invalid tokenization action found for decryption flow: ConnectorToken",);
|
|
Ok(None)
|
|
}
|
|
token_action => {
|
|
logger::info!(
|
|
"Invalid tokenization action found for decryption flow: {:?}",
|
|
token_action
|
|
);
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_merchant_bank_data_for_open_banking_connectors(
|
|
merchant_connector_account: &helpers::MerchantConnectorAccountType,
|
|
key_store: &domain::MerchantKeyStore,
|
|
connector: &api::ConnectorData,
|
|
state: &SessionState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
) -> RouterResult<Option<router_types::MerchantRecipientData>> {
|
|
let merchant_data = merchant_connector_account
|
|
.get_additional_merchant_data()
|
|
.get_required_value("additional_merchant_data")?
|
|
.into_inner()
|
|
.peek()
|
|
.clone();
|
|
|
|
let merchant_recipient_data = merchant_data
|
|
.parse_value::<router_types::AdditionalMerchantData>("AdditionalMerchantData")
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed to decode MerchantRecipientData")?;
|
|
|
|
let connector_name = enums::Connector::to_string(&connector.connector_name);
|
|
let locker_based_connector_list = state.conf.locker_based_open_banking_connectors.clone();
|
|
let contains = locker_based_connector_list
|
|
.connector_list
|
|
.contains(connector_name.as_str());
|
|
|
|
let recipient_id = helpers::get_recipient_id_for_open_banking(&merchant_recipient_data)?;
|
|
let final_recipient_data = if let Some(id) = recipient_id {
|
|
if contains {
|
|
// Customer Id for OpenBanking connectors will be merchant_id as the account data stored at locker belongs to the merchant
|
|
let merchant_id_str = merchant_account.get_id().get_string_repr().to_owned();
|
|
let cust_id = id_type::CustomerId::try_from(std::borrow::Cow::from(merchant_id_str))
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to convert to CustomerId")?;
|
|
let locker_resp = cards::get_payment_method_from_hs_locker(
|
|
state,
|
|
key_store,
|
|
&cust_id,
|
|
merchant_account.get_id(),
|
|
id.as_str(),
|
|
Some(enums::LockerChoice::HyperswitchCardVault),
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Merchant bank account data could not be fetched from locker")?;
|
|
|
|
let parsed: router_types::MerchantAccountData = locker_resp
|
|
.peek()
|
|
.to_string()
|
|
.parse_struct("MerchantAccountData")
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
|
|
|
Some(router_types::MerchantRecipientData::AccountData(parsed))
|
|
} else {
|
|
Some(router_types::MerchantRecipientData::ConnectorRecipientId(
|
|
Secret::new(id),
|
|
))
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
Ok(final_recipient_data)
|
|
}
|
|
|
|
async fn blocklist_guard<F, ApiRequest, D>(
|
|
state: &SessionState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
key_store: &domain::MerchantKeyStore,
|
|
operation: &BoxedOperation<'_, F, ApiRequest, D>,
|
|
payment_data: &mut D,
|
|
) -> CustomResult<bool, errors::ApiErrorResponse>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
let merchant_id = merchant_account.get_id();
|
|
let blocklist_enabled_key = merchant_id.get_blocklist_guard_key();
|
|
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, key_store, payment_data)
|
|
.await?)
|
|
} else {
|
|
Ok(false)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn call_multiple_connectors_service<F, Op, Req, D>(
|
|
state: &SessionState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
key_store: &domain::MerchantKeyStore,
|
|
connectors: Vec<api::SessionConnectorData>,
|
|
_operation: &Op,
|
|
mut payment_data: D,
|
|
customer: &Option<domain::Customer>,
|
|
_session_surcharge_details: Option<api::SessionSurchargeDetails>,
|
|
business_profile: &domain::Profile,
|
|
header_payload: HeaderPayload,
|
|
) -> RouterResult<D>
|
|
where
|
|
Op: Debug,
|
|
F: Send + Clone,
|
|
|
|
// To create connector flow specific interface data
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
D: ConstructFlowSpecificData<F, Req, router_types::PaymentsResponseData>,
|
|
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>,
|
|
{
|
|
let call_connectors_start_time = Instant::now();
|
|
let mut join_handlers = Vec::with_capacity(connectors.len());
|
|
for session_connector_data in connectors.iter() {
|
|
let merchant_connector_id = session_connector_data
|
|
.connector
|
|
.merchant_connector_id
|
|
.as_ref()
|
|
.get_required_value("merchant_connector_id")
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("connector id is not set")?;
|
|
// TODO: make this DB call parallel
|
|
let merchant_connector_account = state
|
|
.store
|
|
.find_merchant_connector_account_by_id(&state.into(), merchant_connector_id, key_store)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
|
|
id: merchant_connector_id.get_string_repr().to_owned(),
|
|
})?;
|
|
let connector_id = session_connector_data.connector.connector.id();
|
|
let router_data = payment_data
|
|
.construct_router_data(
|
|
state,
|
|
connector_id,
|
|
merchant_account,
|
|
key_store,
|
|
customer,
|
|
&merchant_connector_account,
|
|
None,
|
|
None,
|
|
)
|
|
.await?;
|
|
|
|
let res = router_data.decide_flows(
|
|
state,
|
|
&session_connector_data.connector,
|
|
CallConnectorAction::Trigger,
|
|
None,
|
|
business_profile,
|
|
header_payload.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.clone()
|
|
{
|
|
// 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.push_sessions_token(session_token);
|
|
}
|
|
}
|
|
if let Err(connector_error_response) = connector_response.response {
|
|
logger::error!(
|
|
"sessions_connector_error {} {:?}",
|
|
connector_name,
|
|
connector_error_response
|
|
);
|
|
}
|
|
}
|
|
Err(api_error) => {
|
|
logger::error!("sessions_api_error {} {:?}", connector_name, api_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)
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn call_multiple_connectors_service<F, Op, Req, D>(
|
|
state: &SessionState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
key_store: &domain::MerchantKeyStore,
|
|
connectors: Vec<api::SessionConnectorData>,
|
|
_operation: &Op,
|
|
mut payment_data: D,
|
|
customer: &Option<domain::Customer>,
|
|
session_surcharge_details: Option<api::SessionSurchargeDetails>,
|
|
business_profile: &domain::Profile,
|
|
header_payload: HeaderPayload,
|
|
) -> RouterResult<D>
|
|
where
|
|
Op: Debug,
|
|
F: Send + Clone,
|
|
|
|
// To create connector flow specific interface data
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
D: ConstructFlowSpecificData<F, Req, router_types::PaymentsResponseData>,
|
|
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>,
|
|
{
|
|
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,
|
|
&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.set_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,
|
|
None,
|
|
None,
|
|
)
|
|
.await?;
|
|
|
|
let res = router_data.decide_flows(
|
|
state,
|
|
&session_connector_data.connector,
|
|
CallConnectorAction::Trigger,
|
|
None,
|
|
business_profile,
|
|
header_payload.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.clone()
|
|
{
|
|
// 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.push_sessions_token(session_token);
|
|
}
|
|
}
|
|
if let Err(connector_error_response) = connector_response.response {
|
|
logger::error!(
|
|
"sessions_connector_error {} {:?}",
|
|
connector_name,
|
|
connector_error_response
|
|
);
|
|
}
|
|
}
|
|
Err(api_error) => {
|
|
logger::error!("sessions_api_error {} {:?}", connector_name, api_error);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If click_to_pay is enabled and authentication_product_ids is configured in profile, we need to attach click_to_pay block in the session response for invoking click_to_pay SDK
|
|
if business_profile.is_click_to_pay_enabled {
|
|
if let Some(value) = business_profile.authentication_product_ids.clone() {
|
|
let session_token = get_session_token_for_click_to_pay(
|
|
state,
|
|
merchant_account.get_id(),
|
|
key_store,
|
|
value,
|
|
payment_data.get_payment_intent(),
|
|
)
|
|
.await?;
|
|
payment_data.push_sessions_token(session_token);
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
pub async fn get_session_token_for_click_to_pay(
|
|
state: &SessionState,
|
|
merchant_id: &id_type::MerchantId,
|
|
key_store: &domain::MerchantKeyStore,
|
|
authentication_product_ids: common_types::payments::AuthenticationConnectorAccountMap,
|
|
payment_intent: &hyperswitch_domain_models::payments::PaymentIntent,
|
|
) -> RouterResult<api_models::payments::SessionToken> {
|
|
let click_to_pay_mca_id = authentication_product_ids
|
|
.get_click_to_pay_connector_account_id()
|
|
.change_context(errors::ApiErrorResponse::MissingRequiredField {
|
|
field_name: "authentication_product_ids",
|
|
})?;
|
|
let key_manager_state = &(state).into();
|
|
let merchant_connector_account = state
|
|
.store
|
|
.find_by_merchant_connector_account_merchant_id_merchant_connector_id(
|
|
key_manager_state,
|
|
merchant_id,
|
|
&click_to_pay_mca_id,
|
|
key_store,
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
|
|
id: click_to_pay_mca_id.get_string_repr().to_string(),
|
|
})?;
|
|
let click_to_pay_metadata: ClickToPayMetaData = merchant_connector_account
|
|
.metadata
|
|
.parse_value("ClickToPayMetaData")
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Error while parsing ClickToPayMetaData")?;
|
|
let transaction_currency = payment_intent
|
|
.currency
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("currency is not present in payment_data.payment_intent")?;
|
|
let required_amount_type = common_utils::types::StringMajorUnitForConnector;
|
|
let transaction_amount = required_amount_type
|
|
.convert(payment_intent.amount, transaction_currency)
|
|
.change_context(errors::ApiErrorResponse::AmountConversionFailed {
|
|
amount_type: "string major unit",
|
|
})?;
|
|
|
|
let customer_details_value = payment_intent
|
|
.customer_details
|
|
.clone()
|
|
.get_required_value("customer_details")?;
|
|
|
|
let customer_details: CustomerData = customer_details_value
|
|
.parse_value("CustomerData")
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Error while parsing customer data from payment intent")?;
|
|
|
|
validate_customer_details_for_click_to_pay(&customer_details)?;
|
|
|
|
Ok(api_models::payments::SessionToken::ClickToPay(Box::new(
|
|
api_models::payments::ClickToPaySessionResponse {
|
|
dpa_id: click_to_pay_metadata.dpa_id,
|
|
dpa_name: click_to_pay_metadata.dpa_name,
|
|
locale: click_to_pay_metadata.locale,
|
|
card_brands: click_to_pay_metadata.card_brands,
|
|
acquirer_bin: click_to_pay_metadata.acquirer_bin,
|
|
acquirer_merchant_id: click_to_pay_metadata.acquirer_merchant_id,
|
|
merchant_category_code: click_to_pay_metadata.merchant_category_code,
|
|
merchant_country_code: click_to_pay_metadata.merchant_country_code,
|
|
transaction_amount,
|
|
transaction_currency_code: transaction_currency,
|
|
phone_number: customer_details.phone.clone(),
|
|
email: customer_details.email.clone(),
|
|
phone_country_code: customer_details.phone_country_code.clone(),
|
|
},
|
|
)))
|
|
}
|
|
|
|
fn validate_customer_details_for_click_to_pay(customer_details: &CustomerData) -> RouterResult<()> {
|
|
match (
|
|
customer_details.phone.as_ref(),
|
|
customer_details.phone_country_code.as_ref(),
|
|
customer_details.email.as_ref()
|
|
) {
|
|
(None, None, Some(_)) => Ok(()),
|
|
(Some(_), Some(_), Some(_)) => Ok(()),
|
|
(Some(_), Some(_), None) => Ok(()),
|
|
(Some(_), None, Some(_)) => Ok(()),
|
|
(None, Some(_), None) => Err(errors::ApiErrorResponse::MissingRequiredField {
|
|
field_name: "phone",
|
|
})
|
|
.attach_printable("phone number is not present in payment_intent.customer_details"),
|
|
(Some(_), None, None) => Err(errors::ApiErrorResponse::MissingRequiredField {
|
|
field_name: "phone_country_code",
|
|
})
|
|
.attach_printable("phone_country_code is not present in payment_intent.customer_details"),
|
|
(_, _, _) => Err(errors::ApiErrorResponse::MissingRequiredFields {
|
|
field_names: vec!["phone", "phone_country_code", "email"],
|
|
})
|
|
.attach_printable("either of phone, phone_country_code or email is not present in payment_intent.customer_details"),
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
pub async fn call_create_connector_customer_if_required<F, Req, D>(
|
|
state: &SessionState,
|
|
customer: &Option<domain::Customer>,
|
|
merchant_account: &domain::MerchantAccount,
|
|
key_store: &domain::MerchantKeyStore,
|
|
merchant_connector_account: &helpers::MerchantConnectorAccountType,
|
|
payment_data: &mut D,
|
|
) -> RouterResult<Option<storage::CustomerUpdate>>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
Req: Send + Sync,
|
|
|
|
// To create connector flow specific interface data
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
D: ConstructFlowSpecificData<F, Req, router_types::PaymentsResponseData>,
|
|
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.get_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(),
|
|
)?;
|
|
|
|
#[cfg(feature = "v1")]
|
|
let label = {
|
|
let connector_label = core_utils::get_connector_label(
|
|
payment_data.get_payment_intent().business_country,
|
|
payment_data.get_payment_intent().business_label.as_ref(),
|
|
payment_data
|
|
.get_payment_attempt()
|
|
.business_sub_label
|
|
.as_ref(),
|
|
&connector_name,
|
|
);
|
|
|
|
if let Some(connector_label) = merchant_connector_account
|
|
.get_mca_id()
|
|
.map(|mca_id| mca_id.get_string_repr().to_string())
|
|
.or(connector_label)
|
|
{
|
|
connector_label
|
|
} else {
|
|
let profile_id = payment_data
|
|
.get_payment_intent()
|
|
.profile_id
|
|
.as_ref()
|
|
.get_required_value("profile_id")
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("profile_id is not set in payment_intent")?;
|
|
|
|
format!("{connector_name}_{}", profile_id.get_string_repr())
|
|
}
|
|
};
|
|
|
|
#[cfg(feature = "v2")]
|
|
let label = {
|
|
merchant_connector_account
|
|
.get_mca_id()
|
|
.get_required_value("merchant_connector_account_id")?
|
|
.get_string_repr()
|
|
.to_owned()
|
|
};
|
|
|
|
let (should_call_connector, existing_connector_customer_id) =
|
|
customers::should_call_connector_create_customer(
|
|
state, &connector, customer, &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,
|
|
None,
|
|
None,
|
|
)
|
|
.await?;
|
|
|
|
let connector_customer_id = router_data
|
|
.create_connector_customer(state, &connector)
|
|
.await?;
|
|
|
|
let customer_update = customers::update_connector_customer_in_customers(
|
|
&label,
|
|
customer.as_ref(),
|
|
&connector_customer_id,
|
|
)
|
|
.await;
|
|
|
|
payment_data.set_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.set_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, D>(
|
|
state: &SessionState,
|
|
connector: &api::ConnectorData,
|
|
payment_data: &D,
|
|
mut router_data: RouterData<F, Req, router_types::PaymentsResponseData>,
|
|
operation: &BoxedOperation<'_, F, Q, D>,
|
|
should_continue_payment: bool,
|
|
) -> RouterResult<(RouterData<F, Req, router_types::PaymentsResponseData>, bool)>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
D: OperationSessionGetters<F> + Send + Sync + Clone,
|
|
Req: Send + Sync,
|
|
RouterData<F, Req, router_types::PaymentsResponseData>: Feature<F, Req> + Send,
|
|
dyn api::Connector:
|
|
services::api::ConnectorIntegration<F, Req, router_types::PaymentsResponseData>,
|
|
{
|
|
if !is_operation_complete_authorize(&operation)
|
|
&& connector
|
|
.connector_name
|
|
.is_pre_processing_required_before_authorize()
|
|
{
|
|
router_data = router_data.preprocessing_steps(state, connector).await?;
|
|
return Ok((router_data, should_continue_payment));
|
|
}
|
|
//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.get_payment_method_data() {
|
|
Some(domain::PaymentMethodData::BankTransfer(data)) => match data.deref() {
|
|
domain::BankTransferData::AchBankTransfer { .. }
|
|
| domain::BankTransferData::MultibancoBankTransfer { .. }
|
|
if connector.connector_name == router_types::Connector::Stripe =>
|
|
{
|
|
if payment_data
|
|
.get_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(domain::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(domain::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
|
|
&& !matches!(
|
|
payment_data
|
|
.get_payment_attempt()
|
|
.external_three_ds_authentication_attempted,
|
|
Some(true)
|
|
)
|
|
{
|
|
router_data = router_data.preprocessing_steps(state, connector).await?;
|
|
|
|
(router_data, false)
|
|
} else if connector.connector_name == router_types::Connector::Cybersource
|
|
&& 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 {
|
|
ref redirection_data,
|
|
..
|
|
}) if redirection_data.is_none()
|
|
) && router_data.status
|
|
!= common_enums::AttemptStatus::AuthenticationFailed;
|
|
(router_data, should_continue)
|
|
} else if router_data.auth_type == common_enums::AuthenticationType::ThreeDs
|
|
&& ((connector.connector_name == router_types::Connector::Nexixpay
|
|
&& is_operation_complete_authorize(&operation))
|
|
|| ((connector.connector_name == router_types::Connector::Nuvei
|
|
|| connector.connector_name == router_types::Connector::Shift4)
|
|
&& !is_operation_complete_authorize(&operation)))
|
|
{
|
|
router_data = router_data.preprocessing_steps(state, connector).await?;
|
|
(router_data, should_continue_payment)
|
|
} else {
|
|
(router_data, should_continue_payment)
|
|
}
|
|
}
|
|
Some(domain::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(domain::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.get_payment_attempt().get_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)
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn complete_postprocessing_steps_if_required<F, Q, RouterDReq, D>(
|
|
state: &SessionState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
key_store: &domain::MerchantKeyStore,
|
|
customer: &Option<domain::Customer>,
|
|
merchant_conn_account: &helpers::MerchantConnectorAccountType,
|
|
connector: &api::ConnectorData,
|
|
payment_data: &mut D,
|
|
_operation: &BoxedOperation<'_, F, Q, D>,
|
|
header_payload: Option<HeaderPayload>,
|
|
) -> RouterResult<RouterData<F, RouterDReq, router_types::PaymentsResponseData>>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
RouterDReq: Send + Sync,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
|
|
RouterData<F, RouterDReq, router_types::PaymentsResponseData>: Feature<F, RouterDReq> + Send,
|
|
dyn api::Connector:
|
|
services::api::ConnectorIntegration<F, RouterDReq, router_types::PaymentsResponseData>,
|
|
D: ConstructFlowSpecificData<F, RouterDReq, router_types::PaymentsResponseData>,
|
|
{
|
|
let mut router_data = payment_data
|
|
.construct_router_data(
|
|
state,
|
|
connector.connector.id(),
|
|
merchant_account,
|
|
key_store,
|
|
customer,
|
|
merchant_conn_account,
|
|
None,
|
|
header_payload,
|
|
)
|
|
.await?;
|
|
|
|
match payment_data.get_payment_method_data() {
|
|
Some(domain::PaymentMethodData::OpenBanking(domain::OpenBankingData::OpenBankingPIS {
|
|
..
|
|
})) => {
|
|
if connector.connector_name == router_types::Connector::Plaid {
|
|
router_data = router_data.postprocessing_steps(state, connector).await?;
|
|
let token = if let Ok(ref res) = router_data.response {
|
|
match res {
|
|
router_types::PaymentsResponseData::PostProcessingResponse {
|
|
session_token,
|
|
} => session_token
|
|
.as_ref()
|
|
.map(|token| api::SessionToken::OpenBanking(token.clone())),
|
|
_ => None,
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
if let Some(t) = token {
|
|
payment_data.push_sessions_token(t);
|
|
}
|
|
|
|
Ok(router_data)
|
|
} else {
|
|
Ok(router_data)
|
|
}
|
|
}
|
|
_ => Ok(router_data),
|
|
}
|
|
}
|
|
|
|
pub fn is_preprocessing_required_for_wallets(connector_name: String) -> bool {
|
|
connector_name == *"trustpay" || connector_name == *"payme"
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[instrument(skip_all)]
|
|
pub async fn construct_profile_id_and_get_mca<'a, F, D>(
|
|
state: &'a SessionState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
payment_data: &D,
|
|
connector_name: &str,
|
|
merchant_connector_id: Option<&id_type::MerchantConnectorAccountId>,
|
|
key_store: &domain::MerchantKeyStore,
|
|
_should_validate: bool,
|
|
) -> RouterResult<helpers::MerchantConnectorAccountType>
|
|
where
|
|
F: Clone,
|
|
D: OperationSessionGetters<F> + Send + Sync + Clone,
|
|
{
|
|
let profile_id = payment_data
|
|
.get_payment_intent()
|
|
.profile_id
|
|
.as_ref()
|
|
.get_required_value("profile_id")
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("profile_id is not set in payment_intent")?
|
|
.clone();
|
|
|
|
#[cfg(feature = "v2")]
|
|
let profile_id = payment_data.get_payment_intent().profile_id.clone();
|
|
|
|
let merchant_connector_account = helpers::get_merchant_connector_account(
|
|
state,
|
|
merchant_account.get_id(),
|
|
payment_data.get_creds_identifier(),
|
|
key_store,
|
|
&profile_id,
|
|
connector_name,
|
|
merchant_connector_id,
|
|
)
|
|
.await?;
|
|
|
|
Ok(merchant_connector_account)
|
|
}
|
|
|
|
fn is_payment_method_tokenization_enabled_for_connector(
|
|
state: &SessionState,
|
|
connector_name: &str,
|
|
payment_method: storage::enums::PaymentMethod,
|
|
payment_method_type: Option<storage::enums::PaymentMethodType>,
|
|
apple_pay_flow: &Option<domain::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<domain::ApplePayFlow>,
|
|
apple_pay_pre_decrypt_flow_filter: Option<ApplePayPreDecryptFlow>,
|
|
) -> bool {
|
|
match (payment_method_type, apple_pay_flow) {
|
|
(
|
|
Some(storage::enums::PaymentMethodType::ApplePay),
|
|
Some(domain::ApplePayFlow::Simplified(_)),
|
|
) => !matches!(
|
|
apple_pay_pre_decrypt_flow_filter,
|
|
Some(ApplePayPreDecryptFlow::NetworkTokenization)
|
|
),
|
|
_ => true,
|
|
}
|
|
}
|
|
|
|
fn decide_apple_pay_flow(
|
|
state: &SessionState,
|
|
payment_method_type: Option<enums::PaymentMethodType>,
|
|
merchant_connector_account: Option<&helpers::MerchantConnectorAccountType>,
|
|
) -> Option<domain::ApplePayFlow> {
|
|
payment_method_type.and_then(|pmt| match pmt {
|
|
enums::PaymentMethodType::ApplePay => {
|
|
check_apple_pay_metadata(state, merchant_connector_account)
|
|
}
|
|
_ => None,
|
|
})
|
|
}
|
|
|
|
fn check_apple_pay_metadata(
|
|
state: &SessionState,
|
|
merchant_connector_account: Option<&helpers::MerchantConnectorAccountType>,
|
|
) -> Option<domain::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 { .. } => {
|
|
domain::ApplePayFlow::Simplified(payments_api::PaymentProcessingDetails {
|
|
payment_processing_certificate: state
|
|
.conf
|
|
.applepay_decrypt_keys
|
|
.get_inner()
|
|
.apple_pay_ppc
|
|
.clone(),
|
|
payment_processing_certificate_key: state
|
|
.conf
|
|
.applepay_decrypt_keys
|
|
.get_inner()
|
|
.apple_pay_ppc_key
|
|
.clone(),
|
|
})
|
|
}
|
|
api_models::payments::ApplePayCombinedMetadata::Manual {
|
|
payment_request_data: _,
|
|
session_token_data,
|
|
} => {
|
|
if let Some(manual_payment_processing_details_at) =
|
|
session_token_data.payment_processing_details_at
|
|
{
|
|
match manual_payment_processing_details_at {
|
|
payments_api::PaymentProcessingDetailsAt::Hyperswitch(
|
|
payment_processing_details,
|
|
) => domain::ApplePayFlow::Simplified(payment_processing_details),
|
|
payments_api::PaymentProcessingDetailsAt::Connector => {
|
|
domain::ApplePayFlow::Manual
|
|
}
|
|
}
|
|
} else {
|
|
domain::ApplePayFlow::Manual
|
|
}
|
|
}
|
|
},
|
|
api_models::payments::ApplepaySessionTokenMetadata::ApplePay(_) => {
|
|
domain::ApplePayFlow::Manual
|
|
}
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
fn get_google_pay_connector_wallet_details(
|
|
state: &SessionState,
|
|
merchant_connector_account: &helpers::MerchantConnectorAccountType,
|
|
) -> Option<GooglePayPaymentProcessingDetails> {
|
|
let google_pay_root_signing_keys = state
|
|
.conf
|
|
.google_pay_decrypt_keys
|
|
.as_ref()
|
|
.map(|google_pay_keys| google_pay_keys.google_pay_root_signing_keys.clone());
|
|
match merchant_connector_account.get_connector_wallets_details() {
|
|
Some(wallet_details) => {
|
|
let google_pay_wallet_details = wallet_details
|
|
.parse_value::<api_models::payments::GooglePayWalletDetails>(
|
|
"GooglePayWalletDetails",
|
|
)
|
|
.map_err(|error| {
|
|
logger::warn!(?error, "Failed to Parse Value to GooglePayWalletDetails")
|
|
});
|
|
|
|
google_pay_wallet_details
|
|
.ok()
|
|
.and_then(
|
|
|google_pay_wallet_details| {
|
|
match google_pay_wallet_details
|
|
.google_pay
|
|
.provider_details {
|
|
api_models::payments::GooglePayProviderDetails::GooglePayMerchantDetails(merchant_details) => {
|
|
match (
|
|
merchant_details
|
|
.merchant_info
|
|
.tokenization_specification
|
|
.parameters
|
|
.private_key,
|
|
google_pay_root_signing_keys,
|
|
merchant_details
|
|
.merchant_info
|
|
.tokenization_specification
|
|
.parameters
|
|
.recipient_id,
|
|
) {
|
|
(Some(google_pay_private_key), Some(google_pay_root_signing_keys), Some(google_pay_recipient_id)) => {
|
|
Some(GooglePayPaymentProcessingDetails {
|
|
google_pay_private_key,
|
|
google_pay_root_signing_keys,
|
|
google_pay_recipient_id
|
|
})
|
|
}
|
|
_ => {
|
|
logger::warn!("One or more of the following fields are missing in GooglePayMerchantDetails: google_pay_private_key, google_pay_root_signing_keys, google_pay_recipient_id");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn decide_payment_method_tokenize_action(
|
|
state: &SessionState,
|
|
connector_name: &str,
|
|
payment_method: storage::enums::PaymentMethod,
|
|
pm_parent_token: Option<&str>,
|
|
is_connector_tokenization_enabled: bool,
|
|
apple_pay_flow: Option<domain::ApplePayFlow>,
|
|
payment_method_type: Option<storage_enums::PaymentMethodType>,
|
|
merchant_connector_account: &helpers::MerchantConnectorAccountType,
|
|
) -> RouterResult<TokenizationAction> {
|
|
if let Some(storage_enums::PaymentMethodType::Paze) = payment_method_type {
|
|
// Paze generates a one time use network token which should not be tokenized in the connector or router.
|
|
match &state.conf.paze_decrypt_keys {
|
|
Some(paze_keys) => Ok(TokenizationAction::DecryptPazeToken(
|
|
PazePaymentProcessingDetails {
|
|
paze_private_key: paze_keys.get_inner().paze_private_key.clone(),
|
|
paze_private_key_passphrase: paze_keys
|
|
.get_inner()
|
|
.paze_private_key_passphrase
|
|
.clone(),
|
|
},
|
|
)),
|
|
None => Err(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to fetch Paze configs"),
|
|
}
|
|
} else if let Some(storage_enums::PaymentMethodType::GooglePay) = payment_method_type {
|
|
let google_pay_details =
|
|
get_google_pay_connector_wallet_details(state, merchant_connector_account);
|
|
|
|
match google_pay_details {
|
|
Some(wallet_details) => Ok(TokenizationAction::DecryptGooglePayToken(wallet_details)),
|
|
None => {
|
|
if is_connector_tokenization_enabled {
|
|
Ok(TokenizationAction::TokenizeInConnectorAndRouter)
|
|
} else {
|
|
Ok(TokenizationAction::TokenizeInRouter)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
match pm_parent_token {
|
|
None => Ok(match (is_connector_tokenization_enabled, apple_pay_flow) {
|
|
(true, Some(domain::ApplePayFlow::Simplified(payment_processing_details))) => {
|
|
TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt(
|
|
payment_processing_details,
|
|
)
|
|
}
|
|
(true, _) => TokenizationAction::TokenizeInConnectorAndRouter,
|
|
(false, Some(domain::ApplePayFlow::Simplified(payment_processing_details))) => {
|
|
TokenizationAction::DecryptApplePayToken(payment_processing_details)
|
|
}
|
|
(false, _) => 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.into())
|
|
.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 => Ok(match (is_connector_tokenization_enabled, apple_pay_flow) {
|
|
(
|
|
true,
|
|
Some(domain::ApplePayFlow::Simplified(payment_processing_details)),
|
|
) => TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt(
|
|
payment_processing_details,
|
|
),
|
|
(true, _) => TokenizationAction::TokenizeInConnectorAndRouter,
|
|
(
|
|
false,
|
|
Some(domain::ApplePayFlow::Simplified(payment_processing_details)),
|
|
) => TokenizationAction::DecryptApplePayToken(payment_processing_details),
|
|
(false, _) => TokenizationAction::TokenizeInRouter,
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
|
pub struct PazePaymentProcessingDetails {
|
|
pub paze_private_key: Secret<String>,
|
|
pub paze_private_key_passphrase: Secret<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
pub struct GooglePayPaymentProcessingDetails {
|
|
pub google_pay_private_key: Secret<String>,
|
|
pub google_pay_root_signing_keys: Secret<String>,
|
|
pub google_pay_recipient_id: Secret<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum TokenizationAction {
|
|
TokenizeInRouter,
|
|
TokenizeInConnector,
|
|
TokenizeInConnectorAndRouter,
|
|
ConnectorToken(String),
|
|
SkipConnectorTokenization,
|
|
DecryptApplePayToken(payments_api::PaymentProcessingDetails),
|
|
TokenizeInConnectorAndApplepayPreDecrypt(payments_api::PaymentProcessingDetails),
|
|
DecryptPazeToken(PazePaymentProcessingDetails),
|
|
DecryptGooglePayToken(GooglePayPaymentProcessingDetails),
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn get_connector_tokenization_action_when_confirm_true<F, Req, D>(
|
|
_state: &SessionState,
|
|
_operation: &BoxedOperation<'_, F, Req, D>,
|
|
payment_data: &mut D,
|
|
_validate_result: &operations::ValidateResult,
|
|
_merchant_connector_account: &helpers::MerchantConnectorAccountType,
|
|
_merchant_key_store: &domain::MerchantKeyStore,
|
|
_customer: &Option<domain::Customer>,
|
|
_business_profile: &domain::Profile,
|
|
) -> RouterResult<(D, TokenizationAction)>
|
|
where
|
|
F: Send + Clone,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
// TODO: Implement this function
|
|
let payment_data = payment_data.to_owned();
|
|
Ok((payment_data, TokenizationAction::SkipConnectorTokenization))
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn get_connector_tokenization_action_when_confirm_true<F, Req, D>(
|
|
state: &SessionState,
|
|
operation: &BoxedOperation<'_, F, Req, D>,
|
|
payment_data: &mut D,
|
|
validate_result: &operations::ValidateResult,
|
|
merchant_connector_account: &helpers::MerchantConnectorAccountType,
|
|
merchant_key_store: &domain::MerchantKeyStore,
|
|
customer: &Option<domain::Customer>,
|
|
business_profile: &domain::Profile,
|
|
) -> RouterResult<(D, TokenizationAction)>
|
|
where
|
|
F: Send + Clone,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
let connector = payment_data.get_payment_attempt().connector.to_owned();
|
|
|
|
let is_mandate = payment_data
|
|
.get_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(_)
|
|
| api_models::payments::MandateReferenceId::NetworkTokenWithNTI(_) => 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
|
|
.get_payment_attempt()
|
|
.payment_method
|
|
.get_required_value("payment_method")?;
|
|
let payment_method_type = payment_data.get_payment_attempt().payment_method_type;
|
|
|
|
let apple_pay_flow =
|
|
decide_apple_pay_flow(state, 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.get_payment_attempt().connector.clone(),
|
|
payment_data.get_payment_attempt().merchant_id.clone(),
|
|
);
|
|
|
|
let payment_method_action = decide_payment_method_tokenize_action(
|
|
state,
|
|
&connector,
|
|
payment_method,
|
|
payment_data.get_token(),
|
|
is_connector_tokenization_enabled,
|
|
apple_pay_flow,
|
|
payment_method_type,
|
|
merchant_connector_account,
|
|
)
|
|
.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,
|
|
business_profile,
|
|
)
|
|
.await?;
|
|
payment_data.set_payment_method_data(payment_method_data);
|
|
payment_data.set_payment_method_id_in_attempt(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,
|
|
business_profile,
|
|
)
|
|
.await?;
|
|
|
|
payment_data.set_payment_method_data(payment_method_data);
|
|
payment_data.set_payment_method_id_in_attempt(pm_id);
|
|
TokenizationAction::TokenizeInConnector
|
|
}
|
|
TokenizationAction::ConnectorToken(token) => {
|
|
payment_data.set_pm_token(token);
|
|
TokenizationAction::SkipConnectorTokenization
|
|
}
|
|
TokenizationAction::SkipConnectorTokenization => {
|
|
TokenizationAction::SkipConnectorTokenization
|
|
}
|
|
TokenizationAction::DecryptApplePayToken(payment_processing_details) => {
|
|
TokenizationAction::DecryptApplePayToken(payment_processing_details)
|
|
}
|
|
TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt(
|
|
payment_processing_details,
|
|
) => TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt(
|
|
payment_processing_details,
|
|
),
|
|
TokenizationAction::DecryptPazeToken(paze_payment_processing_details) => {
|
|
TokenizationAction::DecryptPazeToken(paze_payment_processing_details)
|
|
}
|
|
TokenizationAction::DecryptGooglePayToken(
|
|
google_pay_payment_processing_details,
|
|
) => {
|
|
TokenizationAction::DecryptGooglePayToken(google_pay_payment_processing_details)
|
|
}
|
|
};
|
|
(payment_data.to_owned(), connector_tokenization_action)
|
|
}
|
|
_ => (
|
|
payment_data.to_owned(),
|
|
TokenizationAction::SkipConnectorTokenization,
|
|
),
|
|
};
|
|
|
|
Ok(payment_data_and_tokenization_action)
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
pub async fn tokenize_in_router_when_confirm_false_or_external_authentication<F, Req, D>(
|
|
state: &SessionState,
|
|
operation: &BoxedOperation<'_, F, Req, D>,
|
|
payment_data: &mut D,
|
|
validate_result: &operations::ValidateResult,
|
|
merchant_key_store: &domain::MerchantKeyStore,
|
|
customer: &Option<domain::Customer>,
|
|
business_profile: &domain::Profile,
|
|
) -> RouterResult<D>
|
|
where
|
|
F: Send + Clone,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
todo!()
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
pub async fn tokenize_in_router_when_confirm_false_or_external_authentication<F, Req, D>(
|
|
state: &SessionState,
|
|
operation: &BoxedOperation<'_, F, Req, D>,
|
|
payment_data: &mut D,
|
|
validate_result: &operations::ValidateResult,
|
|
merchant_key_store: &domain::MerchantKeyStore,
|
|
customer: &Option<domain::Customer>,
|
|
business_profile: &domain::Profile,
|
|
) -> RouterResult<D>
|
|
where
|
|
F: Send + Clone,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
// On confirm is false and only router related
|
|
let is_external_authentication_requested = payment_data
|
|
.get_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,
|
|
business_profile,
|
|
)
|
|
.await?;
|
|
payment_data.set_payment_method_data(payment_method_data);
|
|
if let Some(payment_method_id) = pm_id {
|
|
payment_data.set_payment_method_id_in_attempt(Some(payment_method_id));
|
|
}
|
|
payment_data
|
|
} else {
|
|
payment_data
|
|
};
|
|
Ok(payment_data.to_owned())
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct MandateConnectorDetails {
|
|
pub connector: String,
|
|
pub merchant_connector_id: Option<id_type::MerchantConnectorAccountId>,
|
|
}
|
|
|
|
#[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<domain::PaymentMethodData>,
|
|
pub payment_method_info: Option<domain::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<hyperswitch_domain_models::router_data::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 recurring_details: Option<RecurringDetails>,
|
|
pub poll_config: Option<router_types::PollConfig>,
|
|
pub tax_data: Option<TaxData>,
|
|
pub session_id: Option<String>,
|
|
pub service_details: Option<api_models::payments::CtpServiceDetails>,
|
|
}
|
|
|
|
#[derive(Clone, serde::Serialize, Debug)]
|
|
pub struct TaxData {
|
|
pub shipping_details: hyperswitch_domain_models::address::Address,
|
|
pub payment_method_type: enums::PaymentMethodType,
|
|
}
|
|
|
|
#[derive(Clone, serde::Serialize, Debug)]
|
|
pub struct PaymentEvent {
|
|
payment_intent: storage::PaymentIntent,
|
|
payment_attempt: storage::PaymentAttempt,
|
|
}
|
|
|
|
impl<F: Clone> PaymentData<F> {
|
|
// Get the method by which a card is discovered during a payment
|
|
#[cfg(feature = "v1")]
|
|
fn get_card_discovery_for_card_payment_method(&self) -> Option<common_enums::CardDiscovery> {
|
|
match self.payment_attempt.payment_method {
|
|
Some(storage_enums::PaymentMethod::Card) => {
|
|
if self
|
|
.token_data
|
|
.as_ref()
|
|
.map(storage::PaymentTokenData::is_permanent_card)
|
|
.unwrap_or(false)
|
|
{
|
|
Some(common_enums::CardDiscovery::SavedCard)
|
|
} else if self.service_details.is_some() {
|
|
Some(common_enums::CardDiscovery::ClickToPay)
|
|
} else {
|
|
Some(common_enums::CardDiscovery::Manual)
|
|
}
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
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: MinorUnit,
|
|
pub total_amount: MinorUnit,
|
|
pub reason: Option<String>,
|
|
pub authorization_id: Option<String>,
|
|
}
|
|
|
|
pub trait CustomerDetailsExt {
|
|
type Error;
|
|
fn get_name(&self) -> Result<Secret<String, masking::WithType>, Self::Error>;
|
|
fn get_email(&self) -> Result<pii::Email, Self::Error>;
|
|
}
|
|
|
|
impl CustomerDetailsExt for CustomerDetails {
|
|
type Error = error_stack::Report<errors::ConnectorError>;
|
|
fn get_name(&self) -> Result<Secret<String, masking::WithType>, Self::Error> {
|
|
self.name.clone().ok_or_else(missing_field_err("name"))
|
|
}
|
|
fn get_email(&self) -> Result<pii::Email, Self::Error> {
|
|
self.email.clone().ok_or_else(missing_field_err("email"))
|
|
}
|
|
}
|
|
|
|
pub async fn get_payment_link_response_from_id(
|
|
state: &SessionState,
|
|
payment_link_id: &str,
|
|
) -> CustomResult<api_models::payments::PaymentLinkResponse, errors::ApiErrorResponse> {
|
|
let db = &*state.store;
|
|
|
|
let payment_link_object = db
|
|
.find_payment_link_by_payment_link_id(payment_link_id)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?;
|
|
|
|
Ok(api_models::payments::PaymentLinkResponse {
|
|
link: payment_link_object.link_to_pay.clone(),
|
|
secure_link: payment_link_object.secure_link,
|
|
payment_link_id: payment_link_object.payment_link_id,
|
|
})
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
pub fn if_not_create_change_operation<'a, Op, F>(
|
|
status: storage_enums::IntentStatus,
|
|
confirm: Option<bool>,
|
|
current: &'a Op,
|
|
) -> BoxedOperation<'a, F, api::PaymentsRequest, PaymentData<F>>
|
|
where
|
|
F: Send + Clone + Sync,
|
|
Op: Operation<F, api::PaymentsRequest, Data = PaymentData<F>> + Send + Sync,
|
|
&'a Op: Operation<F, api::PaymentsRequest, Data = PaymentData<F>>,
|
|
PaymentStatus: Operation<F, api::PaymentsRequest, Data = PaymentData<F>>,
|
|
&'a PaymentStatus: Operation<F, api::PaymentsRequest, Data = PaymentData<F>>,
|
|
{
|
|
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),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
pub fn is_confirm<'a, F: Clone + Send, R, Op>(
|
|
operation: &'a Op,
|
|
confirm: Option<bool>,
|
|
) -> BoxedOperation<'a, F, R, PaymentData<F>>
|
|
where
|
|
PaymentConfirm: Operation<F, R, Data = PaymentData<F>>,
|
|
&'a PaymentConfirm: Operation<F, R, Data = PaymentData<F>>,
|
|
Op: Operation<F, R, Data = PaymentData<F>> + Send + Sync,
|
|
&'a Op: Operation<F, R, Data = PaymentData<F>>,
|
|
{
|
|
if confirm.unwrap_or(false) {
|
|
Box::new(&PaymentConfirm)
|
|
} else {
|
|
Box::new(operation)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
pub fn should_call_connector<Op: Debug, F: Clone, D>(operation: &Op, payment_data: &D) -> bool
|
|
where
|
|
D: OperationSessionGetters<F> + Send + Sync + Clone,
|
|
{
|
|
match format!("{operation:?}").as_str() {
|
|
"PaymentConfirm" => true,
|
|
"PaymentStart" => {
|
|
!matches!(
|
|
payment_data.get_payment_intent().status,
|
|
storage_enums::IntentStatus::Failed | storage_enums::IntentStatus::Succeeded
|
|
) && payment_data
|
|
.get_payment_attempt()
|
|
.authentication_data
|
|
.is_none()
|
|
}
|
|
"PaymentStatus" => {
|
|
matches!(
|
|
payment_data.get_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.get_force_sync().unwrap_or(false)
|
|
}
|
|
"PaymentCancel" => matches!(
|
|
payment_data.get_payment_intent().status,
|
|
storage_enums::IntentStatus::RequiresCapture
|
|
| storage_enums::IntentStatus::PartiallyCapturedAndCapturable
|
|
),
|
|
"PaymentCapture" => {
|
|
matches!(
|
|
payment_data.get_payment_intent().status,
|
|
storage_enums::IntentStatus::RequiresCapture
|
|
| storage_enums::IntentStatus::PartiallyCapturedAndCapturable
|
|
) || (matches!(
|
|
payment_data.get_payment_intent().status,
|
|
storage_enums::IntentStatus::Processing
|
|
) && matches!(
|
|
payment_data.get_capture_method(),
|
|
Some(storage_enums::CaptureMethod::ManualMultiple)
|
|
))
|
|
}
|
|
"CompleteAuthorize" => true,
|
|
"PaymentApprove" => true,
|
|
"PaymentReject" => true,
|
|
"PaymentSession" => true,
|
|
"PaymentSessionUpdate" => true,
|
|
"PaymentPostSessionTokens" => true,
|
|
"PaymentIncrementalAuthorization" => matches!(
|
|
payment_data.get_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(all(feature = "olap", feature = "v1"))]
|
|
pub async fn list_payments(
|
|
state: SessionState,
|
|
merchant: domain::MerchantAccount,
|
|
profile_id_list: Option<Vec<id_type::ProfileId>>,
|
|
key_store: domain::MerchantKeyStore,
|
|
constraints: api::PaymentListConstraints,
|
|
) -> RouterResponse<api::PaymentListResponse> {
|
|
use hyperswitch_domain_models::errors::StorageError;
|
|
helpers::validate_payment_list_request(&constraints)?;
|
|
let merchant_id = merchant.get_id();
|
|
let db = state.store.as_ref();
|
|
let payment_intents = helpers::filter_by_constraints(
|
|
&state,
|
|
&(constraints, profile_id_list).try_into()?,
|
|
merchant_id,
|
|
&key_store,
|
|
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(all(feature = "olap", feature = "v1"))]
|
|
pub async fn apply_filters_on_payments(
|
|
state: SessionState,
|
|
merchant: domain::MerchantAccount,
|
|
profile_id_list: Option<Vec<id_type::ProfileId>>,
|
|
merchant_key_store: domain::MerchantKeyStore,
|
|
constraints: api::PaymentListFilterConstraints,
|
|
) -> RouterResponse<api::PaymentListResponseV2> {
|
|
common_utils::metrics::utils::record_operation_time(
|
|
async {
|
|
let limit = &constraints.limit;
|
|
helpers::validate_payment_list_request_for_joins(*limit)?;
|
|
let db: &dyn StorageInterface = state.store.as_ref();
|
|
let pi_fetch_constraints = (constraints.clone(), profile_id_list.clone()).try_into()?;
|
|
let list: Vec<(storage::PaymentIntent, storage::PaymentAttempt)> = db
|
|
.get_filtered_payment_intents_attempt(
|
|
&(&state).into(),
|
|
merchant.get_id(),
|
|
&pi_fetch_constraints,
|
|
&merchant_key_store,
|
|
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.get_id(),
|
|
&pi_fetch_constraints,
|
|
merchant.storage_scheme,
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::InternalServerError)?;
|
|
|
|
let total_count = if constraints.has_no_attempt_filters() {
|
|
i64::try_from(active_attempt_ids.len())
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Error while converting from usize to i64")
|
|
} else {
|
|
db.get_total_count_of_filtered_payment_attempts(
|
|
merchant.get_id(),
|
|
&active_attempt_ids,
|
|
constraints.connector,
|
|
constraints.payment_method,
|
|
constraints.payment_method_type,
|
|
constraints.authentication_type,
|
|
constraints.merchant_connector_id,
|
|
constraints.card_network,
|
|
constraints.card_discovery,
|
|
merchant.storage_scheme,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
}?;
|
|
|
|
Ok(services::ApplicationResponse::Json(
|
|
api::PaymentListResponseV2 {
|
|
count: data.len(),
|
|
total_count,
|
|
data,
|
|
},
|
|
))
|
|
},
|
|
&metrics::PAYMENT_LIST_LATENCY,
|
|
router_env::metric_attributes!(("merchant_id", merchant.get_id().clone())),
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[cfg(all(feature = "olap", feature = "v1"))]
|
|
pub async fn get_filters_for_payments(
|
|
state: SessionState,
|
|
merchant: domain::MerchantAccount,
|
|
merchant_key_store: domain::MerchantKeyStore,
|
|
time_range: common_utils::types::TimeRange,
|
|
) -> RouterResponse<api::PaymentListFilters> {
|
|
let db = state.store.as_ref();
|
|
let pi = db
|
|
.filter_payment_intents_by_time_range_constraints(
|
|
&(&state).into(),
|
|
merchant.get_id(),
|
|
&time_range,
|
|
&merchant_key_store,
|
|
merchant.storage_scheme,
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
|
|
|
let filters = db
|
|
.get_filters_for_payments(
|
|
pi.as_slice(),
|
|
merchant.get_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(all(feature = "olap", feature = "v1"))]
|
|
pub async fn get_payment_filters(
|
|
state: SessionState,
|
|
merchant: domain::MerchantAccount,
|
|
profile_id_list: Option<Vec<id_type::ProfileId>>,
|
|
) -> RouterResponse<api::PaymentListFiltersV2> {
|
|
let merchant_connector_accounts = if let services::ApplicationResponse::Json(data) =
|
|
super::admin::list_payment_connectors(state, merchant.get_id().to_owned(), profile_id_list)
|
|
.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 = merchant_connector_account.to_merchant_connector_info(label);
|
|
(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(),
|
|
card_network: enums::CardNetwork::iter().collect(),
|
|
card_discovery: enums::CardDiscovery::iter().collect(),
|
|
},
|
|
))
|
|
}
|
|
|
|
#[cfg(all(feature = "olap", feature = "v1"))]
|
|
pub async fn get_aggregates_for_payments(
|
|
state: SessionState,
|
|
merchant: domain::MerchantAccount,
|
|
profile_id_list: Option<Vec<id_type::ProfileId>>,
|
|
time_range: common_utils::types::TimeRange,
|
|
) -> RouterResponse<api::PaymentsAggregateResponse> {
|
|
let db = state.store.as_ref();
|
|
let intent_status_with_count = db
|
|
.get_intent_status_with_count(merchant.get_id(), profile_id_list, &time_range)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
|
|
|
let mut status_map: HashMap<enums::IntentStatus, i64> =
|
|
intent_status_with_count.into_iter().collect();
|
|
for status in enums::IntentStatus::iter() {
|
|
status_map.entry(status).or_default();
|
|
}
|
|
|
|
Ok(services::ApplicationResponse::Json(
|
|
api::PaymentsAggregateResponse {
|
|
status_with_count: status_map,
|
|
},
|
|
))
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
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.get_id().to_owned()),
|
|
..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.get_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(())
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
pub async fn reset_process_sync_task(
|
|
db: &dyn StorageInterface,
|
|
payment_attempt: &storage::PaymentAttempt,
|
|
schedule_time: time::PrimitiveDateTime,
|
|
) -> Result<(), errors::ProcessTrackerError> {
|
|
todo!()
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
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.get_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(())
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
pub fn update_straight_through_routing<F, D>(
|
|
payment_data: &mut D,
|
|
request_straight_through: serde_json::Value,
|
|
) -> CustomResult<(), errors::ParsingError>
|
|
where
|
|
F: Send + Clone,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
let _: api_models::routing::RoutingAlgorithm = request_straight_through
|
|
.clone()
|
|
.parse_value("RoutingAlgorithm")
|
|
.attach_printable("Invalid straight through routing rules format")?;
|
|
|
|
payment_data.set_straight_through_algorithm_in_payment_attempt(request_straight_through);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn get_connector_choice<F, Req, D>(
|
|
operation: &BoxedOperation<'_, F, Req, D>,
|
|
state: &SessionState,
|
|
req: &Req,
|
|
merchant_account: &domain::MerchantAccount,
|
|
business_profile: &domain::Profile,
|
|
key_store: &domain::MerchantKeyStore,
|
|
payment_data: &mut D,
|
|
eligible_connectors: Option<Vec<enums::RoutableConnectors>>,
|
|
mandate_type: Option<api::MandateTransactionType>,
|
|
) -> RouterResult<Option<ConnectorCallType>>
|
|
where
|
|
F: Send + Clone,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
let connector_choice = operation
|
|
.to_domain()?
|
|
.get_connector(
|
|
merchant_account,
|
|
&state.clone(),
|
|
req,
|
|
payment_data.get_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?;
|
|
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)
|
|
}
|
|
|
|
async fn get_eligible_connector_for_nti<T: core_routing::GetRoutableConnectorsForChoice, F, D>(
|
|
state: &SessionState,
|
|
key_store: &domain::MerchantKeyStore,
|
|
payment_data: &D,
|
|
connector_choice: T,
|
|
|
|
business_profile: &domain::Profile,
|
|
) -> RouterResult<(
|
|
api_models::payments::MandateReferenceId,
|
|
hyperswitch_domain_models::payment_method_data::CardDetailsForNetworkTransactionId,
|
|
api::ConnectorData,
|
|
)>
|
|
where
|
|
F: Send + Clone,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
// Since this flow will only be used in the MIT flow, recurring details are mandatory.
|
|
let recurring_payment_details = payment_data
|
|
.get_recurring_details()
|
|
.ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
|
|
.attach_printable("Failed to fetch recurring details for mit")?;
|
|
|
|
let (mandate_reference_id, card_details_for_network_transaction_id)= hyperswitch_domain_models::payment_method_data::CardDetailsForNetworkTransactionId::get_nti_and_card_details_for_mit_flow(recurring_payment_details.clone()).get_required_value("network transaction id and card details").attach_printable("Failed to fetch network transaction id and card details for mit")?;
|
|
|
|
helpers::validate_card_expiry(
|
|
&card_details_for_network_transaction_id.card_exp_month,
|
|
&card_details_for_network_transaction_id.card_exp_year,
|
|
)?;
|
|
|
|
let network_transaction_id_supported_connectors = &state
|
|
.conf
|
|
.network_transaction_id_supported_connectors
|
|
.connector_list
|
|
.iter()
|
|
.map(|value| value.to_string())
|
|
.collect::<HashSet<_>>();
|
|
|
|
let eligible_connector_data_list = connector_choice
|
|
.get_routable_connectors(&*state.store, business_profile)
|
|
.await?
|
|
.filter_network_transaction_id_flow_supported_connectors(
|
|
network_transaction_id_supported_connectors.to_owned(),
|
|
)
|
|
.construct_dsl_and_perform_eligibility_analysis(
|
|
state,
|
|
key_store,
|
|
payment_data,
|
|
business_profile.get_id(),
|
|
)
|
|
.await
|
|
.attach_printable("Failed to fetch eligible connector data")?;
|
|
|
|
let eligible_connector_data = eligible_connector_data_list
|
|
.first()
|
|
.ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
|
|
.attach_printable(
|
|
"No eligible connector found for the network transaction id based mit flow",
|
|
)?;
|
|
Ok((
|
|
mandate_reference_id,
|
|
card_details_for_network_transaction_id,
|
|
eligible_connector_data.clone(),
|
|
))
|
|
}
|
|
|
|
pub async fn set_eligible_connector_for_nti_in_payment_data<F, D>(
|
|
state: &SessionState,
|
|
business_profile: &domain::Profile,
|
|
key_store: &domain::MerchantKeyStore,
|
|
payment_data: &mut D,
|
|
connector_choice: api::ConnectorChoice,
|
|
) -> RouterResult<api::ConnectorData>
|
|
where
|
|
F: Send + Clone,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
let (mandate_reference_id, card_details_for_network_transaction_id, eligible_connector_data) =
|
|
match connector_choice {
|
|
api::ConnectorChoice::StraightThrough(straight_through) => {
|
|
get_eligible_connector_for_nti(
|
|
state,
|
|
key_store,
|
|
payment_data,
|
|
core_routing::StraightThroughAlgorithmTypeSingle(straight_through),
|
|
business_profile,
|
|
)
|
|
.await?
|
|
}
|
|
api::ConnectorChoice::Decide => {
|
|
get_eligible_connector_for_nti(
|
|
state,
|
|
key_store,
|
|
payment_data,
|
|
core_routing::DecideConnector,
|
|
business_profile,
|
|
)
|
|
.await?
|
|
}
|
|
api::ConnectorChoice::SessionMultiple(_) => {
|
|
Err(errors::ApiErrorResponse::InternalServerError).attach_printable(
|
|
"Invalid routing rule configured for nti and card details based mit flow",
|
|
)?
|
|
}
|
|
};
|
|
|
|
// Set the eligible connector in the attempt
|
|
payment_data
|
|
.set_connector_in_payment_attempt(Some(eligible_connector_data.connector_name.to_string()));
|
|
|
|
// Set `NetworkMandateId` as the MandateId
|
|
payment_data.set_mandate_id(payments_api::MandateIds {
|
|
mandate_id: None,
|
|
mandate_reference_id: Some(mandate_reference_id),
|
|
});
|
|
|
|
// Set the card details received in the recurring details within the payment method data.
|
|
payment_data.set_payment_method_data(Some(
|
|
hyperswitch_domain_models::payment_method_data::PaymentMethodData::CardDetailsForNetworkTransactionId(card_details_for_network_transaction_id),
|
|
));
|
|
|
|
Ok(eligible_connector_data)
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn connector_selection<F, D>(
|
|
state: &SessionState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
business_profile: &domain::Profile,
|
|
key_store: &domain::MerchantKeyStore,
|
|
payment_data: &mut D,
|
|
request_straight_through: Option<serde_json::Value>,
|
|
eligible_connectors: Option<Vec<enums::RoutableConnectors>>,
|
|
mandate_type: Option<api::MandateTransactionType>,
|
|
) -> RouterResult<ConnectorCallType>
|
|
where
|
|
F: Send + Clone,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + 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.get_payment_attempt().connector.clone(),
|
|
|
|
merchant_connector_id: payment_data
|
|
.get_payment_attempt()
|
|
.merchant_connector_id
|
|
.clone(),
|
|
|
|
algorithm: request_straight_through.clone(),
|
|
routing_info: payment_data
|
|
.get_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(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.set_connector_in_payment_attempt(routing_data.routed_through);
|
|
|
|
payment_data.set_merchant_connector_id_in_attempt(routing_data.merchant_connector_id);
|
|
payment_data.set_straight_through_algorithm_in_payment_attempt(encoded_info);
|
|
|
|
Ok(decided_connector)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
#[cfg(feature = "v2")]
|
|
pub async fn decide_connector<F, D>(
|
|
state: SessionState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
business_profile: &domain::Profile,
|
|
key_store: &domain::MerchantKeyStore,
|
|
payment_data: &mut D,
|
|
request_straight_through: Option<api::routing::StraightThroughAlgorithm>,
|
|
routing_data: &mut storage::RoutingData,
|
|
eligible_connectors: Option<Vec<enums::RoutableConnectors>>,
|
|
mandate_type: Option<api::MandateTransactionType>,
|
|
) -> RouterResult<ConnectorCallType>
|
|
where
|
|
F: Send + Clone,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
todo!()
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
#[cfg(feature = "v1")]
|
|
pub async fn decide_connector<F, D>(
|
|
state: SessionState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
business_profile: &domain::Profile,
|
|
key_store: &domain::MerchantKeyStore,
|
|
payment_data: &mut D,
|
|
request_straight_through: Option<api::routing::StraightThroughAlgorithm>,
|
|
routing_data: &mut storage::RoutingData,
|
|
eligible_connectors: Option<Vec<enums::RoutableConnectors>>,
|
|
mandate_type: Option<api::MandateTransactionType>,
|
|
) -> RouterResult<ConnectorCallType>
|
|
where
|
|
F: Send + Clone,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + 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.get_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
|
|
.get_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(ConnectorCallType::PreDetermined(connector_data));
|
|
}
|
|
|
|
if let Some(mandate_connector_details) = payment_data.get_mandate_connector().as_ref() {
|
|
let connector_data = api::ConnectorData::get_connector_by_name(
|
|
&state.conf.connectors,
|
|
&mandate_connector_details.connector,
|
|
api::GetToken::Connector,
|
|
mandate_connector_details.merchant_connector_id.clone(),
|
|
)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Invalid connector name received in 'routed_through'")?;
|
|
|
|
routing_data.routed_through = Some(mandate_connector_details.connector.clone());
|
|
|
|
routing_data
|
|
.merchant_connector_id
|
|
.clone_from(&mandate_connector_details.merchant_connector_id);
|
|
|
|
return Ok(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
|
|
.get_payment_attempt()
|
|
.payment_method_type
|
|
.as_ref(),
|
|
)
|
|
{
|
|
if let (Some(routable_connector_choice), None) = (
|
|
pre_routing_results.get(storage_pm_type),
|
|
&payment_data.get_token_data(),
|
|
) {
|
|
let routable_connector_list = match routable_connector_choice {
|
|
storage::PreRoutingConnectorChoice::Single(routable_connector) => {
|
|
vec![routable_connector.clone()]
|
|
}
|
|
storage::PreRoutingConnectorChoice::Multiple(routable_connector_list) => {
|
|
routable_connector_list.clone()
|
|
}
|
|
};
|
|
|
|
let mut pre_routing_connector_data_list = vec![];
|
|
|
|
let first_routable_connector = routable_connector_list
|
|
.first()
|
|
.ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)?;
|
|
|
|
routing_data.routed_through = Some(first_routable_connector.connector.to_string());
|
|
|
|
routing_data
|
|
.merchant_connector_id
|
|
.clone_from(&first_routable_connector.merchant_connector_id);
|
|
|
|
for connector_choice in routable_connector_list.clone() {
|
|
let connector_data = api::ConnectorData::get_connector_by_name(
|
|
&state.conf.connectors,
|
|
&connector_choice.connector.to_string(),
|
|
api::GetToken::Connector,
|
|
connector_choice.merchant_connector_id.clone(),
|
|
)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Invalid connector name received")?;
|
|
|
|
pre_routing_connector_data_list.push(connector_data);
|
|
}
|
|
|
|
#[cfg(feature = "retry")]
|
|
let should_do_retry = retry::config_should_call_gsm(
|
|
&*state.store,
|
|
merchant_account.get_id(),
|
|
business_profile,
|
|
)
|
|
.await;
|
|
|
|
#[cfg(feature = "retry")]
|
|
if payment_data.get_payment_attempt().payment_method_type
|
|
== Some(storage_enums::PaymentMethodType::ApplePay)
|
|
&& should_do_retry
|
|
{
|
|
let retryable_connector_data = helpers::get_apple_pay_retryable_connectors(
|
|
&state,
|
|
merchant_account,
|
|
payment_data,
|
|
key_store,
|
|
&pre_routing_connector_data_list,
|
|
first_routable_connector
|
|
.merchant_connector_id
|
|
.clone()
|
|
.as_ref(),
|
|
business_profile.clone(),
|
|
)
|
|
.await?;
|
|
|
|
if let Some(connector_data_list) = retryable_connector_data {
|
|
if connector_data_list.len() > 1 {
|
|
logger::info!("Constructed apple pay retryable connector list");
|
|
return Ok(ConnectorCallType::Retryable(connector_data_list));
|
|
}
|
|
}
|
|
}
|
|
|
|
let first_pre_routing_connector_data_list = pre_routing_connector_data_list
|
|
.first()
|
|
.ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)?;
|
|
|
|
helpers::override_setup_future_usage_to_on_session(&*state.store, payment_data).await?;
|
|
|
|
return Ok(ConnectorCallType::PreDetermined(
|
|
first_pre_routing_connector_data_list.clone(),
|
|
));
|
|
}
|
|
}
|
|
|
|
if let Some(routing_algorithm) = request_straight_through {
|
|
let (mut connectors, check_eligibility) = routing::perform_straight_through_routing(
|
|
&routing_algorithm,
|
|
payment_data.get_creds_identifier(),
|
|
)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed execution of straight through routing")?;
|
|
|
|
if check_eligibility {
|
|
let transaction_data = core_routing::PaymentsDslInput::new(
|
|
payment_data.get_setup_mandate(),
|
|
payment_data.get_payment_attempt(),
|
|
payment_data.get_payment_intent(),
|
|
payment_data.get_payment_method_data(),
|
|
payment_data.get_address(),
|
|
payment_data.get_recurring_details(),
|
|
payment_data.get_currency(),
|
|
);
|
|
|
|
connectors = routing::perform_eligibility_analysis_with_fallback(
|
|
&state.clone(),
|
|
key_store,
|
|
connectors,
|
|
&TransactionData::Payment(transaction_data),
|
|
eligible_connectors,
|
|
business_profile,
|
|
)
|
|
.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,
|
|
conn.merchant_connector_id.clone(),
|
|
)
|
|
})
|
|
.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,
|
|
business_profile.is_connector_agnostic_mit_enabled,
|
|
business_profile.is_network_tokenization_enabled,
|
|
)
|
|
.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.get_creds_identifier(),
|
|
)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed execution of straight through routing")?;
|
|
|
|
if check_eligibility {
|
|
let transaction_data = core_routing::PaymentsDslInput::new(
|
|
payment_data.get_setup_mandate(),
|
|
payment_data.get_payment_attempt(),
|
|
payment_data.get_payment_intent(),
|
|
payment_data.get_payment_method_data(),
|
|
payment_data.get_address(),
|
|
payment_data.get_recurring_details(),
|
|
payment_data.get_currency(),
|
|
);
|
|
|
|
connectors = routing::perform_eligibility_analysis_with_fallback(
|
|
&state,
|
|
key_store,
|
|
connectors,
|
|
&TransactionData::Payment(transaction_data),
|
|
eligible_connectors,
|
|
business_profile,
|
|
)
|
|
.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,
|
|
conn.merchant_connector_id,
|
|
)
|
|
})
|
|
.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,
|
|
business_profile.is_connector_agnostic_mit_enabled,
|
|
business_profile.is_network_tokenization_enabled,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
let new_pd = payment_data.clone();
|
|
let transaction_data = core_routing::PaymentsDslInput::new(
|
|
new_pd.get_setup_mandate(),
|
|
new_pd.get_payment_attempt(),
|
|
new_pd.get_payment_intent(),
|
|
new_pd.get_payment_method_data(),
|
|
new_pd.get_address(),
|
|
new_pd.get_recurring_details(),
|
|
new_pd.get_currency(),
|
|
);
|
|
|
|
route_connector_v1_for_payments(
|
|
&state,
|
|
merchant_account,
|
|
business_profile,
|
|
key_store,
|
|
payment_data,
|
|
transaction_data,
|
|
routing_data,
|
|
eligible_connectors,
|
|
mandate_type,
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
pub async fn decide_multiplex_connector_for_normal_or_recurring_payment<F: Clone, D>(
|
|
state: &SessionState,
|
|
payment_data: &mut D,
|
|
routing_data: &mut storage::RoutingData,
|
|
connectors: Vec<api::ConnectorData>,
|
|
mandate_type: Option<api::MandateTransactionType>,
|
|
is_connector_agnostic_mit_enabled: Option<bool>,
|
|
) -> RouterResult<ConnectorCallType>
|
|
where
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
todo!()
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn decide_multiplex_connector_for_normal_or_recurring_payment<F: Clone, D>(
|
|
state: &SessionState,
|
|
payment_data: &mut D,
|
|
routing_data: &mut storage::RoutingData,
|
|
connectors: Vec<api::ConnectorData>,
|
|
mandate_type: Option<api::MandateTransactionType>,
|
|
is_connector_agnostic_mit_enabled: Option<bool>,
|
|
is_network_tokenization_enabled: bool,
|
|
) -> RouterResult<ConnectorCallType>
|
|
where
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
match (
|
|
payment_data.get_payment_intent().setup_future_usage,
|
|
payment_data.get_token_data().as_ref(),
|
|
payment_data.get_recurring_details().as_ref(),
|
|
payment_data.get_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
|
|
.get_payment_method_info()
|
|
.get_required_value("payment_method_info")?
|
|
.clone();
|
|
|
|
//fetch connectors that support ntid flow
|
|
let ntid_supported_connectors = &state
|
|
.conf
|
|
.network_transaction_id_supported_connectors
|
|
.connector_list;
|
|
//filered connectors list with ntid_supported_connectors
|
|
let filtered_ntid_supported_connectors =
|
|
filter_ntid_supported_connectors(connectors.clone(), ntid_supported_connectors);
|
|
|
|
//fetch connectors that support network tokenization flow
|
|
let network_tokenization_supported_connectors = &state
|
|
.conf
|
|
.network_tokenization_supported_connectors
|
|
.connector_list;
|
|
//filered connectors list with ntid_supported_connectors and network_tokenization_supported_connectors
|
|
let filtered_nt_supported_connectors = filter_network_tokenization_supported_connectors(
|
|
filtered_ntid_supported_connectors,
|
|
network_tokenization_supported_connectors,
|
|
);
|
|
|
|
let action_type = decide_action_type(
|
|
state,
|
|
is_connector_agnostic_mit_enabled,
|
|
is_network_tokenization_enabled,
|
|
&payment_method_info,
|
|
filtered_nt_supported_connectors.clone(),
|
|
)
|
|
.await;
|
|
|
|
match action_type {
|
|
Some(ActionType::NetworkTokenWithNetworkTransactionId(nt_data)) => {
|
|
logger::info!(
|
|
"using network_tokenization with network_transaction_id for MIT flow"
|
|
);
|
|
|
|
let mandate_reference_id =
|
|
Some(payments_api::MandateReferenceId::NetworkTokenWithNTI(
|
|
payments_api::NetworkTokenWithNTIRef {
|
|
network_transaction_id: nt_data.network_transaction_id.to_string(),
|
|
token_exp_month: nt_data.token_exp_month,
|
|
token_exp_year: nt_data.token_exp_year,
|
|
},
|
|
));
|
|
let chosen_connector_data = filtered_nt_supported_connectors
|
|
.first()
|
|
.ok_or(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());
|
|
|
|
routing_data
|
|
.merchant_connector_id
|
|
.clone_from(&chosen_connector_data.merchant_connector_id);
|
|
|
|
payment_data.set_mandate_id(payments_api::MandateIds {
|
|
mandate_id: None,
|
|
mandate_reference_id,
|
|
});
|
|
|
|
Ok(ConnectorCallType::PreDetermined(
|
|
chosen_connector_data.clone(),
|
|
))
|
|
}
|
|
None => {
|
|
decide_connector_for_normal_or_recurring_payment(
|
|
state,
|
|
payment_data,
|
|
routing_data,
|
|
connectors,
|
|
is_connector_agnostic_mit_enabled,
|
|
&payment_method_info,
|
|
)
|
|
.await
|
|
}
|
|
}
|
|
}
|
|
(
|
|
None,
|
|
None,
|
|
Some(RecurringDetails::ProcessorPaymentToken(_token)),
|
|
Some(true),
|
|
Some(api::MandateTransactionType::RecurringMandateTransaction),
|
|
) => {
|
|
if let Some(connector) = connectors.first() {
|
|
routing_data.routed_through = Some(connector.connector_name.clone().to_string());
|
|
routing_data
|
|
.merchant_connector_id
|
|
.clone_from(&connector.merchant_connector_id);
|
|
Ok(ConnectorCallType::PreDetermined(api::ConnectorData {
|
|
connector: connector.connector.clone(),
|
|
connector_name: connector.connector_name,
|
|
get_token: connector.get_token.clone(),
|
|
merchant_connector_id: connector.merchant_connector_id.clone(),
|
|
}))
|
|
} else {
|
|
logger::error!("no eligible connector found for the ppt_mandate payment");
|
|
Err(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration.into())
|
|
}
|
|
}
|
|
|
|
_ => {
|
|
helpers::override_setup_future_usage_to_on_session(&*state.store, payment_data).await?;
|
|
|
|
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());
|
|
|
|
routing_data.merchant_connector_id = first_choice.merchant_connector_id;
|
|
|
|
Ok(ConnectorCallType::Retryable(connectors))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn decide_connector_for_normal_or_recurring_payment<F: Clone, D>(
|
|
state: &SessionState,
|
|
payment_data: &mut D,
|
|
routing_data: &mut storage::RoutingData,
|
|
connectors: Vec<api::ConnectorData>,
|
|
is_connector_agnostic_mit_enabled: Option<bool>,
|
|
payment_method_info: &domain::PaymentMethod,
|
|
) -> RouterResult<ConnectorCallType>
|
|
where
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
todo!()
|
|
}
|
|
|
|
#[cfg(all(
|
|
any(feature = "v2", feature = "v1"),
|
|
not(feature = "payment_methods_v2")
|
|
))]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn decide_connector_for_normal_or_recurring_payment<F: Clone, D>(
|
|
state: &SessionState,
|
|
payment_data: &mut D,
|
|
routing_data: &mut storage::RoutingData,
|
|
connectors: Vec<api::ConnectorData>,
|
|
is_connector_agnostic_mit_enabled: Option<bool>,
|
|
payment_method_info: &domain::PaymentMethod,
|
|
) -> RouterResult<ConnectorCallType>
|
|
where
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
let connector_common_mandate_details = payment_method_info
|
|
.get_common_mandate_reference()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to get the common mandate reference")?;
|
|
|
|
let connector_mandate_details = connector_common_mandate_details.payments;
|
|
|
|
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)
|
|
.attach_printable("Failed to find the merchant connector id")?;
|
|
if connector_mandate_details
|
|
.clone()
|
|
.map(|connector_mandate_details| {
|
|
connector_mandate_details.contains_key(merchant_connector_id)
|
|
})
|
|
.unwrap_or(false)
|
|
{
|
|
logger::info!("using connector_mandate_id for MIT flow");
|
|
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.get_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(
|
|
api_models::payments::ConnectorMandateReferenceId::new(
|
|
Some(mandate_reference_record.connector_mandate_id.clone()), // connector_mandate_id
|
|
Some(payment_method_info.get_id().clone()), // payment_method_id
|
|
None, // update_history
|
|
mandate_reference_record.mandate_metadata.clone(), // mandate_metadata
|
|
mandate_reference_record.connector_mandate_request_reference_id.clone(), // connector_mandate_request_reference_id
|
|
)
|
|
));
|
|
payment_data.set_recurring_mandate_payment_data(
|
|
hyperswitch_domain_models::router_data::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,
|
|
mandate_metadata: mandate_reference_record
|
|
.mandate_metadata.clone()
|
|
});
|
|
connector_choice = Some((connector_data, mandate_reference_id.clone()));
|
|
break;
|
|
}
|
|
}
|
|
} else if is_network_transaction_id_flow(
|
|
state,
|
|
is_connector_agnostic_mit_enabled,
|
|
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)
|
|
.attach_printable("Failed to fetch the network transaction id")?;
|
|
|
|
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 {
|
|
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());
|
|
|
|
routing_data
|
|
.merchant_connector_id
|
|
.clone_from(&chosen_connector_data.merchant_connector_id);
|
|
|
|
payment_data.set_mandate_id(payments_api::MandateIds {
|
|
mandate_id: None,
|
|
mandate_reference_id,
|
|
});
|
|
|
|
Ok(ConnectorCallType::PreDetermined(chosen_connector_data))
|
|
}
|
|
|
|
pub fn filter_ntid_supported_connectors(
|
|
connectors: Vec<api::ConnectorData>,
|
|
ntid_supported_connectors: &HashSet<enums::Connector>,
|
|
) -> Vec<api::ConnectorData> {
|
|
connectors
|
|
.into_iter()
|
|
.filter(|data| ntid_supported_connectors.contains(&data.connector_name))
|
|
.collect()
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)]
|
|
pub struct NetworkTokenExpiry {
|
|
pub token_exp_month: Option<Secret<String>>,
|
|
pub token_exp_year: Option<Secret<String>>,
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)]
|
|
pub struct NTWithNTIRef {
|
|
pub network_transaction_id: String,
|
|
pub token_exp_month: Option<Secret<String>>,
|
|
pub token_exp_year: Option<Secret<String>>,
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)]
|
|
pub enum ActionType {
|
|
NetworkTokenWithNetworkTransactionId(NTWithNTIRef),
|
|
}
|
|
|
|
pub fn filter_network_tokenization_supported_connectors(
|
|
connectors: Vec<api::ConnectorData>,
|
|
network_tokenization_supported_connectors: &HashSet<enums::Connector>,
|
|
) -> Vec<api::ConnectorData> {
|
|
connectors
|
|
.into_iter()
|
|
.filter(|data| network_tokenization_supported_connectors.contains(&data.connector_name))
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
pub async fn decide_action_type(
|
|
state: &SessionState,
|
|
is_connector_agnostic_mit_enabled: Option<bool>,
|
|
is_network_tokenization_enabled: bool,
|
|
payment_method_info: &domain::PaymentMethod,
|
|
filtered_nt_supported_connectors: Vec<api::ConnectorData>, //network tokenization supported connectors
|
|
) -> Option<ActionType> {
|
|
match (
|
|
is_network_token_with_network_transaction_id_flow(
|
|
is_connector_agnostic_mit_enabled,
|
|
is_network_tokenization_enabled,
|
|
payment_method_info,
|
|
),
|
|
!filtered_nt_supported_connectors.is_empty(),
|
|
) {
|
|
(IsNtWithNtiFlow::NtWithNtiSupported(network_transaction_id), true) => {
|
|
if let Ok((token_exp_month, token_exp_year)) =
|
|
network_tokenization::do_status_check_for_network_token(state, payment_method_info)
|
|
.await
|
|
{
|
|
Some(ActionType::NetworkTokenWithNetworkTransactionId(
|
|
NTWithNTIRef {
|
|
token_exp_month,
|
|
token_exp_year,
|
|
network_transaction_id,
|
|
},
|
|
))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
(IsNtWithNtiFlow::NtWithNtiSupported(_), false)
|
|
| (IsNtWithNtiFlow::NTWithNTINotSupported, _) => None,
|
|
}
|
|
}
|
|
|
|
pub fn is_network_transaction_id_flow(
|
|
state: &SessionState,
|
|
is_connector_agnostic_mit_enabled: Option<bool>,
|
|
connector: enums::Connector,
|
|
payment_method_info: &domain::PaymentMethod,
|
|
) -> bool {
|
|
let ntid_supported_connectors = &state
|
|
.conf
|
|
.network_transaction_id_supported_connectors
|
|
.connector_list;
|
|
|
|
is_connector_agnostic_mit_enabled == Some(true)
|
|
&& payment_method_info.get_payment_method_type() == Some(storage_enums::PaymentMethod::Card)
|
|
&& ntid_supported_connectors.contains(&connector)
|
|
&& payment_method_info.network_transaction_id.is_some()
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)]
|
|
pub enum IsNtWithNtiFlow {
|
|
NtWithNtiSupported(String), //Network token with Network transaction id supported flow
|
|
NTWithNTINotSupported, //Network token with Network transaction id not supported
|
|
}
|
|
|
|
pub fn is_network_token_with_network_transaction_id_flow(
|
|
is_connector_agnostic_mit_enabled: Option<bool>,
|
|
is_network_tokenization_enabled: bool,
|
|
payment_method_info: &domain::PaymentMethod,
|
|
) -> IsNtWithNtiFlow {
|
|
match (
|
|
is_connector_agnostic_mit_enabled,
|
|
is_network_tokenization_enabled,
|
|
payment_method_info.get_payment_method_type(),
|
|
payment_method_info.network_transaction_id.clone(),
|
|
payment_method_info.network_token_locker_id.is_some(),
|
|
payment_method_info
|
|
.network_token_requestor_reference_id
|
|
.is_some(),
|
|
) {
|
|
(
|
|
Some(true),
|
|
true,
|
|
Some(storage_enums::PaymentMethod::Card),
|
|
Some(network_transaction_id),
|
|
true,
|
|
true,
|
|
) => IsNtWithNtiFlow::NtWithNtiSupported(network_transaction_id),
|
|
_ => IsNtWithNtiFlow::NTWithNTINotSupported,
|
|
}
|
|
}
|
|
|
|
pub fn should_add_task_to_process_tracker<F: Clone, D: OperationSessionGetters<F>>(
|
|
payment_data: &D,
|
|
) -> bool {
|
|
let connector = payment_data.get_payment_attempt().connector.as_deref();
|
|
|
|
!matches!(
|
|
(
|
|
payment_data.get_payment_attempt().get_payment_method(),
|
|
connector
|
|
),
|
|
(
|
|
Some(storage_enums::PaymentMethod::BankTransfer),
|
|
Some("stripe")
|
|
)
|
|
)
|
|
}
|
|
|
|
pub async fn perform_session_token_routing<F, D>(
|
|
state: SessionState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
key_store: &domain::MerchantKeyStore,
|
|
payment_data: &D,
|
|
connectors: Vec<api::SessionConnectorData>,
|
|
) -> RouterResult<Vec<api::SessionConnectorData>>
|
|
where
|
|
F: Clone,
|
|
D: OperationSessionGetters<F>,
|
|
{
|
|
// Commenting out this code as `list_payment_method_api` and `perform_session_token_routing`
|
|
// will happen in parallel the behaviour of the session call differ based on filters in
|
|
// list_payment_method_api
|
|
|
|
// let routing_info: Option<storage::PaymentRoutingInfo> = payment_data
|
|
// .get_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, pre_routing_choice) in pre_routing_results.into_iter() {
|
|
// let routable_connector_list = match pre_routing_choice {
|
|
// storage::PreRoutingConnectorChoice::Single(routable_connector) => {
|
|
// vec![routable_connector.clone()]
|
|
// }
|
|
// storage::PreRoutingConnectorChoice::Multiple(routable_connector_list) => {
|
|
// routable_connector_list.clone()
|
|
// }
|
|
// };
|
|
// for routable_connector in routable_connector_list {
|
|
// if let Some(session_connector_data) =
|
|
// payment_methods.remove(&(routable_connector.to_string(), routed_pm_type))
|
|
// {
|
|
// final_list.push(session_connector_data);
|
|
// break;
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// if !final_list.is_empty() {
|
|
// return Ok(final_list);
|
|
// }
|
|
// }
|
|
|
|
let routing_enabled_pms = 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
|
|
.get_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.get_payment_attempt(),
|
|
payment_intent: payment_data.get_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();
|
|
|
|
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) {
|
|
let routing_choice = choice
|
|
.first()
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)?;
|
|
if connector_data.connector.connector_name == routing_choice.connector.connector_name
|
|
&& connector_data.connector.merchant_connector_id
|
|
== routing_choice.connector.merchant_connector_id
|
|
{
|
|
final_list.push(connector_data);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(final_list)
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn route_connector_v1_for_payments<F, D>(
|
|
state: &SessionState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
business_profile: &domain::Profile,
|
|
key_store: &domain::MerchantKeyStore,
|
|
payment_data: &mut D,
|
|
transaction_data: core_routing::PaymentsDslInput<'_>,
|
|
routing_data: &mut storage::RoutingData,
|
|
eligible_connectors: Option<Vec<enums::RoutableConnectors>>,
|
|
mandate_type: Option<api::MandateTransactionType>,
|
|
) -> RouterResult<ConnectorCallType>
|
|
where
|
|
F: Send + Clone,
|
|
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
|
|
{
|
|
let routing_algorithm_id = {
|
|
let routing_algorithm = business_profile.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();
|
|
algorithm_ref.algorithm_id
|
|
};
|
|
|
|
let connectors = routing::perform_static_routing_v1(
|
|
state,
|
|
merchant_account.get_id(),
|
|
routing_algorithm_id.as_ref(),
|
|
business_profile,
|
|
&TransactionData::Payment(transaction_data.clone()),
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
|
|
|
let connectors = routing::perform_eligibility_analysis_with_fallback(
|
|
&state.clone(),
|
|
key_store,
|
|
connectors,
|
|
&TransactionData::Payment(transaction_data),
|
|
eligible_connectors,
|
|
business_profile,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed eligibility analysis and fallback")?;
|
|
|
|
// dynamic success based connector selection
|
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
|
let connectors = {
|
|
if let Some(algo) = business_profile.dynamic_routing_algorithm.clone() {
|
|
let dynamic_routing_config: api_models::routing::DynamicRoutingAlgorithmRef = algo
|
|
.parse_value("DynamicRoutingAlgorithmRef")
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("unable to deserialize DynamicRoutingAlgorithmRef from JSON")?;
|
|
let dynamic_split = api_models::routing::RoutingVolumeSplit {
|
|
routing_type: api_models::routing::RoutingType::Dynamic,
|
|
split: dynamic_routing_config
|
|
.dynamic_routing_volume_split
|
|
.unwrap_or_default(),
|
|
};
|
|
let static_split: api_models::routing::RoutingVolumeSplit =
|
|
api_models::routing::RoutingVolumeSplit {
|
|
routing_type: api_models::routing::RoutingType::Static,
|
|
split: crate::consts::DYNAMIC_ROUTING_MAX_VOLUME
|
|
- dynamic_routing_config
|
|
.dynamic_routing_volume_split
|
|
.unwrap_or_default(),
|
|
};
|
|
let volume_split_vec = vec![dynamic_split, static_split];
|
|
let routing_choice =
|
|
routing::perform_dynamic_routing_volume_split(volume_split_vec, None)
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed to perform volume split on routing type")?;
|
|
|
|
if routing_choice.routing_type.is_dynamic_routing() {
|
|
let dynamic_routing_config_params_interpolator =
|
|
routing_helpers::DynamicRoutingConfigParamsInterpolator::new(
|
|
payment_data.get_payment_attempt().payment_method,
|
|
payment_data.get_payment_attempt().payment_method_type,
|
|
payment_data.get_payment_attempt().authentication_type,
|
|
payment_data.get_payment_attempt().currency,
|
|
payment_data
|
|
.get_billing_address()
|
|
.and_then(|address| address.address)
|
|
.and_then(|address| address.country),
|
|
payment_data
|
|
.get_payment_attempt()
|
|
.payment_method_data
|
|
.as_ref()
|
|
.and_then(|data| data.as_object())
|
|
.and_then(|card| card.get("card"))
|
|
.and_then(|data| data.as_object())
|
|
.and_then(|card| card.get("card_network"))
|
|
.and_then(|network| network.as_str())
|
|
.map(|network| network.to_string()),
|
|
payment_data
|
|
.get_payment_attempt()
|
|
.payment_method_data
|
|
.as_ref()
|
|
.and_then(|data| data.as_object())
|
|
.and_then(|card| card.get("card"))
|
|
.and_then(|data| data.as_object())
|
|
.and_then(|card| card.get("card_isin"))
|
|
.and_then(|card_isin| card_isin.as_str())
|
|
.map(|card_isin| card_isin.to_string()),
|
|
);
|
|
|
|
routing::perform_dynamic_routing(
|
|
state,
|
|
connectors.clone(),
|
|
business_profile,
|
|
dynamic_routing_config_params_interpolator,
|
|
)
|
|
.await
|
|
.map_err(|e| logger::error!(dynamic_routing_error=?e))
|
|
.unwrap_or(connectors)
|
|
} else {
|
|
connectors
|
|
}
|
|
} else {
|
|
connectors
|
|
}
|
|
};
|
|
|
|
let connector_data = connectors
|
|
.into_iter()
|
|
.map(|conn| {
|
|
api::ConnectorData::get_connector_by_name(
|
|
&state.conf.connectors,
|
|
&conn.connector.to_string(),
|
|
api::GetToken::Connector,
|
|
conn.merchant_connector_id,
|
|
)
|
|
})
|
|
.collect::<CustomResult<Vec<_>, _>>()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Invalid connector name received")?;
|
|
|
|
decide_multiplex_connector_for_normal_or_recurring_payment(
|
|
state,
|
|
payment_data,
|
|
routing_data,
|
|
connector_data,
|
|
mandate_type,
|
|
business_profile.is_connector_agnostic_mit_enabled,
|
|
business_profile.is_network_tokenization_enabled,
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[cfg(feature = "payouts")]
|
|
#[cfg(feature = "v2")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn route_connector_v1_for_payouts(
|
|
state: &SessionState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
business_profile: &domain::Profile,
|
|
key_store: &domain::MerchantKeyStore,
|
|
transaction_data: &payouts::PayoutData,
|
|
routing_data: &mut storage::RoutingData,
|
|
eligible_connectors: Option<Vec<enums::RoutableConnectors>>,
|
|
) -> RouterResult<ConnectorCallType> {
|
|
todo!()
|
|
}
|
|
|
|
#[cfg(feature = "payouts")]
|
|
#[cfg(feature = "v1")]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn route_connector_v1_for_payouts(
|
|
state: &SessionState,
|
|
merchant_account: &domain::MerchantAccount,
|
|
business_profile: &domain::Profile,
|
|
key_store: &domain::MerchantKeyStore,
|
|
transaction_data: &payouts::PayoutData,
|
|
routing_data: &mut storage::RoutingData,
|
|
eligible_connectors: Option<Vec<enums::RoutableConnectors>>,
|
|
) -> RouterResult<ConnectorCallType> {
|
|
let routing_algorithm_id = {
|
|
let routing_algorithm = business_profile.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();
|
|
algorithm_ref.algorithm_id
|
|
};
|
|
|
|
let connectors = routing::perform_static_routing_v1(
|
|
state,
|
|
merchant_account.get_id(),
|
|
routing_algorithm_id.as_ref(),
|
|
business_profile,
|
|
&TransactionData::Payout(transaction_data),
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
|
let connectors = routing::perform_eligibility_analysis_with_fallback(
|
|
&state.clone(),
|
|
key_store,
|
|
connectors,
|
|
&TransactionData::Payout(transaction_data),
|
|
eligible_connectors,
|
|
business_profile,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("failed eligibility analysis and fallback")?;
|
|
|
|
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,
|
|
conn.merchant_connector_id,
|
|
)
|
|
})
|
|
.collect::<CustomResult<Vec<_>, _>>()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Invalid connector name received")?;
|
|
|
|
routing_data.routed_through = Some(first_connector_choice.connector.to_string());
|
|
|
|
routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id;
|
|
|
|
Ok(ConnectorCallType::Retryable(connector_data))
|
|
}
|
|
|
|
#[cfg(all(feature = "v2", feature = "customer_v2"))]
|
|
pub async fn payment_external_authentication(
|
|
_state: SessionState,
|
|
_merchant_account: domain::MerchantAccount,
|
|
_key_store: domain::MerchantKeyStore,
|
|
_req: api_models::payments::PaymentsExternalAuthenticationRequest,
|
|
) -> RouterResponse<api_models::payments::PaymentsExternalAuthenticationResponse> {
|
|
todo!()
|
|
}
|
|
|
|
#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))]
|
|
#[instrument(skip_all)]
|
|
pub async fn payment_external_authentication<F: Clone + Sync>(
|
|
state: SessionState,
|
|
merchant_account: domain::MerchantAccount,
|
|
key_store: domain::MerchantKeyStore,
|
|
req: api_models::payments::PaymentsExternalAuthenticationRequest,
|
|
) -> RouterResponse<api_models::payments::PaymentsExternalAuthenticationResponse> {
|
|
use super::unified_authentication_service::types::ExternalAuthentication;
|
|
use crate::core::unified_authentication_service::{
|
|
types::UnifiedAuthenticationService, utils::external_authentication_update_trackers,
|
|
};
|
|
|
|
let db = &*state.store;
|
|
let key_manager_state = &(&state).into();
|
|
|
|
let merchant_id = merchant_account.get_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(
|
|
key_manager_state,
|
|
&payment_id,
|
|
merchant_id,
|
|
&key_store,
|
|
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(
|
|
key_manager_state,
|
|
customer_id,
|
|
merchant_account.get_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();
|
|
let shipping_address = helpers::create_or_find_address_for_payment_by_request(
|
|
&state,
|
|
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(
|
|
&state,
|
|
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,
|
|
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 business_profile = state
|
|
.store
|
|
.find_business_profile_by_profile_id(key_manager_state, &key_store, profile_id)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::ProfileNotFound {
|
|
id: profile_id.get_string_repr().to_owned(),
|
|
})?;
|
|
|
|
let payment_method_details = helpers::get_payment_method_details_from_payment_token(
|
|
&state,
|
|
&payment_attempt,
|
|
&payment_intent,
|
|
&key_store,
|
|
storage_scheme,
|
|
&business_profile,
|
|
)
|
|
.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.base_url,
|
|
&payment_attempt.clone(),
|
|
payment_connector_name,
|
|
));
|
|
let mca_id_option = merchant_connector_account.get_mca_id(); // Bind temporary value
|
|
let merchant_connector_account_id_or_connector_name = mca_id_option
|
|
.as_ref()
|
|
.map(|mca_id| mca_id.get_string_repr())
|
|
.unwrap_or(&authentication_connector);
|
|
|
|
let webhook_url = helpers::create_webhook_url(
|
|
&state.base_url,
|
|
merchant_id,
|
|
merchant_connector_account_id_or_connector_name,
|
|
);
|
|
|
|
let authentication_details = business_profile
|
|
.authentication_connector_details
|
|
.clone()
|
|
.get_required_value("authentication_connector_details")
|
|
.attach_printable("authentication_connector_details not configured by the merchant")?;
|
|
|
|
let authentication_response =
|
|
if helpers::is_merchant_eligible_authentication_service(merchant_account.get_id(), &state)
|
|
.await?
|
|
{
|
|
let auth_response =
|
|
<ExternalAuthentication as UnifiedAuthenticationService<F>>::authentication(
|
|
&state,
|
|
&business_profile,
|
|
payment_method_details.1,
|
|
payment_method_details.0,
|
|
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,
|
|
Some(amount),
|
|
Some(currency),
|
|
authentication::MessageCategory::Payment,
|
|
req.device_channel,
|
|
authentication.clone(),
|
|
return_url,
|
|
req.sdk_information,
|
|
req.threeds_method_comp_ind,
|
|
optional_customer.and_then(|customer| customer.email.map(pii::Email::from)),
|
|
webhook_url,
|
|
authentication_details.three_ds_requestor_url.clone(),
|
|
&merchant_connector_account,
|
|
&authentication_connector,
|
|
)
|
|
.await?;
|
|
let authentication = external_authentication_update_trackers(
|
|
&state,
|
|
auth_response,
|
|
authentication.clone(),
|
|
None,
|
|
)
|
|
.await?;
|
|
authentication::AuthenticationResponse::try_from(authentication)?
|
|
} else {
|
|
Box::pin(authentication_core::perform_authentication(
|
|
&state,
|
|
business_profile.merchant_id,
|
|
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,
|
|
merchant_connector_account,
|
|
Some(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(pii::Email::from)),
|
|
webhook_url,
|
|
authentication_details.three_ds_requestor_url.clone(),
|
|
payment_intent.psd2_sca_exemption_type,
|
|
))
|
|
.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,
|
|
three_ds_requestor_url: authentication_details.three_ds_requestor_url,
|
|
},
|
|
))
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
#[cfg(feature = "v2")]
|
|
pub async fn payment_start_redirection(
|
|
state: SessionState,
|
|
merchant_account: domain::MerchantAccount,
|
|
key_store: domain::MerchantKeyStore,
|
|
req: api_models::payments::PaymentStartRedirectionRequest,
|
|
) -> RouterResponse<serde_json::Value> {
|
|
let db = &*state.store;
|
|
let key_manager_state = &(&state).into();
|
|
|
|
let storage_scheme = merchant_account.storage_scheme;
|
|
|
|
let payment_intent = db
|
|
.find_payment_intent_by_id(key_manager_state, &req.id, &key_store, storage_scheme)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
|
|
|
//TODO: send valid html error pages in this case, or atleast redirect to valid html error pages
|
|
utils::when(
|
|
payment_intent.status != storage_enums::IntentStatus::RequiresCustomerAction,
|
|
|| {
|
|
Err(errors::ApiErrorResponse::PaymentUnexpectedState {
|
|
current_flow: "PaymentStartRedirection".to_string(),
|
|
field_name: "status".to_string(),
|
|
current_value: payment_intent.status.to_string(),
|
|
states: ["requires_customer_action".to_string()].join(", "),
|
|
})
|
|
},
|
|
)?;
|
|
|
|
let payment_attempt = db
|
|
.find_payment_attempt_by_id(
|
|
key_manager_state,
|
|
&key_store,
|
|
payment_intent
|
|
.active_attempt_id
|
|
.as_ref()
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("missing active attempt in payment_intent")?,
|
|
storage_scheme,
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Error while fetching payment_attempt")?;
|
|
let redirection_data = payment_attempt
|
|
.redirection_data
|
|
.clone()
|
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("missing authentication_data in payment_attempt")?;
|
|
|
|
Ok(services::ApplicationResponse::Form(Box::new(
|
|
services::RedirectionFormData {
|
|
redirect_form: redirection_data,
|
|
payment_method_data: None,
|
|
amount: payment_attempt.amount_details.get_net_amount().to_string(),
|
|
currency: payment_intent.amount_details.currency.to_string(),
|
|
},
|
|
)))
|
|
}
|
|
|
|
#[instrument(skip_all)]
|
|
pub async fn get_extended_card_info(
|
|
state: SessionState,
|
|
merchant_id: id_type::MerchantId,
|
|
payment_id: id_type::PaymentId,
|
|
) -> RouterResponse<payments_api::ExtendedCardInfoResponse> {
|
|
let redis_conn = state
|
|
.store
|
|
.get_redis_conn()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to get redis connection")?;
|
|
|
|
let key = helpers::get_redis_key_for_extended_card_info(&merchant_id, &payment_id);
|
|
let payload = redis_conn
|
|
.get_key::<String>(&key.into())
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::ExtendedCardInfoNotFound)?;
|
|
|
|
Ok(services::ApplicationResponse::Json(
|
|
payments_api::ExtendedCardInfoResponse { payload },
|
|
))
|
|
}
|
|
|
|
#[cfg(all(feature = "olap", feature = "v1"))]
|
|
pub async fn payments_manual_update(
|
|
state: SessionState,
|
|
req: api_models::payments::PaymentsManualUpdateRequest,
|
|
) -> RouterResponse<api_models::payments::PaymentsManualUpdateResponse> {
|
|
let api_models::payments::PaymentsManualUpdateRequest {
|
|
payment_id,
|
|
attempt_id,
|
|
merchant_id,
|
|
attempt_status,
|
|
error_code,
|
|
error_message,
|
|
error_reason,
|
|
connector_transaction_id,
|
|
} = req;
|
|
let key_manager_state = &(&state).into();
|
|
let key_store = state
|
|
.store
|
|
.get_merchant_key_store_by_merchant_id(
|
|
key_manager_state,
|
|
&merchant_id,
|
|
&state.store.get_master_key().to_vec().into(),
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
|
|
.attach_printable("Error while fetching the key store by merchant_id")?;
|
|
let merchant_account = state
|
|
.store
|
|
.find_merchant_account_by_merchant_id(key_manager_state, &merchant_id, &key_store)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
|
|
.attach_printable("Error while fetching the merchant_account by merchant_id")?;
|
|
let payment_attempt = state
|
|
.store
|
|
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
|
|
&payment_id,
|
|
&merchant_id,
|
|
&attempt_id.clone(),
|
|
merchant_account.storage_scheme,
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)
|
|
.attach_printable(
|
|
"Error while fetching the payment_attempt by payment_id, merchant_id and attempt_id",
|
|
)?;
|
|
|
|
let payment_intent = state
|
|
.store
|
|
.find_payment_intent_by_payment_id_merchant_id(
|
|
key_manager_state,
|
|
&payment_id,
|
|
merchant_account.get_id(),
|
|
&key_store,
|
|
merchant_account.storage_scheme,
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)
|
|
.attach_printable("Error while fetching the payment_intent by payment_id, merchant_id")?;
|
|
|
|
let option_gsm = if let Some(((code, message), connector_name)) = error_code
|
|
.as_ref()
|
|
.zip(error_message.as_ref())
|
|
.zip(payment_attempt.connector.as_ref())
|
|
{
|
|
helpers::get_gsm_record(
|
|
&state,
|
|
Some(code.to_string()),
|
|
Some(message.to_string()),
|
|
connector_name.to_string(),
|
|
// We need to get the unified_code and unified_message of the Authorize flow
|
|
"Authorize".to_string(),
|
|
)
|
|
.await
|
|
} else {
|
|
None
|
|
};
|
|
// Update the payment_attempt
|
|
let attempt_update = storage::PaymentAttemptUpdate::ManualUpdate {
|
|
status: attempt_status,
|
|
error_code,
|
|
error_message,
|
|
error_reason,
|
|
updated_by: merchant_account.storage_scheme.to_string(),
|
|
unified_code: option_gsm.as_ref().and_then(|gsm| gsm.unified_code.clone()),
|
|
unified_message: option_gsm.and_then(|gsm| gsm.unified_message),
|
|
connector_transaction_id,
|
|
};
|
|
let updated_payment_attempt = state
|
|
.store
|
|
.update_payment_attempt_with_attempt_id(
|
|
payment_attempt.clone(),
|
|
attempt_update,
|
|
merchant_account.storage_scheme,
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)
|
|
.attach_printable("Error while updating the payment_attempt")?;
|
|
// If the payment_attempt is active attempt for an intent, update the intent status
|
|
if payment_intent.active_attempt.get_id() == payment_attempt.attempt_id {
|
|
let intent_status = enums::IntentStatus::foreign_from(updated_payment_attempt.status);
|
|
let payment_intent_update = storage::PaymentIntentUpdate::ManualUpdate {
|
|
status: Some(intent_status),
|
|
updated_by: merchant_account.storage_scheme.to_string(),
|
|
};
|
|
state
|
|
.store
|
|
.update_payment_intent(
|
|
key_manager_state,
|
|
payment_intent,
|
|
payment_intent_update,
|
|
&key_store,
|
|
merchant_account.storage_scheme,
|
|
)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)
|
|
.attach_printable("Error while updating payment_intent")?;
|
|
}
|
|
Ok(services::ApplicationResponse::Json(
|
|
api_models::payments::PaymentsManualUpdateResponse {
|
|
payment_id: updated_payment_attempt.payment_id,
|
|
attempt_id: updated_payment_attempt.attempt_id,
|
|
merchant_id: updated_payment_attempt.merchant_id,
|
|
attempt_status: updated_payment_attempt.status,
|
|
error_code: updated_payment_attempt.error_code,
|
|
error_message: updated_payment_attempt.error_message,
|
|
error_reason: updated_payment_attempt.error_reason,
|
|
connector_transaction_id: updated_payment_attempt.connector_transaction_id,
|
|
},
|
|
))
|
|
}
|
|
|
|
pub trait PaymentMethodChecker<F> {
|
|
fn should_update_in_post_update_tracker(&self) -> bool;
|
|
fn should_update_in_update_tracker(&self) -> bool;
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
impl<F: Clone> PaymentMethodChecker<F> for PaymentData<F> {
|
|
fn should_update_in_post_update_tracker(&self) -> bool {
|
|
let payment_method_type = self
|
|
.payment_intent
|
|
.tax_details
|
|
.as_ref()
|
|
.and_then(|tax_details| tax_details.payment_method_type.as_ref().map(|pmt| pmt.pmt));
|
|
|
|
matches!(
|
|
payment_method_type,
|
|
Some(storage_enums::PaymentMethodType::Paypal)
|
|
)
|
|
}
|
|
|
|
fn should_update_in_update_tracker(&self) -> bool {
|
|
let payment_method_type = self
|
|
.payment_intent
|
|
.tax_details
|
|
.as_ref()
|
|
.and_then(|tax_details| tax_details.payment_method_type.as_ref().map(|pmt| pmt.pmt));
|
|
|
|
matches!(
|
|
payment_method_type,
|
|
Some(storage_enums::PaymentMethodType::ApplePay)
|
|
| Some(storage_enums::PaymentMethodType::GooglePay)
|
|
)
|
|
}
|
|
}
|
|
|
|
pub trait OperationSessionGetters<F> {
|
|
fn get_payment_attempt(&self) -> &storage::PaymentAttempt;
|
|
fn get_payment_intent(&self) -> &storage::PaymentIntent;
|
|
fn get_payment_method_info(&self) -> Option<&domain::PaymentMethod>;
|
|
fn get_mandate_id(&self) -> Option<&payments_api::MandateIds>;
|
|
fn get_address(&self) -> &PaymentAddress;
|
|
fn get_creds_identifier(&self) -> Option<&str>;
|
|
fn get_token(&self) -> Option<&str>;
|
|
fn get_multiple_capture_data(&self) -> Option<&types::MultipleCaptureData>;
|
|
fn get_payment_link_data(&self) -> Option<api_models::payments::PaymentLinkResponse>;
|
|
fn get_ephemeral_key(&self) -> Option<ephemeral_key::EphemeralKey>;
|
|
fn get_setup_mandate(&self) -> Option<&MandateData>;
|
|
fn get_poll_config(&self) -> Option<router_types::PollConfig>;
|
|
fn get_authentication(&self) -> Option<&storage::Authentication>;
|
|
fn get_frm_message(&self) -> Option<FraudCheck>;
|
|
fn get_refunds(&self) -> Vec<storage::Refund>;
|
|
fn get_disputes(&self) -> Vec<storage::Dispute>;
|
|
fn get_authorizations(&self) -> Vec<diesel_models::authorization::Authorization>;
|
|
fn get_attempts(&self) -> Option<Vec<storage::PaymentAttempt>>;
|
|
fn get_recurring_details(&self) -> Option<&RecurringDetails>;
|
|
// TODO: this should be a mandatory field, should we throw an error instead of returning an Option?
|
|
fn get_payment_intent_profile_id(&self) -> Option<&id_type::ProfileId>;
|
|
fn get_currency(&self) -> storage_enums::Currency;
|
|
fn get_amount(&self) -> api::Amount;
|
|
fn get_payment_attempt_connector(&self) -> Option<&str>;
|
|
fn get_billing_address(&self) -> Option<hyperswitch_domain_models::address::Address>;
|
|
fn get_payment_method_data(&self) -> Option<&domain::PaymentMethodData>;
|
|
fn get_sessions_token(&self) -> Vec<api::SessionToken>;
|
|
fn get_token_data(&self) -> Option<&storage::PaymentTokenData>;
|
|
fn get_mandate_connector(&self) -> Option<&MandateConnectorDetails>;
|
|
fn get_force_sync(&self) -> Option<bool>;
|
|
fn get_capture_method(&self) -> Option<enums::CaptureMethod>;
|
|
|
|
#[cfg(feature = "v2")]
|
|
fn get_optional_payment_attempt(&self) -> Option<&storage::PaymentAttempt>;
|
|
}
|
|
|
|
pub trait OperationSessionSetters<F> {
|
|
// Setter functions for PaymentData
|
|
fn set_payment_intent(&mut self, payment_intent: storage::PaymentIntent);
|
|
fn set_payment_attempt(&mut self, payment_attempt: storage::PaymentAttempt);
|
|
fn set_payment_method_data(&mut self, payment_method_data: Option<domain::PaymentMethodData>);
|
|
fn set_email_if_not_present(&mut self, email: pii::Email);
|
|
fn set_payment_method_id_in_attempt(&mut self, payment_method_id: Option<String>);
|
|
fn set_pm_token(&mut self, token: String);
|
|
fn set_connector_customer_id(&mut self, customer_id: Option<String>);
|
|
fn push_sessions_token(&mut self, token: api::SessionToken);
|
|
fn set_surcharge_details(&mut self, surcharge_details: Option<types::SurchargeDetails>);
|
|
fn set_merchant_connector_id_in_attempt(
|
|
&mut self,
|
|
merchant_connector_id: Option<id_type::MerchantConnectorAccountId>,
|
|
);
|
|
#[cfg(feature = "v1")]
|
|
fn set_capture_method_in_attempt(&mut self, capture_method: enums::CaptureMethod);
|
|
fn set_frm_message(&mut self, frm_message: FraudCheck);
|
|
fn set_payment_intent_status(&mut self, status: storage_enums::IntentStatus);
|
|
fn set_authentication_type_in_attempt(
|
|
&mut self,
|
|
authentication_type: Option<enums::AuthenticationType>,
|
|
);
|
|
fn set_recurring_mandate_payment_data(
|
|
&mut self,
|
|
recurring_mandate_payment_data:
|
|
hyperswitch_domain_models::router_data::RecurringMandatePaymentData,
|
|
);
|
|
fn set_mandate_id(&mut self, mandate_id: api_models::payments::MandateIds);
|
|
fn set_setup_future_usage_in_payment_intent(
|
|
&mut self,
|
|
setup_future_usage: storage_enums::FutureUsage,
|
|
);
|
|
|
|
#[cfg(feature = "v1")]
|
|
fn set_straight_through_algorithm_in_payment_attempt(
|
|
&mut self,
|
|
straight_through_algorithm: serde_json::Value,
|
|
);
|
|
fn set_connector_in_payment_attempt(&mut self, connector: Option<String>);
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
impl<F: Clone> OperationSessionGetters<F> for PaymentData<F> {
|
|
fn get_payment_attempt(&self) -> &storage::PaymentAttempt {
|
|
&self.payment_attempt
|
|
}
|
|
|
|
fn get_payment_intent(&self) -> &storage::PaymentIntent {
|
|
&self.payment_intent
|
|
}
|
|
|
|
fn get_payment_method_info(&self) -> Option<&domain::PaymentMethod> {
|
|
self.payment_method_info.as_ref()
|
|
}
|
|
|
|
fn get_mandate_id(&self) -> Option<&payments_api::MandateIds> {
|
|
self.mandate_id.as_ref()
|
|
}
|
|
|
|
// what is this address find out and not required remove this
|
|
fn get_address(&self) -> &PaymentAddress {
|
|
&self.address
|
|
}
|
|
|
|
fn get_creds_identifier(&self) -> Option<&str> {
|
|
self.creds_identifier.as_deref()
|
|
}
|
|
|
|
fn get_token(&self) -> Option<&str> {
|
|
self.token.as_deref()
|
|
}
|
|
|
|
fn get_multiple_capture_data(&self) -> Option<&types::MultipleCaptureData> {
|
|
self.multiple_capture_data.as_ref()
|
|
}
|
|
|
|
fn get_payment_link_data(&self) -> Option<api_models::payments::PaymentLinkResponse> {
|
|
self.payment_link_data.clone()
|
|
}
|
|
|
|
fn get_ephemeral_key(&self) -> Option<ephemeral_key::EphemeralKey> {
|
|
self.ephemeral_key.clone()
|
|
}
|
|
|
|
fn get_setup_mandate(&self) -> Option<&MandateData> {
|
|
self.setup_mandate.as_ref()
|
|
}
|
|
|
|
fn get_poll_config(&self) -> Option<router_types::PollConfig> {
|
|
self.poll_config.clone()
|
|
}
|
|
|
|
fn get_authentication(&self) -> Option<&storage::Authentication> {
|
|
self.authentication.as_ref()
|
|
}
|
|
|
|
fn get_frm_message(&self) -> Option<FraudCheck> {
|
|
self.frm_message.clone()
|
|
}
|
|
|
|
fn get_refunds(&self) -> Vec<storage::Refund> {
|
|
self.refunds.clone()
|
|
}
|
|
|
|
fn get_disputes(&self) -> Vec<storage::Dispute> {
|
|
self.disputes.clone()
|
|
}
|
|
|
|
fn get_authorizations(&self) -> Vec<diesel_models::authorization::Authorization> {
|
|
self.authorizations.clone()
|
|
}
|
|
|
|
fn get_attempts(&self) -> Option<Vec<storage::PaymentAttempt>> {
|
|
self.attempts.clone()
|
|
}
|
|
|
|
fn get_recurring_details(&self) -> Option<&RecurringDetails> {
|
|
self.recurring_details.as_ref()
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
fn get_payment_intent_profile_id(&self) -> Option<&id_type::ProfileId> {
|
|
self.payment_intent.profile_id.as_ref()
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
fn get_payment_intent_profile_id(&self) -> Option<&id_type::ProfileId> {
|
|
Some(&self.payment_intent.profile_id)
|
|
}
|
|
|
|
fn get_currency(&self) -> storage_enums::Currency {
|
|
self.currency
|
|
}
|
|
|
|
fn get_amount(&self) -> api::Amount {
|
|
self.amount
|
|
}
|
|
|
|
fn get_payment_attempt_connector(&self) -> Option<&str> {
|
|
self.payment_attempt.connector.as_deref()
|
|
}
|
|
|
|
fn get_billing_address(&self) -> Option<hyperswitch_domain_models::address::Address> {
|
|
self.address.get_payment_method_billing().cloned()
|
|
}
|
|
|
|
fn get_payment_method_data(&self) -> Option<&domain::PaymentMethodData> {
|
|
self.payment_method_data.as_ref()
|
|
}
|
|
|
|
fn get_sessions_token(&self) -> Vec<api::SessionToken> {
|
|
self.sessions_token.clone()
|
|
}
|
|
|
|
fn get_token_data(&self) -> Option<&storage::PaymentTokenData> {
|
|
self.token_data.as_ref()
|
|
}
|
|
|
|
fn get_mandate_connector(&self) -> Option<&MandateConnectorDetails> {
|
|
self.mandate_connector.as_ref()
|
|
}
|
|
|
|
fn get_force_sync(&self) -> Option<bool> {
|
|
self.force_sync
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
fn get_capture_method(&self) -> Option<enums::CaptureMethod> {
|
|
self.payment_attempt.capture_method
|
|
}
|
|
|
|
// #[cfg(feature = "v2")]
|
|
// fn get_capture_method(&self) -> Option<enums::CaptureMethod> {
|
|
// Some(self.payment_intent.capture_method)
|
|
// }
|
|
|
|
// #[cfg(feature = "v2")]
|
|
// fn get_optional_payment_attempt(&self) -> Option<&storage::PaymentAttempt> {
|
|
// todo!();
|
|
// }
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
impl<F: Clone> OperationSessionSetters<F> for PaymentData<F> {
|
|
// Setters Implementation
|
|
fn set_payment_intent(&mut self, payment_intent: storage::PaymentIntent) {
|
|
self.payment_intent = payment_intent;
|
|
}
|
|
|
|
fn set_payment_attempt(&mut self, payment_attempt: storage::PaymentAttempt) {
|
|
self.payment_attempt = payment_attempt;
|
|
}
|
|
|
|
fn set_payment_method_data(&mut self, payment_method_data: Option<domain::PaymentMethodData>) {
|
|
self.payment_method_data = payment_method_data;
|
|
}
|
|
|
|
fn set_payment_method_id_in_attempt(&mut self, payment_method_id: Option<String>) {
|
|
self.payment_attempt.payment_method_id = payment_method_id;
|
|
}
|
|
|
|
fn set_email_if_not_present(&mut self, email: pii::Email) {
|
|
self.email = self.email.clone().or(Some(email));
|
|
}
|
|
|
|
fn set_pm_token(&mut self, token: String) {
|
|
self.pm_token = Some(token);
|
|
}
|
|
|
|
fn set_connector_customer_id(&mut self, customer_id: Option<String>) {
|
|
self.connector_customer_id = customer_id;
|
|
}
|
|
|
|
fn push_sessions_token(&mut self, token: api::SessionToken) {
|
|
self.sessions_token.push(token);
|
|
}
|
|
|
|
fn set_surcharge_details(&mut self, surcharge_details: Option<types::SurchargeDetails>) {
|
|
self.surcharge_details = surcharge_details;
|
|
}
|
|
|
|
fn set_merchant_connector_id_in_attempt(
|
|
&mut self,
|
|
merchant_connector_id: Option<id_type::MerchantConnectorAccountId>,
|
|
) {
|
|
self.payment_attempt.merchant_connector_id = merchant_connector_id;
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
fn set_capture_method_in_attempt(&mut self, capture_method: enums::CaptureMethod) {
|
|
self.payment_attempt.capture_method = Some(capture_method);
|
|
}
|
|
|
|
fn set_frm_message(&mut self, frm_message: FraudCheck) {
|
|
self.frm_message = Some(frm_message);
|
|
}
|
|
|
|
fn set_payment_intent_status(&mut self, status: storage_enums::IntentStatus) {
|
|
self.payment_intent.status = status;
|
|
}
|
|
|
|
fn set_authentication_type_in_attempt(
|
|
&mut self,
|
|
authentication_type: Option<enums::AuthenticationType>,
|
|
) {
|
|
self.payment_attempt.authentication_type = authentication_type;
|
|
}
|
|
|
|
fn set_recurring_mandate_payment_data(
|
|
&mut self,
|
|
recurring_mandate_payment_data:
|
|
hyperswitch_domain_models::router_data::RecurringMandatePaymentData,
|
|
) {
|
|
self.recurring_mandate_payment_data = Some(recurring_mandate_payment_data);
|
|
}
|
|
|
|
fn set_mandate_id(&mut self, mandate_id: api_models::payments::MandateIds) {
|
|
self.mandate_id = Some(mandate_id);
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
fn set_setup_future_usage_in_payment_intent(
|
|
&mut self,
|
|
setup_future_usage: storage_enums::FutureUsage,
|
|
) {
|
|
self.payment_intent.setup_future_usage = Some(setup_future_usage);
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
fn set_setup_future_usage_in_payment_intent(
|
|
&mut self,
|
|
setup_future_usage: storage_enums::FutureUsage,
|
|
) {
|
|
self.payment_intent.setup_future_usage = setup_future_usage;
|
|
}
|
|
|
|
#[cfg(feature = "v1")]
|
|
fn set_straight_through_algorithm_in_payment_attempt(
|
|
&mut self,
|
|
straight_through_algorithm: serde_json::Value,
|
|
) {
|
|
self.payment_attempt.straight_through_algorithm = Some(straight_through_algorithm);
|
|
}
|
|
|
|
fn set_connector_in_payment_attempt(&mut self, connector: Option<String>) {
|
|
self.payment_attempt.connector = connector;
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
impl<F: Clone> OperationSessionGetters<F> for PaymentIntentData<F> {
|
|
fn get_payment_attempt(&self) -> &storage::PaymentAttempt {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_intent(&self) -> &storage::PaymentIntent {
|
|
&self.payment_intent
|
|
}
|
|
|
|
fn get_payment_method_info(&self) -> Option<&domain::PaymentMethod> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_mandate_id(&self) -> Option<&payments_api::MandateIds> {
|
|
todo!()
|
|
}
|
|
|
|
// what is this address find out and not required remove this
|
|
fn get_address(&self) -> &PaymentAddress {
|
|
todo!()
|
|
}
|
|
|
|
fn get_creds_identifier(&self) -> Option<&str> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_token(&self) -> Option<&str> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_multiple_capture_data(&self) -> Option<&types::MultipleCaptureData> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_link_data(&self) -> Option<api_models::payments::PaymentLinkResponse> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_ephemeral_key(&self) -> Option<ephemeral_key::EphemeralKey> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_setup_mandate(&self) -> Option<&MandateData> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_poll_config(&self) -> Option<router_types::PollConfig> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_authentication(&self) -> Option<&storage::Authentication> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_frm_message(&self) -> Option<FraudCheck> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_refunds(&self) -> Vec<storage::Refund> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_disputes(&self) -> Vec<storage::Dispute> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_authorizations(&self) -> Vec<diesel_models::authorization::Authorization> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_attempts(&self) -> Option<Vec<storage::PaymentAttempt>> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_recurring_details(&self) -> Option<&RecurringDetails> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_intent_profile_id(&self) -> Option<&id_type::ProfileId> {
|
|
Some(&self.payment_intent.profile_id)
|
|
}
|
|
|
|
fn get_currency(&self) -> storage_enums::Currency {
|
|
self.payment_intent.amount_details.currency
|
|
}
|
|
|
|
fn get_amount(&self) -> api::Amount {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_attempt_connector(&self) -> Option<&str> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_billing_address(&self) -> Option<hyperswitch_domain_models::address::Address> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_method_data(&self) -> Option<&domain::PaymentMethodData> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_sessions_token(&self) -> Vec<api::SessionToken> {
|
|
self.sessions_token.clone()
|
|
}
|
|
|
|
fn get_token_data(&self) -> Option<&storage::PaymentTokenData> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_mandate_connector(&self) -> Option<&MandateConnectorDetails> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_force_sync(&self) -> Option<bool> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_capture_method(&self) -> Option<enums::CaptureMethod> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_optional_payment_attempt(&self) -> Option<&storage::PaymentAttempt> {
|
|
todo!();
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
impl<F: Clone> OperationSessionSetters<F> for PaymentIntentData<F> {
|
|
// Setters Implementation
|
|
fn set_payment_intent(&mut self, payment_intent: storage::PaymentIntent) {
|
|
self.payment_intent = payment_intent;
|
|
}
|
|
|
|
fn set_payment_attempt(&mut self, _payment_attempt: storage::PaymentAttempt) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_payment_method_data(&mut self, _payment_method_data: Option<domain::PaymentMethodData>) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_payment_method_id_in_attempt(&mut self, _payment_method_id: Option<String>) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_email_if_not_present(&mut self, _email: pii::Email) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_pm_token(&mut self, _token: String) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_connector_customer_id(&mut self, _customer_id: Option<String>) {
|
|
todo!()
|
|
}
|
|
|
|
fn push_sessions_token(&mut self, token: api::SessionToken) {
|
|
self.sessions_token.push(token);
|
|
}
|
|
|
|
fn set_surcharge_details(&mut self, _surcharge_details: Option<types::SurchargeDetails>) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_merchant_connector_id_in_attempt(
|
|
&mut self,
|
|
merchant_connector_id: Option<id_type::MerchantConnectorAccountId>,
|
|
) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_frm_message(&mut self, _frm_message: FraudCheck) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_payment_intent_status(&mut self, status: storage_enums::IntentStatus) {
|
|
self.payment_intent.status = status;
|
|
}
|
|
|
|
fn set_authentication_type_in_attempt(
|
|
&mut self,
|
|
_authentication_type: Option<enums::AuthenticationType>,
|
|
) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_recurring_mandate_payment_data(
|
|
&mut self,
|
|
_recurring_mandate_payment_data:
|
|
hyperswitch_domain_models::router_data::RecurringMandatePaymentData,
|
|
) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_mandate_id(&mut self, _mandate_id: api_models::payments::MandateIds) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_setup_future_usage_in_payment_intent(
|
|
&mut self,
|
|
setup_future_usage: storage_enums::FutureUsage,
|
|
) {
|
|
self.payment_intent.setup_future_usage = setup_future_usage;
|
|
}
|
|
|
|
fn set_connector_in_payment_attempt(&mut self, _connector: Option<String>) {
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
impl<F: Clone> OperationSessionGetters<F> for PaymentConfirmData<F> {
|
|
fn get_payment_attempt(&self) -> &storage::PaymentAttempt {
|
|
&self.payment_attempt
|
|
}
|
|
|
|
fn get_payment_intent(&self) -> &storage::PaymentIntent {
|
|
&self.payment_intent
|
|
}
|
|
|
|
fn get_payment_method_info(&self) -> Option<&domain::PaymentMethod> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_mandate_id(&self) -> Option<&payments_api::MandateIds> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_address(&self) -> &PaymentAddress {
|
|
&self.payment_address
|
|
}
|
|
|
|
fn get_creds_identifier(&self) -> Option<&str> {
|
|
None
|
|
}
|
|
|
|
fn get_token(&self) -> Option<&str> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_multiple_capture_data(&self) -> Option<&types::MultipleCaptureData> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_link_data(&self) -> Option<api_models::payments::PaymentLinkResponse> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_ephemeral_key(&self) -> Option<ephemeral_key::EphemeralKey> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_setup_mandate(&self) -> Option<&MandateData> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_poll_config(&self) -> Option<router_types::PollConfig> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_authentication(&self) -> Option<&storage::Authentication> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_frm_message(&self) -> Option<FraudCheck> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_refunds(&self) -> Vec<storage::Refund> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_disputes(&self) -> Vec<storage::Dispute> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_authorizations(&self) -> Vec<diesel_models::authorization::Authorization> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_attempts(&self) -> Option<Vec<storage::PaymentAttempt>> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_recurring_details(&self) -> Option<&RecurringDetails> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_intent_profile_id(&self) -> Option<&id_type::ProfileId> {
|
|
Some(&self.payment_intent.profile_id)
|
|
}
|
|
|
|
fn get_currency(&self) -> storage_enums::Currency {
|
|
self.payment_intent.amount_details.currency
|
|
}
|
|
|
|
fn get_amount(&self) -> api::Amount {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_attempt_connector(&self) -> Option<&str> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_billing_address(&self) -> Option<hyperswitch_domain_models::address::Address> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_method_data(&self) -> Option<&domain::PaymentMethodData> {
|
|
self.payment_method_data.as_ref()
|
|
}
|
|
|
|
fn get_sessions_token(&self) -> Vec<api::SessionToken> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_token_data(&self) -> Option<&storage::PaymentTokenData> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_mandate_connector(&self) -> Option<&MandateConnectorDetails> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_force_sync(&self) -> Option<bool> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_capture_method(&self) -> Option<enums::CaptureMethod> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_optional_payment_attempt(&self) -> Option<&storage::PaymentAttempt> {
|
|
Some(&self.payment_attempt)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
impl<F: Clone> OperationSessionSetters<F> for PaymentConfirmData<F> {
|
|
// Setters Implementation
|
|
fn set_payment_intent(&mut self, payment_intent: storage::PaymentIntent) {
|
|
self.payment_intent = payment_intent;
|
|
}
|
|
|
|
fn set_payment_attempt(&mut self, payment_attempt: storage::PaymentAttempt) {
|
|
self.payment_attempt = payment_attempt;
|
|
}
|
|
|
|
fn set_payment_method_data(&mut self, _payment_method_data: Option<domain::PaymentMethodData>) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_payment_method_id_in_attempt(&mut self, _payment_method_id: Option<String>) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_email_if_not_present(&mut self, _email: pii::Email) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_pm_token(&mut self, _token: String) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_connector_customer_id(&mut self, _customer_id: Option<String>) {
|
|
// TODO: handle this case. Should we add connector_customer_id in paymentConfirmData?
|
|
}
|
|
|
|
fn push_sessions_token(&mut self, _token: api::SessionToken) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_surcharge_details(&mut self, _surcharge_details: Option<types::SurchargeDetails>) {
|
|
todo!()
|
|
}
|
|
|
|
#[track_caller]
|
|
fn set_merchant_connector_id_in_attempt(
|
|
&mut self,
|
|
merchant_connector_id: Option<id_type::MerchantConnectorAccountId>,
|
|
) {
|
|
self.payment_attempt.merchant_connector_id = merchant_connector_id;
|
|
}
|
|
|
|
fn set_frm_message(&mut self, _frm_message: FraudCheck) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_payment_intent_status(&mut self, status: storage_enums::IntentStatus) {
|
|
self.payment_intent.status = status;
|
|
}
|
|
|
|
fn set_authentication_type_in_attempt(
|
|
&mut self,
|
|
_authentication_type: Option<enums::AuthenticationType>,
|
|
) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_recurring_mandate_payment_data(
|
|
&mut self,
|
|
_recurring_mandate_payment_data:
|
|
hyperswitch_domain_models::router_data::RecurringMandatePaymentData,
|
|
) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_mandate_id(&mut self, _mandate_id: api_models::payments::MandateIds) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_setup_future_usage_in_payment_intent(
|
|
&mut self,
|
|
setup_future_usage: storage_enums::FutureUsage,
|
|
) {
|
|
self.payment_intent.setup_future_usage = setup_future_usage;
|
|
}
|
|
|
|
fn set_connector_in_payment_attempt(&mut self, connector: Option<String>) {
|
|
self.payment_attempt.connector = connector;
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
impl<F: Clone> OperationSessionGetters<F> for PaymentStatusData<F> {
|
|
#[track_caller]
|
|
fn get_payment_attempt(&self) -> &storage::PaymentAttempt {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_intent(&self) -> &storage::PaymentIntent {
|
|
&self.payment_intent
|
|
}
|
|
|
|
fn get_payment_method_info(&self) -> Option<&domain::PaymentMethod> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_mandate_id(&self) -> Option<&payments_api::MandateIds> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_address(&self) -> &PaymentAddress {
|
|
&self.payment_address
|
|
}
|
|
|
|
fn get_creds_identifier(&self) -> Option<&str> {
|
|
None
|
|
}
|
|
|
|
fn get_token(&self) -> Option<&str> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_multiple_capture_data(&self) -> Option<&types::MultipleCaptureData> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_link_data(&self) -> Option<api_models::payments::PaymentLinkResponse> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_ephemeral_key(&self) -> Option<ephemeral_key::EphemeralKey> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_setup_mandate(&self) -> Option<&MandateData> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_poll_config(&self) -> Option<router_types::PollConfig> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_authentication(&self) -> Option<&storage::Authentication> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_frm_message(&self) -> Option<FraudCheck> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_refunds(&self) -> Vec<storage::Refund> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_disputes(&self) -> Vec<storage::Dispute> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_authorizations(&self) -> Vec<diesel_models::authorization::Authorization> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_attempts(&self) -> Option<Vec<storage::PaymentAttempt>> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_recurring_details(&self) -> Option<&RecurringDetails> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_intent_profile_id(&self) -> Option<&id_type::ProfileId> {
|
|
Some(&self.payment_intent.profile_id)
|
|
}
|
|
|
|
fn get_currency(&self) -> storage_enums::Currency {
|
|
self.payment_intent.amount_details.currency
|
|
}
|
|
|
|
fn get_amount(&self) -> api::Amount {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_attempt_connector(&self) -> Option<&str> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_billing_address(&self) -> Option<hyperswitch_domain_models::address::Address> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_method_data(&self) -> Option<&domain::PaymentMethodData> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_sessions_token(&self) -> Vec<api::SessionToken> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_token_data(&self) -> Option<&storage::PaymentTokenData> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_mandate_connector(&self) -> Option<&MandateConnectorDetails> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_force_sync(&self) -> Option<bool> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_capture_method(&self) -> Option<enums::CaptureMethod> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_optional_payment_attempt(&self) -> Option<&storage::PaymentAttempt> {
|
|
self.payment_attempt.as_ref()
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
impl<F: Clone> OperationSessionSetters<F> for PaymentStatusData<F> {
|
|
fn set_payment_intent(&mut self, payment_intent: storage::PaymentIntent) {
|
|
self.payment_intent = payment_intent;
|
|
}
|
|
|
|
fn set_payment_attempt(&mut self, payment_attempt: storage::PaymentAttempt) {
|
|
self.payment_attempt = Some(payment_attempt);
|
|
}
|
|
|
|
fn set_payment_method_data(&mut self, _payment_method_data: Option<domain::PaymentMethodData>) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_payment_method_id_in_attempt(&mut self, _payment_method_id: Option<String>) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_email_if_not_present(&mut self, _email: pii::Email) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_pm_token(&mut self, _token: String) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_connector_customer_id(&mut self, _customer_id: Option<String>) {
|
|
// TODO: handle this case. Should we add connector_customer_id in paymentConfirmData?
|
|
}
|
|
|
|
fn push_sessions_token(&mut self, _token: api::SessionToken) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_surcharge_details(&mut self, _surcharge_details: Option<types::SurchargeDetails>) {
|
|
todo!()
|
|
}
|
|
|
|
#[track_caller]
|
|
fn set_merchant_connector_id_in_attempt(
|
|
&mut self,
|
|
merchant_connector_id: Option<id_type::MerchantConnectorAccountId>,
|
|
) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_frm_message(&mut self, _frm_message: FraudCheck) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_payment_intent_status(&mut self, status: storage_enums::IntentStatus) {
|
|
self.payment_intent.status = status;
|
|
}
|
|
|
|
fn set_authentication_type_in_attempt(
|
|
&mut self,
|
|
_authentication_type: Option<enums::AuthenticationType>,
|
|
) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_recurring_mandate_payment_data(
|
|
&mut self,
|
|
_recurring_mandate_payment_data:
|
|
hyperswitch_domain_models::router_data::RecurringMandatePaymentData,
|
|
) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_mandate_id(&mut self, _mandate_id: api_models::payments::MandateIds) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_setup_future_usage_in_payment_intent(
|
|
&mut self,
|
|
setup_future_usage: storage_enums::FutureUsage,
|
|
) {
|
|
self.payment_intent.setup_future_usage = setup_future_usage;
|
|
}
|
|
|
|
fn set_connector_in_payment_attempt(&mut self, connector: Option<String>) {
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
impl<F: Clone> OperationSessionGetters<F> for PaymentCaptureData<F> {
|
|
#[track_caller]
|
|
fn get_payment_attempt(&self) -> &storage::PaymentAttempt {
|
|
&self.payment_attempt
|
|
}
|
|
|
|
fn get_payment_intent(&self) -> &storage::PaymentIntent {
|
|
&self.payment_intent
|
|
}
|
|
|
|
fn get_payment_method_info(&self) -> Option<&domain::PaymentMethod> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_mandate_id(&self) -> Option<&payments_api::MandateIds> {
|
|
todo!()
|
|
}
|
|
|
|
// what is this address find out and not required remove this
|
|
fn get_address(&self) -> &PaymentAddress {
|
|
todo!()
|
|
}
|
|
|
|
fn get_creds_identifier(&self) -> Option<&str> {
|
|
None
|
|
}
|
|
|
|
fn get_token(&self) -> Option<&str> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_multiple_capture_data(&self) -> Option<&types::MultipleCaptureData> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_link_data(&self) -> Option<api_models::payments::PaymentLinkResponse> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_ephemeral_key(&self) -> Option<ephemeral_key::EphemeralKey> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_setup_mandate(&self) -> Option<&MandateData> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_poll_config(&self) -> Option<router_types::PollConfig> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_authentication(&self) -> Option<&storage::Authentication> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_frm_message(&self) -> Option<FraudCheck> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_refunds(&self) -> Vec<storage::Refund> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_disputes(&self) -> Vec<storage::Dispute> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_authorizations(&self) -> Vec<diesel_models::authorization::Authorization> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_attempts(&self) -> Option<Vec<storage::PaymentAttempt>> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_recurring_details(&self) -> Option<&RecurringDetails> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_intent_profile_id(&self) -> Option<&id_type::ProfileId> {
|
|
Some(&self.payment_intent.profile_id)
|
|
}
|
|
|
|
fn get_currency(&self) -> storage_enums::Currency {
|
|
self.payment_intent.amount_details.currency
|
|
}
|
|
|
|
fn get_amount(&self) -> api::Amount {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_attempt_connector(&self) -> Option<&str> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_billing_address(&self) -> Option<hyperswitch_domain_models::address::Address> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_payment_method_data(&self) -> Option<&domain::PaymentMethodData> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_sessions_token(&self) -> Vec<api::SessionToken> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_token_data(&self) -> Option<&storage::PaymentTokenData> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_mandate_connector(&self) -> Option<&MandateConnectorDetails> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_force_sync(&self) -> Option<bool> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_capture_method(&self) -> Option<enums::CaptureMethod> {
|
|
todo!()
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
fn get_optional_payment_attempt(&self) -> Option<&storage::PaymentAttempt> {
|
|
Some(&self.payment_attempt)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "v2")]
|
|
impl<F: Clone> OperationSessionSetters<F> for PaymentCaptureData<F> {
|
|
fn set_payment_intent(&mut self, payment_intent: storage::PaymentIntent) {
|
|
self.payment_intent = payment_intent;
|
|
}
|
|
|
|
fn set_payment_attempt(&mut self, payment_attempt: storage::PaymentAttempt) {
|
|
self.payment_attempt = payment_attempt;
|
|
}
|
|
|
|
fn set_payment_method_data(&mut self, _payment_method_data: Option<domain::PaymentMethodData>) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_payment_method_id_in_attempt(&mut self, _payment_method_id: Option<String>) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_email_if_not_present(&mut self, _email: pii::Email) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_pm_token(&mut self, _token: String) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_connector_customer_id(&mut self, _customer_id: Option<String>) {
|
|
// TODO: handle this case. Should we add connector_customer_id in paymentConfirmData?
|
|
}
|
|
|
|
fn push_sessions_token(&mut self, _token: api::SessionToken) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_surcharge_details(&mut self, _surcharge_details: Option<types::SurchargeDetails>) {
|
|
todo!()
|
|
}
|
|
|
|
#[track_caller]
|
|
fn set_merchant_connector_id_in_attempt(
|
|
&mut self,
|
|
merchant_connector_id: Option<id_type::MerchantConnectorAccountId>,
|
|
) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_frm_message(&mut self, _frm_message: FraudCheck) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_payment_intent_status(&mut self, status: storage_enums::IntentStatus) {
|
|
self.payment_intent.status = status;
|
|
}
|
|
|
|
fn set_authentication_type_in_attempt(
|
|
&mut self,
|
|
_authentication_type: Option<enums::AuthenticationType>,
|
|
) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_recurring_mandate_payment_data(
|
|
&mut self,
|
|
_recurring_mandate_payment_data:
|
|
hyperswitch_domain_models::router_data::RecurringMandatePaymentData,
|
|
) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_mandate_id(&mut self, _mandate_id: api_models::payments::MandateIds) {
|
|
todo!()
|
|
}
|
|
|
|
fn set_setup_future_usage_in_payment_intent(
|
|
&mut self,
|
|
setup_future_usage: storage_enums::FutureUsage,
|
|
) {
|
|
self.payment_intent.setup_future_usage = setup_future_usage;
|
|
}
|
|
|
|
fn set_connector_in_payment_attempt(&mut self, connector: Option<String>) {
|
|
todo!()
|
|
}
|
|
}
|