feat(core): Constraint Graph for Payment Methods List (#5081)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Prajjwal Kumar
2024-07-09 22:45:15 +05:30
committed by GitHub
parent fdac313241
commit 82c6e0e649
17 changed files with 1288 additions and 541 deletions

View File

@ -79,6 +79,8 @@ pub enum MandateAcceptanceType {
pub enum PaymentType {
SetupMandate,
NonMandate,
NewMandate,
UpdateMandate,
}
#[derive(

View File

@ -194,7 +194,7 @@ where
nodes: &[(NodeId, Relation, Strength)],
info: Option<&'static str>,
metadata: Option<M>,
domain: Option<String>,
domain_id: Option<DomainId>,
) -> Result<NodeId, GraphError<V>> {
nodes
.iter()
@ -208,13 +208,7 @@ where
.push(metadata.map(|meta| -> Arc<dyn Metadata> { Arc::new(meta) }));
for (node_id, relation, strength) in nodes {
self.make_edge(
*node_id,
aggregator_id,
*strength,
*relation,
domain.clone(),
)?;
self.make_edge(*node_id, aggregator_id, *strength, *relation, domain_id)?;
}
Ok(aggregator_id)
@ -225,7 +219,7 @@ where
nodes: &[(NodeId, Relation, Strength)],
info: Option<&'static str>,
metadata: Option<M>,
domain: Option<String>,
domain_id: Option<DomainId>,
) -> Result<NodeId, GraphError<V>> {
nodes
.iter()
@ -239,13 +233,7 @@ where
.push(metadata.map(|meta| -> Arc<dyn Metadata> { Arc::new(meta) }));
for (node_id, relation, strength) in nodes {
self.make_edge(
*node_id,
aggregator_id,
*strength,
*relation,
domain.clone(),
)?;
self.make_edge(*node_id, aggregator_id, *strength, *relation, domain_id)?;
}
Ok(aggregator_id)

View File

@ -158,14 +158,6 @@ impl From<String> for DomainIdentifier {
}
}
// impl Deref for DomainIdentifier {
// type Target = &String;
//
// fn deref(&self) -> Self::Target {
// self.0
// }
// }
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DomainInfo {
pub domain_identifier: DomainIdentifier,

View File

@ -5,6 +5,8 @@ use euclid::{dssa::types::AnalysisErrorType, frontend::dir};
pub enum KgraphError {
#[error("Invalid connector name encountered: '{0}'")]
InvalidConnectorName(String),
#[error("Error in domain creation")]
DomainCreationError,
#[error("There was an error constructing the graph: {0}")]
GraphConstructionError(hyperswitch_constraint_graph::GraphError<dir::DirValue>),
#[error("There was an error constructing the context")]

View File

@ -1,6 +1,7 @@
pub mod cards;
pub mod surcharge_decision_configs;
pub mod transformers;
pub mod utils;
pub mod vault;
pub use api_models::enums::Connector;
#[cfg(feature = "payouts")]

View File

@ -5,8 +5,8 @@ use std::{
};
use api_models::{
admin::{self, PaymentMethodsEnabled},
enums::{self as api_enums},
admin::PaymentMethodsEnabled,
enums as api_enums,
payment_methods::{
BankAccountTokenData, Card, CardDetailUpdate, CardDetailsPaymentMethod, CardNetworkTypes,
CountryCodeWithName, CustomerDefaultPaymentMethodResponse, ListCountriesCurrenciesRequest,
@ -27,12 +27,15 @@ use common_utils::{
generate_id, id_type,
types::MinorUnit,
};
use diesel_models::{
business_profile::BusinessProfile, encryption::Encryption, enums as storage_enums,
payment_method,
};
use diesel_models::{business_profile::BusinessProfile, encryption::Encryption, payment_method};
use domain::CustomerUpdate;
use error_stack::{report, ResultExt};
use euclid::{
dssa::graph::{AnalysisContext, CgraphExt},
frontend::dir,
};
use hyperswitch_constraint_graph as cgraph;
use kgraph_utils::transformers::IntoDirValue;
use masking::Secret;
use router_env::{instrument, metrics::add_attributes, tracing};
use strum::IntoEnumIterator;
@ -48,7 +51,9 @@ use crate::{
core::{
errors::{self, StorageErrorExt},
payment_methods::{
add_payment_method_status_update_task, transformers as payment_methods, vault,
add_payment_method_status_update_task, transformers as payment_methods,
utils::{get_merchant_pm_filter_graph, make_pm_graph, refresh_pm_filters_cache},
vault,
},
payments::{
helpers,
@ -1945,18 +1950,39 @@ pub async fn list_payment_methods(
.await?;
// filter out connectors based on the business country
let filtered_mcas = helpers::filter_mca_based_on_business_profile(all_mcas, profile_id);
let filtered_mcas = helpers::filter_mca_based_on_business_profile(all_mcas, profile_id.clone());
logger::debug!(mca_before_filtering=?filtered_mcas);
let mut response: Vec<ResponsePaymentMethodIntermediate> = vec![];
for mca in &filtered_mcas {
let payment_methods = match &mca.payment_methods_enabled {
Some(pm) => pm.clone(),
None => continue,
// Key creation for storing PM_FILTER_CGRAPH
#[cfg(feature = "business_profile_routing")]
let key = {
let profile_id = profile_id
.clone()
.get_required_value("profile_id")
.change_context(errors::ApiErrorResponse::GenericNotFoundError {
message: "Profile id not found".to_string(),
})?;
format!(
"pm_filters_cgraph_{}_{}",
&merchant_account.merchant_id, profile_id
)
};
#[cfg(not(feature = "business_profile_routing"))]
let key = { format!("pm_filters_cgraph_{}", &merchant_account.merchant_id) };
if let Some(graph) = get_merchant_pm_filter_graph(&state, &key).await {
// Derivation of PM_FILTER_CGRAPH from MokaCache successful
for mca in &filtered_mcas {
let payment_methods = match &mca.payment_methods_enabled {
Some(pm) => pm,
None => continue,
};
filter_payment_methods(
&graph,
mca.merchant_connector_id.clone(),
payment_methods,
&mut req,
&mut response,
@ -1964,13 +1990,66 @@ pub async fn list_payment_methods(
payment_attempt.as_ref(),
billing_address.as_ref(),
mca.connector_name.clone(),
pm_config_mapping,
&state.conf.mandates.supported_payment_methods,
&state.conf.mandates.update_mandate_supported,
&state.conf.saved_payment_methods,
)
.await?;
}
} else {
// No PM_FILTER_CGRAPH Cache present in MokaCache
let mut builder = cgraph::ConstraintGraphBuilder::new();
for mca in &filtered_mcas {
let domain_id = builder.make_domain(
mca.merchant_connector_id.clone(),
mca.connector_name.as_str(),
);
let Ok(domain_id) = domain_id else {
logger::error!("Failed to construct domain for list payment methods");
return Err(errors::ApiErrorResponse::InternalServerError.into());
};
let payment_methods = match &mca.payment_methods_enabled {
Some(pm) => pm,
None => continue,
};
if let Err(e) = make_pm_graph(
&mut builder,
domain_id,
payment_methods,
mca.connector_name.clone(),
pm_config_mapping,
&state.conf.mandates.supported_payment_methods,
&state.conf.mandates.update_mandate_supported,
) {
logger::error!(
"Failed to construct constraint graph for list payment methods {e:?}"
);
}
}
// Refreshing our CGraph cache
let graph = refresh_pm_filters_cache(&state, &key, builder.build()).await;
for mca in &filtered_mcas {
let payment_methods = match &mca.payment_methods_enabled {
Some(pm) => pm,
None => continue,
};
filter_payment_methods(
&graph,
mca.merchant_connector_id.clone(),
payment_methods,
&mut req,
&mut response,
payment_intent.as_ref(),
payment_attempt.as_ref(),
billing_address.as_ref(),
mca.connector_name.clone(),
&state.conf.saved_payment_methods,
)
.await?;
}
}
// Filter out wallet payment method from mca if customer has already saved it
customer
@ -2902,20 +2981,19 @@ pub async fn call_surcharge_decision_management_for_saved_card(
#[allow(clippy::too_many_arguments)]
pub async fn filter_payment_methods(
payment_methods: Vec<serde_json::Value>,
graph: &cgraph::ConstraintGraph<dir::DirValue>,
mca_id: String,
payment_methods: &[serde_json::Value],
req: &mut api::PaymentMethodListRequest,
resp: &mut Vec<ResponsePaymentMethodIntermediate>,
payment_intent: Option<&storage::PaymentIntent>,
payment_attempt: Option<&storage::PaymentAttempt>,
address: Option<&domain::Address>,
connector: String,
config: &settings::ConnectorFilters,
supported_payment_methods_for_mandate: &settings::SupportedPaymentMethodsForMandate,
supported_payment_methods_for_update_mandate: &settings::SupportedPaymentMethodsForMandate,
saved_payment_methods: &settings::EligiblePaymentMethods,
) -> errors::CustomResult<(), errors::ApiErrorResponse> {
for payment_method in payment_methods.into_iter() {
let parse_result = serde_json::from_value::<PaymentMethodsEnabled>(payment_method);
for payment_method in payment_methods.iter() {
let parse_result = serde_json::from_value::<PaymentMethodsEnabled>(payment_method.clone());
if let Ok(payment_methods_enabled) = parse_result {
let payment_method = payment_methods_enabled.payment_method;
@ -2942,57 +3020,13 @@ pub async fn filter_payment_methods(
)
&& filter_amount_based(&payment_method_type_info, req.amount)
{
let mut payment_method_object = payment_method_type_info;
let payment_method_object = payment_method_type_info.clone();
let filter;
(
payment_method_object.accepted_countries,
req.accepted_countries,
filter,
) = filter_pm_country_based(
&payment_method_object.accepted_countries,
&req.accepted_countries,
);
let filter2;
(
payment_method_object.accepted_currencies,
req.accepted_currencies,
filter2,
) = filter_pm_currencies_based(
&payment_method_object.accepted_currencies,
&req.accepted_currencies,
);
let filter4 = filter_pm_card_network_based(
payment_method_object.card_networks.as_ref(),
req.card_networks.as_ref(),
&payment_method_object.payment_method_type,
);
let filter3 = if let Some(payment_intent) = payment_intent {
filter_payment_country_based(&payment_method_object, address).await?
&& filter_payment_currency_based(payment_intent, &payment_method_object)
&& filter_payment_amount_based(payment_intent, &payment_method_object)
&& filter_payment_mandate_based(payment_attempt, &payment_method_object)
.await?
} else {
true
};
let filter5 = filter_pm_based_on_config(
config,
&connector,
&payment_method_object.payment_method_type,
payment_attempt,
&mut payment_method_object.card_networks,
&address.and_then(|inner| inner.country),
payment_attempt.and_then(|value| value.currency),
);
let filter6 = filter_pm_based_on_allowed_types(
allowed_payment_method_types.as_ref(),
&payment_method_object.payment_method_type,
);
let pm_dir_value: dir::DirValue =
(payment_method_type_info.payment_method_type, payment_method)
.into_dir_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("pm_value_node not created")?;
let connector_variant = api_enums::Connector::from_str(connector.as_str())
.change_context(errors::ConnectorError::InvalidConnectorName)
@ -3002,35 +3036,85 @@ pub async fn filter_payment_methods(
.attach_printable_lazy(|| {
format!("unable to parse connector name {connector:?}")
})?;
let filter7 = payment_attempt
.and_then(|attempt| attempt.mandate_details.as_ref())
.map(|_mandate_details| {
filter_pm_based_on_supported_payments_for_mandate(
supported_payment_methods_for_mandate,
&payment_method,
&payment_method_object.payment_method_type,
connector_variant,
)
})
.unwrap_or(true);
let filter8 = payment_attempt
let mut context_values: Vec<dir::DirValue> = Vec::new();
context_values.push(pm_dir_value.clone());
payment_intent.map(|intent| {
intent.currency.map(|currency| {
context_values.push(dir::DirValue::PaymentCurrency(currency))
})
});
address.map(|address| {
address.country.map(|country| {
context_values.push(dir::DirValue::BillingCountry(
common_enums::Country::from_alpha2(country),
))
})
});
// Addition of Connector to context
if let Ok(connector) = api_enums::RoutableConnectors::from_str(
connector_variant.to_string().as_str(),
) {
context_values.push(dir::DirValue::Connector(Box::new(
api_models::routing::ast::ConnectorChoice {
connector,
#[cfg(not(feature = "connector_choice_mca_id"))]
sub_label: None,
},
)));
};
let filter_pm_based_on_allowed_types = filter_pm_based_on_allowed_types(
allowed_payment_method_types.as_ref(),
&payment_method_object.payment_method_type,
);
if payment_attempt
.and_then(|attempt| attempt.mandate_details.as_ref())
.is_some()
{
context_values.push(dir::DirValue::PaymentType(
euclid::enums::PaymentType::NewMandate,
));
};
payment_attempt
.and_then(|attempt| attempt.mandate_data.as_ref())
.map(|mandate_detail| {
if mandate_detail.update_mandate_id.is_some() {
filter_pm_based_on_update_mandate_support_for_connector(
supported_payment_methods_for_update_mandate,
&payment_method,
&payment_method_object.payment_method_type,
connector_variant,
)
} else {
true
context_values.push(dir::DirValue::PaymentType(
euclid::enums::PaymentType::UpdateMandate,
));
}
})
.unwrap_or(true);
});
let filter9 = req
payment_attempt
.map(|attempt| {
attempt.mandate_data.is_none() && attempt.mandate_details.is_none()
})
.and_then(|res| {
res.then(|| {
context_values.push(dir::DirValue::PaymentType(
euclid::enums::PaymentType::NonMandate,
))
})
});
payment_attempt
.and_then(|inner| inner.capture_method)
.map(|capture_method| {
context_values.push(dir::DirValue::CaptureMethod(capture_method));
});
let filter_pm_card_network_based = filter_pm_card_network_based(
payment_method_object.card_networks.as_ref(),
req.card_networks.as_ref(),
&payment_method_object.payment_method_type,
);
let saved_payment_methods_filter = req
.client_secret
.as_ref()
.map(|cs| {
@ -3044,25 +3128,29 @@ pub async fn filter_payment_methods(
})
.unwrap_or(true);
let connector = connector.clone();
let context = AnalysisContext::from_dir_values(context_values.clone());
let domain_ident: &[String] = &[mca_id.clone()];
let result = graph.key_value_analysis(
pm_dir_value.clone(),
&context,
&mut cgraph::Memoization::new(),
&mut cgraph::CycleCheck::new(),
Some(domain_ident),
);
if filter_pm_based_on_allowed_types
&& filter_pm_card_network_based
&& saved_payment_methods_filter
&& matches!(result, Ok(()))
{
let response_pm_type = ResponsePaymentMethodIntermediate::new(
payment_method_object,
connector,
connector.clone(),
payment_method,
);
if filter
&& filter2
&& filter3
&& filter4
&& filter5
&& filter6
&& filter7
&& filter8
&& filter9
{
resp.push(response_pm_type);
} else {
logger::error!("Filtering Payment Methods Failed");
}
}
}
@ -3070,117 +3158,6 @@ pub async fn filter_payment_methods(
}
Ok(())
}
pub fn filter_pm_based_on_update_mandate_support_for_connector(
supported_payment_methods_for_mandate: &settings::SupportedPaymentMethodsForMandate,
payment_method: &api_enums::PaymentMethod,
payment_method_type: &api_enums::PaymentMethodType,
connector: api_enums::Connector,
) -> bool {
if payment_method == &api_enums::PaymentMethod::Card {
supported_payment_methods_for_mandate
.0
.get(payment_method)
.map(|payment_method_type_hm| {
let pm_credit = payment_method_type_hm
.0
.get(&api_enums::PaymentMethodType::Credit)
.map(|conn| conn.connector_list.clone())
.unwrap_or_default();
let pm_debit = payment_method_type_hm
.0
.get(&api_enums::PaymentMethodType::Debit)
.map(|conn| conn.connector_list.clone())
.unwrap_or_default();
&pm_credit | &pm_debit
})
.map(|supported_connectors| supported_connectors.contains(&connector))
.unwrap_or(false)
} else {
supported_payment_methods_for_mandate
.0
.get(payment_method)
.and_then(|payment_method_type_hm| payment_method_type_hm.0.get(payment_method_type))
.map(|supported_connectors| supported_connectors.connector_list.contains(&connector))
.unwrap_or(false)
}
}
fn filter_pm_based_on_supported_payments_for_mandate(
supported_payment_methods_for_mandate: &settings::SupportedPaymentMethodsForMandate,
payment_method: &api_enums::PaymentMethod,
payment_method_type: &api_enums::PaymentMethodType,
connector: api_enums::Connector,
) -> bool {
supported_payment_methods_for_mandate
.0
.get(payment_method)
.and_then(|payment_method_type_hm| payment_method_type_hm.0.get(payment_method_type))
.map(|supported_connectors| supported_connectors.connector_list.contains(&connector))
.unwrap_or(false)
}
fn filter_pm_based_on_config<'a>(
config: &'a settings::ConnectorFilters,
connector: &'a str,
payment_method_type: &'a api_enums::PaymentMethodType,
payment_attempt: Option<&storage::PaymentAttempt>,
card_network: &mut Option<Vec<api_enums::CardNetwork>>,
country: &Option<api_enums::CountryAlpha2>,
currency: Option<api_enums::Currency>,
) -> bool {
config
.0
.get(connector)
.or_else(|| config.0.get("default"))
.and_then(|inner| match payment_method_type {
api_enums::PaymentMethodType::Credit | api_enums::PaymentMethodType::Debit => {
let country_currency_filter = inner
.0
.get(&settings::PaymentMethodFilterKey::PaymentMethodType(
*payment_method_type,
))
.map(|value| global_country_currency_filter(value, country, currency));
card_network_filter(country, currency, card_network, inner);
let capture_method_filter = payment_attempt
.and_then(|inner| inner.capture_method)
.and_then(|capture_method| {
(capture_method == storage_enums::CaptureMethod::Manual).then(|| {
filter_pm_based_on_capture_method_used(inner, payment_method_type)
})
});
Some(
country_currency_filter.unwrap_or(true)
&& capture_method_filter.unwrap_or(true),
)
}
payment_method_type => inner
.0
.get(&settings::PaymentMethodFilterKey::PaymentMethodType(
*payment_method_type,
))
.map(|value| global_country_currency_filter(value, country, currency)),
})
.unwrap_or(true)
}
///Filters the payment method list on basis of Capture methods, checks whether the connector issues Manual payments using cards or not if not it won't be visible in payment methods list
fn filter_pm_based_on_capture_method_used(
payment_method_filters: &settings::PaymentMethodFilters,
payment_method_type: &api_enums::PaymentMethodType,
) -> bool {
payment_method_filters
.0
.get(&settings::PaymentMethodFilterKey::PaymentMethodType(
*payment_method_type,
))
.and_then(|v| v.not_available_flows)
.and_then(|v| v.capture_method)
.map(|v| !matches!(v, api_enums::CaptureMethod::Manual))
.unwrap_or(true)
}
fn filter_amount_based(
payment_method: &RequestPaymentMethodTypes,
@ -3195,45 +3172,13 @@ fn filter_amount_based(
(min_check && max_check) || amount == Some(MinorUnit::zero())
}
fn card_network_filter(
country: &Option<api_enums::CountryAlpha2>,
currency: Option<api_enums::Currency>,
card_network: &mut Option<Vec<api_enums::CardNetwork>>,
payment_method_filters: &settings::PaymentMethodFilters,
) {
if let Some(value) = card_network.as_mut() {
let filtered_card_networks = value
.iter()
.filter(|&element| {
let key = settings::PaymentMethodFilterKey::CardNetwork(element.clone());
payment_method_filters
.0
.get(&key)
.map(|value| global_country_currency_filter(value, country, currency))
.unwrap_or(true)
})
.cloned()
.collect::<Vec<_>>();
*value = filtered_card_networks;
}
}
fn global_country_currency_filter(
item: &settings::CurrencyCountryFlowFilter,
country: &Option<api_enums::CountryAlpha2>,
currency: Option<api_enums::Currency>,
fn filter_installment_based(
payment_method: &RequestPaymentMethodTypes,
installment_payment_enabled: Option<bool>,
) -> bool {
let country_condition = item
.country
.as_ref()
.zip(country.as_ref())
.map(|(lhs, rhs)| lhs.contains(rhs));
let currency_condition = item
.currency
.as_ref()
.zip(currency)
.map(|(lhs, rhs)| lhs.contains(&rhs));
country_condition.unwrap_or(true) && currency_condition.unwrap_or(true)
installment_payment_enabled.map_or(true, |enabled| {
payment_method.installment_payment_enabled == enabled
})
}
fn filter_pm_card_network_based(
@ -3254,117 +3199,6 @@ fn filter_pm_card_network_based(
_ => true,
}
}
fn filter_pm_country_based(
accepted_countries: &Option<admin::AcceptedCountries>,
req_country_list: &Option<Vec<api_enums::CountryAlpha2>>,
) -> (
Option<admin::AcceptedCountries>,
Option<Vec<api_enums::CountryAlpha2>>,
bool,
) {
match (accepted_countries, req_country_list) {
(None, None) => (None, None, true),
(None, Some(ref r)) => (
Some(admin::AcceptedCountries::EnableOnly(r.to_vec())),
Some(r.to_vec()),
true,
),
(Some(l), None) => (Some(l.to_owned()), None, true),
(Some(l), Some(ref r)) => {
let updated = match l {
admin::AcceptedCountries::EnableOnly(acc) => {
filter_accepted_enum_based(&Some(acc.clone()), &Some(r.to_owned()))
.map(admin::AcceptedCountries::EnableOnly)
}
admin::AcceptedCountries::DisableOnly(den) => {
filter_disabled_enum_based(&Some(den.clone()), &Some(r.to_owned()))
.map(admin::AcceptedCountries::DisableOnly)
}
admin::AcceptedCountries::AllAccepted => {
Some(admin::AcceptedCountries::AllAccepted)
}
};
(updated, Some(r.to_vec()), true)
}
}
}
fn filter_pm_currencies_based(
accepted_currency: &Option<admin::AcceptedCurrencies>,
req_currency_list: &Option<Vec<api_enums::Currency>>,
) -> (
Option<admin::AcceptedCurrencies>,
Option<Vec<api_enums::Currency>>,
bool,
) {
match (accepted_currency, req_currency_list) {
(None, None) => (None, None, true),
(None, Some(ref r)) => (
Some(admin::AcceptedCurrencies::EnableOnly(r.to_vec())),
Some(r.to_vec()),
true,
),
(Some(l), None) => (Some(l.to_owned()), None, true),
(Some(l), Some(ref r)) => {
let updated = match l {
admin::AcceptedCurrencies::EnableOnly(acc) => {
filter_accepted_enum_based(&Some(acc.clone()), &Some(r.to_owned()))
.map(admin::AcceptedCurrencies::EnableOnly)
}
admin::AcceptedCurrencies::DisableOnly(den) => {
filter_disabled_enum_based(&Some(den.clone()), &Some(r.to_owned()))
.map(admin::AcceptedCurrencies::DisableOnly)
}
admin::AcceptedCurrencies::AllAccepted => {
Some(admin::AcceptedCurrencies::AllAccepted)
}
};
(updated, Some(r.to_vec()), true)
}
}
}
fn filter_accepted_enum_based<T: Eq + std::hash::Hash + Clone>(
left: &Option<Vec<T>>,
right: &Option<Vec<T>>,
) -> Option<Vec<T>> {
match (left, right) {
(Some(ref l), Some(ref r)) => {
let a: HashSet<&T> = HashSet::from_iter(l.iter());
let b: HashSet<&T> = HashSet::from_iter(r.iter());
let y: Vec<T> = a.intersection(&b).map(|&i| i.to_owned()).collect();
Some(y)
}
(Some(ref l), None) => Some(l.to_vec()),
(_, _) => None,
}
}
fn filter_disabled_enum_based<T: Eq + std::hash::Hash + Clone>(
left: &Option<Vec<T>>,
right: &Option<Vec<T>>,
) -> Option<Vec<T>> {
match (left, right) {
(Some(ref l), Some(ref r)) => {
let mut enabled = Vec::new();
for element in r {
if !l.contains(element) {
enabled.push(element.to_owned());
}
}
Some(enabled)
}
(None, Some(r)) => Some(r.to_vec()),
(_, _) => None,
}
}
fn filter_pm_based_on_allowed_types(
allowed_types: Option<&Vec<api_enums::PaymentMethodType>>,
@ -3380,65 +3214,6 @@ fn filter_recurring_based(
recurring_enabled.map_or(true, |enabled| payment_method.recurring_enabled == enabled)
}
fn filter_installment_based(
payment_method: &RequestPaymentMethodTypes,
installment_payment_enabled: Option<bool>,
) -> bool {
installment_payment_enabled.map_or(true, |enabled| {
payment_method.installment_payment_enabled == enabled
})
}
async fn filter_payment_country_based(
pm: &RequestPaymentMethodTypes,
address: Option<&domain::Address>,
) -> errors::CustomResult<bool, errors::ApiErrorResponse> {
Ok(address.map_or(true, |address| {
address.country.as_ref().map_or(true, |country| {
pm.accepted_countries.as_ref().map_or(true, |ac| match ac {
admin::AcceptedCountries::EnableOnly(acc) => acc.contains(country),
admin::AcceptedCountries::DisableOnly(den) => !den.contains(country),
admin::AcceptedCountries::AllAccepted => true,
})
})
}))
}
fn filter_payment_currency_based(
payment_intent: &storage::PaymentIntent,
pm: &RequestPaymentMethodTypes,
) -> bool {
payment_intent.currency.map_or(true, |currency| {
pm.accepted_currencies.as_ref().map_or(true, |ac| match ac {
admin::AcceptedCurrencies::EnableOnly(acc) => acc.contains(&currency),
admin::AcceptedCurrencies::DisableOnly(den) => !den.contains(&currency),
admin::AcceptedCurrencies::AllAccepted => true,
})
})
}
fn filter_payment_amount_based(
payment_intent: &storage::PaymentIntent,
pm: &RequestPaymentMethodTypes,
) -> bool {
let amount = payment_intent.amount;
(pm.maximum_amount.map_or(true, |amt| amount <= amt)
&& pm.minimum_amount.map_or(true, |amt| amount >= amt))
|| payment_intent.amount == MinorUnit::zero()
}
async fn filter_payment_mandate_based(
payment_attempt: Option<&storage::PaymentAttempt>,
pm: &RequestPaymentMethodTypes,
) -> errors::CustomResult<bool, errors::ApiErrorResponse> {
let recurring_filter = if !pm.recurring_enabled {
payment_attempt.map_or(true, |pa| pa.mandate_id.is_none())
} else {
true
};
Ok(recurring_filter)
}
pub async fn do_list_customer_pm_fetch_customer_if_not_passed(
state: routes::SessionState,
merchant_account: domain::MerchantAccount,

View File

@ -1,5 +1,5 @@
use std::{str::FromStr, sync::Arc};
:
use api_models::{
admin::{self, PaymentMethodsEnabled},
enums as api_enums,
@ -14,7 +14,8 @@ use storage_impl::redis::cache::{CacheKey, PM_FILTERS_CGRAPH_CACHE};
use crate::{configs::settings, routes::SessionState};
pub fn make_pm_graph(
builder: &mut cgraph::ConstraintGraphBuilder<'_, dir::DirValue>,
builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
domain_id: cgraph::DomainId,
payment_methods: &[serde_json::value::Value],
connector: String,
pm_config_mapping: &settings::ConnectorFilters,
@ -26,6 +27,7 @@ pub fn make_pm_graph(
if let Ok(payment_methods_enabled) = pm_enabled {
compile_pm_graph(
builder,
domain_id,
payment_methods_enabled.clone(),
connector.clone(),
pm_config_mapping,
@ -40,22 +42,20 @@ pub fn make_pm_graph(
pub async fn get_merchant_pm_filter_graph<'a>(
state: &SessionState,
key: &str,
) -> Option<Arc<hyperswitch_constraint_graph::ConstraintGraph<'a, dir::DirValue>>> {
) -> Option<Arc<hyperswitch_constraint_graph::ConstraintGraph<dir::DirValue>>> {
PM_FILTERS_CGRAPH_CACHE
.get_val::<Arc<hyperswitch_constraint_graph::ConstraintGraph<'_, dir::DirValue>>>(
CacheKey {
.get_val::<Arc<hyperswitch_constraint_graph::ConstraintGraph<dir::DirValue>>>(CacheKey {
key: key.to_string(),
prefix: state.tenant.redis_key_prefix.clone(),
},
)
})
.await
}
pub async fn refresh_pm_filters_cache(
state: &SessionState,
key: &str,
graph: cgraph::ConstraintGraph<'static, dir::DirValue>,
) -> Arc<hyperswitch_constraint_graph::ConstraintGraph<'static, dir::DirValue>> {
graph: cgraph::ConstraintGraph<dir::DirValue>,
) -> Arc<hyperswitch_constraint_graph::ConstraintGraph<dir::DirValue>> {
let pm_filter_graph = Arc::new(graph);
PM_FILTERS_CGRAPH_CACHE
.push(
@ -70,7 +70,8 @@ pub async fn refresh_pm_filters_cache(
}
fn compile_pm_graph(
builder: &mut cgraph::ConstraintGraphBuilder<'_, dir::DirValue>,
builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
domain_id: cgraph::DomainId,
pm_enabled: PaymentMethodsEnabled,
connector: String,
config: &settings::ConnectorFilters,
@ -90,6 +91,7 @@ fn compile_pm_graph(
// Connector supported for Update mandate filter
let res = construct_supported_connectors_for_update_mandate_node(
builder,
domain_id,
supported_payment_methods_for_update_mandate,
pmt.clone(),
&pm_enabled.payment_method,
@ -119,6 +121,7 @@ fn compile_pm_graph(
if let Ok(Some(connector_eligible_for_mandates_node)) =
construct_supported_connectors_for_mandate_node(
builder,
domain_id,
supported_connectors,
)
{
@ -163,7 +166,7 @@ fn compile_pm_graph(
],
None,
None::<()>,
None,
Some(domain_id),
)
.map_err(KgraphError::GraphConstructionError)?;
@ -174,7 +177,12 @@ fn compile_pm_graph(
));
let agg_or_node = builder
.make_any_aggregator(&agg_or_nodes_for_mandate_filters, None, None::<()>, None)
.make_any_aggregator(
&agg_or_nodes_for_mandate_filters,
None,
None::<()>,
Some(domain_id),
)
.map_err(KgraphError::GraphConstructionError)?;
agg_nodes.push((
@ -203,6 +211,7 @@ fn compile_pm_graph(
// Country filter
if let Ok(Some(country_node)) = compile_accepted_countries_for_mca(
builder,
domain_id,
&pmt.payment_method_type,
pmt.accepted_countries,
config,
@ -218,6 +227,7 @@ fn compile_pm_graph(
// Currency filter
if let Ok(Some(currency_node)) = compile_accepted_currency_for_mca(
builder,
domain_id,
&pmt.payment_method_type,
pmt.accepted_currencies,
config,
@ -231,7 +241,7 @@ fn compile_pm_graph(
}
let and_node_for_all_the_filters = builder
.make_all_aggregator(&agg_nodes, None, None::<()>, None)
.make_all_aggregator(&agg_nodes, None, None::<()>, Some(domain_id))
.map_err(KgraphError::GraphConstructionError)?;
// Making our output node
@ -247,9 +257,9 @@ fn compile_pm_graph(
.make_edge(
and_node_for_all_the_filters,
payment_method_type_value_node,
cgraph::Strength::Strong,
cgraph::Strength::Normal,
cgraph::Relation::Positive,
None::<cgraph::DomainId>,
Some(domain_id),
)
.map_err(KgraphError::GraphConstructionError)?;
}
@ -257,34 +267,9 @@ fn compile_pm_graph(
Ok(())
}
fn construct_capture_method_node(
builder: &mut cgraph::ConstraintGraphBuilder<'_, dir::DirValue>,
payment_method_filters: &settings::PaymentMethodFilters,
payment_method_type: &api_enums::PaymentMethodType,
) -> Result<Option<cgraph::NodeId>, KgraphError> {
if !payment_method_filters
.0
.get(&settings::PaymentMethodFilterKey::PaymentMethodType(
*payment_method_type,
))
.and_then(|v| v.not_available_flows)
.and_then(|v| v.capture_method)
.map(|v| !matches!(v, api_enums::CaptureMethod::Manual))
.unwrap_or(true)
{
return Ok(Some(builder.make_value_node(
cgraph::NodeValue::Value(dir::DirValue::CaptureMethod(
common_enums::CaptureMethod::Manual,
)),
None,
None::<()>,
)));
}
Ok(None)
}
fn construct_supported_connectors_for_update_mandate_node(
builder: &mut cgraph::ConstraintGraphBuilder<'_, dir::DirValue>,
builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
domain_id: cgraph::DomainId,
supported_payment_methods_for_update_mandate: &settings::SupportedPaymentMethodsForMandate,
pmt: RequestPaymentMethodTypes,
payment_method: &enums::PaymentMethod,
@ -386,7 +371,7 @@ fn construct_supported_connectors_for_update_mandate_node(
],
None,
None::<()>,
None,
Some(domain_id),
)
.map_err(KgraphError::GraphConstructionError)?;
@ -442,7 +427,7 @@ fn construct_supported_connectors_for_update_mandate_node(
],
None,
None::<()>,
None,
Some(domain_id),
)
.map_err(KgraphError::GraphConstructionError)?;
@ -460,14 +445,15 @@ fn construct_supported_connectors_for_update_mandate_node(
&agg_nodes,
Some("any node for card and non card pm"),
None::<()>,
None,
Some(domain_id),
)
.map_err(KgraphError::GraphConstructionError)?,
))
}
fn construct_supported_connectors_for_mandate_node(
builder: &mut cgraph::ConstraintGraphBuilder<'_, dir::DirValue>,
builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
domain_id: cgraph::DomainId,
eligible_connectors: Vec<api_enums::Connector>,
) -> Result<Option<cgraph::NodeId>, KgraphError> {
let payment_type_value_node = builder.make_value_node(
@ -516,15 +502,41 @@ fn construct_supported_connectors_for_mandate_node(
],
None,
None::<()>,
None,
Some(domain_id),
)
.map_err(KgraphError::GraphConstructionError)?,
))
}
}
fn construct_capture_method_node(
builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
payment_method_filters: &settings::PaymentMethodFilters,
payment_method_type: &api_enums::PaymentMethodType,
) -> Result<Option<cgraph::NodeId>, KgraphError> {
if !payment_method_filters
.0
.get(&settings::PaymentMethodFilterKey::PaymentMethodType(
*payment_method_type,
))
.and_then(|v| v.not_available_flows)
.and_then(|v| v.capture_method)
.map(|v| !matches!(v, api_enums::CaptureMethod::Manual))
.unwrap_or(true)
{
return Ok(Some(builder.make_value_node(
cgraph::NodeValue::Value(dir::DirValue::CaptureMethod(
common_enums::CaptureMethod::Manual,
)),
None,
None::<()>,
)));
}
Ok(None)
}
// fn construct_card_network_nodes(
// builder: &mut cgraph::ConstraintGraphBuilder<'_, dir::DirValue>,
// builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
// mca_card_networks: Vec<api_enums::CardNetwork>,
// ) -> Result<Option<cgraph::NodeId>, KgraphError> {
// Ok(Some(
@ -542,7 +554,8 @@ fn construct_supported_connectors_for_mandate_node(
// }
fn compile_accepted_countries_for_mca(
builder: &mut cgraph::ConstraintGraphBuilder<'_, dir::DirValue>,
builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
domain_id: cgraph::DomainId,
payment_method_type: &enums::PaymentMethodType,
pm_countries: Option<admin::AcceptedCountries>,
config: &settings::ConnectorFilters,
@ -571,7 +584,7 @@ fn compile_accepted_countries_for_mca(
agg_nodes.push((
pm_object_country_value_node,
cgraph::Relation::Positive,
cgraph::Strength::Strong,
cgraph::Strength::Weak,
));
}
admin::AcceptedCountries::DisableOnly(countries) => {
@ -592,7 +605,7 @@ fn compile_accepted_countries_for_mca(
agg_nodes.push((
pm_object_country_value_node,
cgraph::Relation::Positive,
cgraph::Strength::Strong,
cgraph::Strength::Weak,
));
}
admin::AcceptedCountries::AllAccepted => return Ok(None),
@ -600,12 +613,13 @@ fn compile_accepted_countries_for_mca(
}
// country from config
if let Some(config) = config
if let Some(derived_config) = config
.0
.get(connector.as_str())
.or_else(|| config.0.get("default"))
{
if let Some(value) = config
if let Some(value) =
derived_config
.0
.get(&settings::PaymentMethodFilterKey::PaymentMethodType(
*payment_method_type,
@ -628,20 +642,51 @@ fn compile_accepted_countries_for_mca(
agg_nodes.push((
config_country_agg_node,
cgraph::Relation::Positive,
cgraph::Strength::Strong,
cgraph::Strength::Weak,
));
}
} else if let Some(default_derived_config) = config.0.get("default") {
if let Some(value) =
default_derived_config
.0
.get(&settings::PaymentMethodFilterKey::PaymentMethodType(
*payment_method_type,
))
{
if let Some(config_countries) = value.country.as_ref() {
let config_countries: Vec<common_enums::Country> =
Vec::from_iter(config_countries)
.into_iter()
.map(|country| common_enums::Country::from_alpha2(*country))
.collect();
let dir_countries: Vec<dir::DirValue> = config_countries
.into_iter()
.map(dir::DirValue::BillingCountry)
.collect();
let config_country_agg_node = builder
.make_in_aggregator(dir_countries, None, None::<()>)
.map_err(KgraphError::GraphConstructionError)?;
agg_nodes.push((
config_country_agg_node,
cgraph::Relation::Positive,
cgraph::Strength::Weak,
));
}
}
};
}
Ok(Some(
builder
.make_all_aggregator(&agg_nodes, None, None::<()>, None)
.make_all_aggregator(&agg_nodes, None, None::<()>, Some(domain_id))
.map_err(KgraphError::GraphConstructionError)?,
))
}
fn compile_accepted_currency_for_mca(
builder: &mut cgraph::ConstraintGraphBuilder<'_, dir::DirValue>,
builder: &mut cgraph::ConstraintGraphBuilder<dir::DirValue>,
domain_id: cgraph::DomainId,
payment_method_type: &enums::PaymentMethodType,
pm_currency: Option<admin::AcceptedCurrencies>,
config: &settings::ConnectorFilters,
@ -665,7 +710,7 @@ fn compile_accepted_currency_for_mca(
agg_nodes.push((
pm_object_currency_value_node,
cgraph::Relation::Positive,
cgraph::Strength::Strong,
cgraph::Strength::Weak,
));
}
admin::AcceptedCurrencies::DisableOnly(currency) => {
@ -682,20 +727,21 @@ fn compile_accepted_currency_for_mca(
agg_nodes.push((
pm_object_currency_value_node,
cgraph::Relation::Positive,
cgraph::Strength::Strong,
cgraph::Strength::Weak,
));
}
admin::AcceptedCurrencies::AllAccepted => return Ok(None),
}
}
// country from config
if let Some(config) = config
// currency from config
if let Some(derived_config) = config
.0
.get(connector.as_str())
.or_else(|| config.0.get("default"))
{
if let Some(value) = config
if let Some(value) =
derived_config
.0
.get(&settings::PaymentMethodFilterKey::PaymentMethodType(
*payment_method_type,
@ -720,14 +766,45 @@ fn compile_accepted_currency_for_mca(
agg_nodes.push((
config_currency_agg_node,
cgraph::Relation::Positive,
cgraph::Strength::Strong,
cgraph::Strength::Weak,
));
}
} else if let Some(default_derived_config) = config.0.get("default") {
if let Some(value) =
default_derived_config
.0
.get(&settings::PaymentMethodFilterKey::PaymentMethodType(
*payment_method_type,
))
{
if let Some(config_currencies) = value.currency.as_ref() {
let config_currency: Vec<common_enums::Currency> =
Vec::from_iter(config_currencies)
.into_iter()
.cloned()
.collect();
let dir_currencies: Vec<dir::DirValue> = config_currency
.into_iter()
.map(dir::DirValue::PaymentCurrency)
.collect();
let config_currency_agg_node = builder
.make_in_aggregator(dir_currencies, None, None::<()>)
.map_err(KgraphError::GraphConstructionError)?;
agg_nodes.push((
config_currency_agg_node,
cgraph::Relation::Positive,
cgraph::Strength::Weak,
))
}
}
};
}
Ok(Some(
builder
.make_all_aggregator(&agg_nodes, None, None::<()>, None)
.make_all_aggregator(&agg_nodes, None, None::<()>, Some(domain_id))
.map_err(KgraphError::GraphConstructionError)?,
))
}

View File

@ -538,6 +538,9 @@ impl MerchantConnectorAccountInterface for Store {
cache::CacheKind::CGraph(
format!("cgraph_{}_{_profile_id}", _merchant_id).into(),
),
cache::CacheKind::PmFiltersCGraph(
format!("pm_filters_cgraph_{}_{_profile_id}", _merchant_id).into(),
),
],
update_call,
)
@ -595,6 +598,9 @@ impl MerchantConnectorAccountInterface for Store {
cache::CacheKind::CGraph(
format!("cgraph_{}_{_profile_id}", mca.merchant_id).into(),
),
cache::CacheKind::PmFiltersCGraph(
format!("pm_filters_cgraph_{}_{_profile_id}", mca.merchant_id).into(),
),
],
delete_call,
)

View File

@ -11,6 +11,7 @@ pub fn spawn_metrics_collector(metrics_collection_interval_in_secs: &Option<u16>
&cache::ACCOUNTS_CACHE,
&cache::ROUTING_CACHE,
&cache::CGRAPH_CACHE,
&cache::PM_FILTERS_CGRAPH_CACHE,
&cache::DECISION_MANAGER_CACHE,
&cache::SURCHARGE_CACHE,
];

View File

@ -44,6 +44,9 @@ const CGRAPH_CACHE_PREFIX: &str = "cgraph";
/// Prefix for all kinds of cache key
const ALL_CACHE_PREFIX: &str = "all_cache_kind";
/// Prefix for PM Filter cgraph cache key
const PM_FILTERS_CGRAPH_CACHE_PREFIX: &str = "pm_filters_cgraph";
/// Time to live 30 mins
const CACHE_TTL: u64 = 30 * 60;
@ -83,6 +86,16 @@ pub static SURCHARGE_CACHE: Lazy<Cache> =
pub static CGRAPH_CACHE: Lazy<Cache> =
Lazy::new(|| Cache::new("CGRAPH_CACHE", CACHE_TTL, CACHE_TTI, Some(MAX_CAPACITY)));
/// PM Filter CGraph Cache
pub static PM_FILTERS_CGRAPH_CACHE: Lazy<Cache> = Lazy::new(|| {
Cache::new(
"PM_FILTERS_CGRAPH_CACHE",
CACHE_TTL,
CACHE_TTI,
Some(MAX_CAPACITY),
)
});
/// Trait which defines the behaviour of types that's gonna be stored in Cache
pub trait Cacheable: Any + Send + Sync + DynClone {
fn as_any(&self) -> &dyn Any;
@ -95,6 +108,7 @@ pub enum CacheKind<'a> {
DecisionManager(Cow<'a, str>),
Surcharge(Cow<'a, str>),
CGraph(Cow<'a, str>),
PmFiltersCGraph(Cow<'a, str>),
All(Cow<'a, str>),
}
@ -107,6 +121,7 @@ impl<'a> From<CacheKind<'a>> for RedisValue {
CacheKind::DecisionManager(s) => format!("{DECISION_MANAGER_CACHE_PREFIX},{s}"),
CacheKind::Surcharge(s) => format!("{SURCHARGE_CACHE_PREFIX},{s}"),
CacheKind::CGraph(s) => format!("{CGRAPH_CACHE_PREFIX},{s}"),
CacheKind::PmFiltersCGraph(s) => format!("{PM_FILTERS_CGRAPH_CACHE_PREFIX},{s}"),
CacheKind::All(s) => format!("{ALL_CACHE_PREFIX},{s}"),
};
Self::from_string(value)
@ -130,6 +145,9 @@ impl<'a> TryFrom<RedisValue> for CacheKind<'a> {
}
SURCHARGE_CACHE_PREFIX => Ok(Self::Surcharge(Cow::Owned(split.1.to_string()))),
CGRAPH_CACHE_PREFIX => Ok(Self::CGraph(Cow::Owned(split.1.to_string()))),
PM_FILTERS_CGRAPH_CACHE_PREFIX => {
Ok(Self::PmFiltersCGraph(Cow::Owned(split.1.to_string())))
}
ALL_CACHE_PREFIX => Ok(Self::All(Cow::Owned(split.1.to_string()))),
_ => Err(validation_err.into()),
}

View File

@ -6,7 +6,7 @@ use router_env::{logger, tracing::Instrument};
use crate::redis::cache::{
CacheKey, CacheKind, ACCOUNTS_CACHE, CGRAPH_CACHE, CONFIG_CACHE, DECISION_MANAGER_CACHE,
ROUTING_CACHE, SURCHARGE_CACHE,
PM_FILTERS_CGRAPH_CACHE, ROUTING_CACHE, SURCHARGE_CACHE,
};
#[async_trait::async_trait]
@ -120,6 +120,15 @@ impl PubSubInterface for std::sync::Arc<redis_interface::RedisConnectionPool> {
.await;
key
}
CacheKind::PmFiltersCGraph(key) => {
PM_FILTERS_CGRAPH_CACHE
.remove(CacheKey {
key: key.to_string(),
prefix: self.key_prefix.clone(),
})
.await;
key
}
CacheKind::Routing(key) => {
ROUTING_CACHE
.remove(CacheKey {
@ -166,6 +175,12 @@ impl PubSubInterface for std::sync::Arc<redis_interface::RedisConnectionPool> {
prefix: self.key_prefix.clone(),
})
.await;
PM_FILTERS_CGRAPH_CACHE
.remove(CacheKey {
key: key.to_string(),
prefix: self.key_prefix.clone(),
})
.await;
ROUTING_CACHE
.remove(CacheKey {
key: key.to_string(),

View File

@ -0,0 +1,428 @@
import apiKeyCreateBody from "../../fixtures/create-api-key-body.json";
import createConnectorBody from "../../fixtures/create-connector-body.json";
import getConnectorDetails from "../PaymentMethodListUtils/utils";
import merchantCreateBody from "../../fixtures/merchant-create-body.json";
import * as utils from "../PaymentMethodListUtils/Utils";
import {
card_credit_enabled,
card_credit_enabled_in_US,
card_credit_enabled_in_USD,
bank_redirect_ideal_enabled,
bank_redirect_ideal_and_credit_enabled,
create_payment_body_with_currency,
create_payment_body_with_currency_country,
} from "../PaymentMethodListUtils/Commons";
import State from "../../utils/State";
// Testing for scenario:
// MCA1 -> Stripe configured with ideal = { country = "NL", currency = "EUR" }
// MCA2 -> Cybersource configured with credit = { currency = "USD" }
// Payment is done with currency as EUR and no billing address
// The resultant Payment Method list should only have ideal with stripe
let globalState;
describe("Payment Method list using Constraint Graph flow tests", () => {
// Testing for scenario:
// MCA1 -> Stripe configured with ideal = { country = "NL", currency = "EUR" }
// MCA2 -> Cybersource configured with credit = { currency = "USD" }
// Payment is done with currency as EUR and no billing address
// The resultant Payment Method list should only have ideal with stripe
context(
`
MCA1 -> Stripe configured with ideal = { country = "NL", currency = "EUR" }\n
MCA2 -> Cybersource configured with credit = { currency = "USD" }\n
Payment is done with currency as EUR and no billing address\n
The resultant Payment Method list should only have ideal with stripe\n
`,
() => {
before("seed global state", () => {
cy.task("getGlobalState").then((state) => {
globalState = new State(state);
});
});
after("flush global state", () => {
cy.task("setGlobalState", globalState.data);
});
it("merchant-create-call-test", () => {
cy.merchantCreateCallTest(merchantCreateBody, globalState);
});
it("api-key-create-call-test", () => {
cy.apiKeyCreateTest(apiKeyCreateBody, globalState);
});
// stripe connector create with ideal enabled
it("connector-create-call-test", () => {
cy.createNamedConnectorCallTest(
createConnectorBody,
bank_redirect_ideal_enabled,
globalState,
"stripe"
);
});
// cybersource connector create with card credit enabled
it("connector-create-call-test", () => {
cy.createNamedConnectorCallTest(
createConnectorBody,
card_credit_enabled,
globalState,
"cybersource"
);
});
// creating payment with currency as EUR and no billing address
it("create-payment-call-test", () => {
let data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"];
let req_data = data["RequestCurrencyEUR"];
let res_data = data["Response"];
cy.createPaymentIntentTest(
create_payment_body_with_currency("EUR"),
req_data,
res_data,
"no_three_ds",
"automatic",
globalState
);
});
// payment method list which should only have ideal with stripe
it("payment-method-list-call-test", () => {
let data = getConnectorDetails(globalState.get("connectorId"))[
"pm_list"
]["PmListResponse"]["PmListWithStripeForIdeal"];
cy.paymentMethodListTestLessThanEqualToOnePaymentMethod(
data,
globalState
);
});
}
);
// Testing for scenario:
// MCA1 -> Stripe configured with ideal = { country = "NL", currency = "EUR" }
// MCA2 -> Cybersource configured with credit = { currency = "USD" }
// Payment is done with currency as INR and no billing address
// The resultant Payment Method list shouldn't have any payment method
context(
`
MCA1 -> Stripe configured with ideal = { country = "NL", currency = "EUR" }\n
MCA2 -> Cybersource configured with credit = { currency = "USD" }\n
Payment is done with currency as INR and no billing address\n
The resultant Payment Method list shouldn't have any payment method\n
`,
() => {
before("seed global state", () => {
cy.task("getGlobalState").then((state) => {
globalState = new State(state);
});
});
after("flush global state", () => {
cy.task("setGlobalState", globalState.data);
});
it("merchant-create-call-test", () => {
cy.merchantCreateCallTest(merchantCreateBody, globalState);
});
it("api-key-create-call-test", () => {
cy.apiKeyCreateTest(apiKeyCreateBody, globalState);
});
// stripe connector create with ideal enabled
it("connector-create-call-test", () => {
cy.createNamedConnectorCallTest(
createConnectorBody,
bank_redirect_ideal_enabled,
globalState,
"stripe"
);
});
// cybersource connector create with card credit enabled in USD
it("connector-create-call-test", () => {
cy.createNamedConnectorCallTest(
createConnectorBody,
card_credit_enabled_in_USD,
globalState,
"cybersource"
);
});
// creating payment with currency as INR and no billing address
it("create-payment-call-test", () => {
let data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"];
let req_data = data["RequestCurrencyINR"];
let res_data = data["Response"];
cy.createPaymentIntentTest(
create_payment_body_with_currency("INR"),
req_data,
res_data,
"no_three_ds",
"automatic",
globalState
);
});
// payment method list which should only have ideal with stripe
it("payment-method-list-call-test", () => {
let data = getConnectorDetails(globalState.get("connectorId"))[
"pm_list"
]["PmListResponse"]["PmListNull"];
cy.paymentMethodListTestLessThanEqualToOnePaymentMethod(
data,
globalState
);
});
}
);
// Testing for scenario:
// MCA1 -> Stripe configured with credit = { country = "US" }
// MCA2 -> Cybersource configured with credit = { country = "US" }
// Payment is done with country as US and currency as USD
// The resultant Payment Method list should have both Stripe and cybersource
context(
`
MCA1 -> Stripe configured with credit = { country = "US" }\n
MCA2 -> Cybersource configured with credit = { country = "US" }\n
Payment is done with country as US and currency as USD\n
The resultant Payment Method list should have both Stripe and Cybersource\n
`,
() => {
before("seed global state", () => {
cy.task("getGlobalState").then((state) => {
globalState = new State(state);
});
});
after("flush global state", () => {
cy.task("setGlobalState", globalState.data);
});
it("merchant-create-call-test", () => {
cy.merchantCreateCallTest(merchantCreateBody, globalState);
});
it("api-key-create-call-test", () => {
cy.apiKeyCreateTest(apiKeyCreateBody, globalState);
});
// stripe connector create with credit enabled for US
it("connector-create-call-test", () => {
cy.createNamedConnectorCallTest(
createConnectorBody,
card_credit_enabled_in_US,
globalState,
"stripe"
);
});
// cybersource connector create with card credit enabled in US
it("connector-create-call-test", () => {
cy.createNamedConnectorCallTest(
createConnectorBody,
card_credit_enabled_in_US,
globalState,
"cybersource"
);
});
// creating payment with currency as USD and billing address as US
it("create-payment-call-test", () => {
let data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"];
let req_data = data["RequestCurrencyUSD"];
let res_data = data["Response"];
cy.createPaymentIntentTest(
create_payment_body_with_currency("USD"),
req_data,
res_data,
"no_three_ds",
"automatic",
globalState
);
});
// payment method list which should only have credit with Stripe and Cybersource
it("payment-method-list-call-test", () => {
let data = getConnectorDetails(globalState.get("connectorId"))[
"pm_list"
]["PmListResponse"]["PmListWithCreditTwoConnector"];
cy.paymentMethodListTestTwoConnectorsForOnePaymentMethodCredit(
data,
globalState
);
});
}
);
// Testing for scenario:
// MCA1 -> Stripe configured with ideal = { country = "NL", currency = "EUR" }
// MCA2 -> Cybersource configured with ideal = { country = "NL", currency = "EUR" }
// Payment is done with country as US and currency as EUR
// The resultant Payment Method list shouldn't have anything
context(
`
MCA1 -> Stripe configured with ideal = { country = "NL", currency = "EUR" }\n
MCA2 -> Cybersource configured with ideal = { country = "NL", currency = "EUR" }\n
Payment is done with country as US and currency as EUR\n
The resultant Payment Method list shouldn't have anything\n
`,
() => {
before("seed global state", () => {
cy.task("getGlobalState").then((state) => {
globalState = new State(state);
});
});
after("flush global state", () => {
cy.task("setGlobalState", globalState.data);
});
it("merchant-create-call-test", () => {
cy.merchantCreateCallTest(merchantCreateBody, globalState);
});
it("api-key-create-call-test", () => {
cy.apiKeyCreateTest(apiKeyCreateBody, globalState);
});
// stripe connector create with ideal enabled
it("connector-create-call-test", () => {
cy.createNamedConnectorCallTest(
createConnectorBody,
bank_redirect_ideal_enabled,
globalState,
"stripe"
);
});
// cybersource connector create with ideal enabled
it("connector-create-call-test", () => {
cy.createNamedConnectorCallTest(
createConnectorBody,
bank_redirect_ideal_enabled,
globalState,
"cybersource"
);
});
// creating payment with currency as EUR and billing address as US
it("create-payment-call-test", () => {
let data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"];
let req_data = data["RequestCurrencyEUR"];
let res_data = data["Response"];
cy.createPaymentIntentTest(
create_payment_body_with_currency_country("EUR", "US"),
req_data,
res_data,
"no_three_ds",
"automatic",
globalState
);
});
// payment method list which shouldn't have anything
it("payment-method-list-call-test", () => {
let data = getConnectorDetails(globalState.get("connectorId"))[
"pm_list"
]["PmListResponse"]["PmListNull"];
cy.paymentMethodListTestLessThanEqualToOnePaymentMethod(
data,
globalState
);
});
}
);
// Testing for scenario:
// MCA1 -> Stripe configured with card credit no configs present
// MCA1 -> Cybersource configured with card credit = { currency = "USD" }
// and ideal (default config present as = { country = "NL", currency = "EUR" } )
// Payment is done with country as IN and currency as USD
// The resultant Payment Method list should have
// Stripe and cybersource both for credit and none for ideal
context(
`
MCA1 -> Stripe configured with card credit no configs present\n
MCA2 -> Cybersource configured with card credit = { currency = "USD" }\n
and ideal (default config present as = { country = "NL", currency = "EUR" })\n
Payment is done with country as IN and currency as USD\n
The resultant Payment Method list should have\n
Stripe and Cybersource both for credit and none for ideal\n
`,
() => {
before("seed global state", () => {
cy.task("getGlobalState").then((state) => {
globalState = new State(state);
});
});
after("flush global state", () => {
cy.task("setGlobalState", globalState.data);
});
it("merchant-create-call-test", () => {
cy.merchantCreateCallTest(merchantCreateBody, globalState);
});
it("api-key-create-call-test", () => {
cy.apiKeyCreateTest(apiKeyCreateBody, globalState);
});
// stripe connector create with card credit enabled
it("connector-create-call-test", () => {
cy.createNamedConnectorCallTest(
createConnectorBody,
card_credit_enabled,
globalState,
"stripe"
);
});
// cybersource connector create with card credit and ideal enabled
it("connector-create-call-test", () => {
cy.createNamedConnectorCallTest(
createConnectorBody,
bank_redirect_ideal_and_credit_enabled,
globalState,
"cybersource"
);
});
// creating payment with currency as USD and billing address as IN
it("create-payment-call-test", () => {
let data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"];
let req_data = data["RequestCurrencyUSD"];
let res_data = data["Response"];
cy.createPaymentIntentTest(
create_payment_body_with_currency_country("USD", "IN"),
req_data,
res_data,
"no_three_ds",
"automatic",
globalState
);
});
// payment method list which should have credit with stripe and cybersource and no ideal
it("payment-method-list-call-test", () => {
let data = getConnectorDetails(globalState.get("connectorId"))[
"pm_list"
]["PmListResponse"]["PmListWithCreditTwoConnector"];
cy.paymentMethodListTestTwoConnectorsForOnePaymentMethodCredit(
data,
globalState
);
});
}
);
});

View File

@ -0,0 +1,179 @@
import State from "../../utils/State";
const globalState = new State({
connectorId: Cypress.env("CONNECTOR"),
baseUrl: Cypress.env("BASEURL"),
adminApiKey: Cypress.env("ADMINAPIKEY"),
connectorAuthFilePath: Cypress.env("CONNECTOR_AUTH_FILE_PATH"),
});
export const card_credit_enabled = [
{
payment_method: "card",
payment_method_types: [
{
payment_method_type: "credit",
card_networks: ["Visa"],
minimum_amount: 0,
maximum_amount: 68607706,
recurring_enabled: false,
installment_payment_enabled: true,
},
],
},
];
export const card_credit_enabled_in_USD = [
{
payment_method: "card",
payment_method_types: [
{
payment_method_type: "credit",
card_networks: ["Visa"],
minimum_amount: 0,
accepted_currencies: {
type: "enable_only",
list: ["USD"],
},
maximum_amount: 68607706,
recurring_enabled: false,
installment_payment_enabled: true,
},
],
},
];
export const card_credit_enabled_in_US = [
{
payment_method: "card",
payment_method_types: [
{
payment_method_type: "credit",
card_networks: ["Visa"],
minimum_amount: 0,
accepted_countries: {
type: "enable_only",
list: ["US"],
},
maximum_amount: 68607706,
recurring_enabled: false,
installment_payment_enabled: true,
},
],
},
];
export const bank_redirect_ideal_enabled = [
{
payment_method: "bank_redirect",
payment_method_types: [
{
payment_method_type: "ideal",
payment_experience: null,
card_networks: null,
accepted_countries: null,
minimum_amount: 0,
maximum_amount: 68607706,
recurring_enabled: true,
installment_payment_enabled: false,
},
],
},
];
export const bank_redirect_ideal_and_credit_enabled = [
{
payment_method: "card",
payment_method_types: [
{
payment_method_type: "credit",
card_networks: ["Visa"],
minimum_amount: 0,
maximum_amount: 68607706,
recurring_enabled: false,
installment_payment_enabled: true,
},
],
},
{
payment_method: "bank_redirect",
payment_method_types: [
{
payment_method_type: "ideal",
payment_experience: null,
card_networks: null,
accepted_countries: null,
minimum_amount: 0,
maximum_amount: 68607706,
recurring_enabled: true,
installment_payment_enabled: false,
},
],
},
];
export const create_payment_body_with_currency_country = (
currency,
billingCountry
) => ({
currency: currency,
amount: 6500,
authentication_type: "three_ds",
description: "Joseph First Crypto",
email: "hyperswitch_sdk_demo_id@gmail.com",
setup_future_usage: "",
metadata: {
udf1: "value1",
new_customer: "true",
login_date: "2019-09-10T10:11:12Z",
},
business_label: "default",
billing: {
address: {
line1: "1946",
line2: "Gandhi Nagar",
line3: "Ramnagar",
city: "Ranchi",
state: "Jharkhand",
zip: "827013",
country: billingCountry, // Billing country from parameter
first_name: "Joseph",
last_name: "Doe",
},
phone: {
number: "8056594427",
country_code: "+91",
},
email: "example@example.com",
},
});
export const create_payment_body_with_currency = (currency) => ({
currency: currency,
amount: 6500,
authentication_type: "three_ds",
description: "Joseph First Crypto",
email: "hyperswitch_sdk_demo_id@gmail.com",
setup_future_usage: "",
metadata: {
udf1: "value1",
new_customer: "true",
login_date: "2019-09-10T10:11:12Z",
},
business_label: "default",
});
export const create_payment_body_in_USD = {
currency: "USD",
amount: 6500,
authentication_type: "three_ds",
description: "Joseph First Crypto",
email: "hyperswitch_sdk_demo_id@gmail.com",
setup_future_usage: "",
metadata: {
udf1: "value1",
new_customer: "true",
login_date: "2019-09-10T10:11:12Z",
},
business_label: "default",
};

View File

@ -0,0 +1,99 @@
const successfulNo3DSCardDetails = {
card_number: "4242424242424242",
card_exp_month: "10",
card_exp_year: "25",
card_holder_name: "morino",
card_cvc: "737",
};
export const connectorDetails = {
pm_list: {
PaymentIntent: {
RequestCurrencyUSD: {
payment_method_data: {
card: successfulNo3DSCardDetails,
},
currency: "USD",
customer_acceptance: null,
setup_future_usage: "off_session",
authentication_type: "no_three_ds",
},
RequestCurrencyEUR: {
payment_method_data: {
card: successfulNo3DSCardDetails,
},
currency: "EUR",
customer_acceptance: null,
setup_future_usage: "off_session",
authentication_type: "no_three_ds",
},
RequestCurrencyINR: {
payment_method_data: {
card: successfulNo3DSCardDetails,
},
currency: "INR",
customer_acceptance: null,
setup_future_usage: "off_session",
authentication_type: "no_three_ds",
},
Response: {
status: 200,
body: {
status: "requires_payment_method",
},
},
},
PmListResponse: {
PmListNull: {
payment_methods: [],
},
PmListWithStripeForIdeal: {
status: "requires_payment_method",
payment_methods: [
{
payment_method: "bank_redirect",
payment_method_types: [
{
payment_method_type: "ideal",
bank_names: [
{
eligible_connectors: ["stripe"],
},
],
},
],
},
],
},
PmListWithCreditOneConnector: {
payment_methods: [
{
payment_method: "card",
payment_method_types: [
{
payment_method_type: "credit",
},
],
},
],
},
PmListWithCreditTwoConnector: {
payment_methods: [
{
payment_method: "card",
payment_method_types: [
{
payment_method_type: "credit",
card_networks: [
{
eligible_connectors: ["stripe", "cybersource"],
},
],
},
],
},
],
},
},
},
};

View File

@ -0,0 +1,41 @@
import { connectorDetails as CommonConnectorDetails } from "./Commons.js";
import { connectorDetails as stripeConnectorDetails } from "./Stripe.js";
const connectorDetails = {
commons: CommonConnectorDetails,
stripe: stripeConnectorDetails,
};
export default function getConnectorDetails(connectorId) {
let x = mergeDetails(connectorId);
return x;
}
function mergeDetails(connectorId) {
const connectorData = getValueByKey(connectorDetails, connectorId);
return connectorData;
}
function getValueByKey(jsonObject, key) {
const data =
typeof jsonObject === "string" ? JSON.parse(jsonObject) : jsonObject;
if (data && typeof data === "object" && key in data) {
return data[key];
} else {
return null;
}
}
export const should_continue_further = (res_data) => {
if (
res_data.body.error !== undefined ||
res_data.body.error_code !== undefined ||
res_data.body.error_message !== undefined
) {
return false;
} else {
return true;
}
};

View File

@ -80,6 +80,53 @@ Cypress.Commands.add("apiKeyCreateTest", (apiKeyCreateBody, globalState) => {
});
});
Cypress.Commands.add(
"createNamedConnectorCallTest",
(
createConnectorBody,
payment_methods_enabled,
globalState,
connectorName
) => {
const merchantId = globalState.get("merchantId");
createConnectorBody.connector_name = connectorName;
createConnectorBody.payment_methods_enabled = payment_methods_enabled;
// readFile is used to read the contents of the file and it always returns a promise ([Object Object]) due to its asynchronous nature
// it is best to use then() to handle the response within the same block of code
cy.readFile(globalState.get("connectorAuthFilePath")).then(
(jsonContent) => {
const authDetails = getValueByKey(
JSON.stringify(jsonContent),
connectorName
);
createConnectorBody.connector_account_details = authDetails;
cy.request({
method: "POST",
url: `${globalState.get("baseUrl")}/account/${merchantId}/connectors`,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"api-key": globalState.get("adminApiKey"),
},
body: createConnectorBody,
failOnStatusCode: false,
}).then((response) => {
logRequestId(response.headers["x-request-id"]);
if (response.status === 200) {
expect(connectorName).to.equal(response.body.connector_name);
} else {
cy.task(
"cli_log",
"response status -> " + JSON.stringify(response.status)
);
}
});
}
);
}
);
Cypress.Commands.add(
"createConnectorCallTest",
(createConnectorBody, payment_methods_enabled, globalState) => {
@ -215,6 +262,82 @@ Cypress.Commands.add(
}
);
Cypress.Commands.add(
"paymentMethodListTestLessThanEqualToOnePaymentMethod",
(res_data, globalState) => {
cy.request({
method: "GET",
url: `${globalState.get("baseUrl")}/account/payment_methods?client_secret=${globalState.get("clientSecret")}`,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"api-key": globalState.get("publishableKey"),
},
failOnStatusCode: false,
}).then((response) => {
logRequestId(response.headers["x-request-id"]);
expect(response.headers["content-type"]).to.include("application/json");
if (response.status === 200) {
expect(response.body).to.have.property("currency");
if (res_data["payment_methods"].length == 1) {
function getPaymentMethodType(obj) {
return obj["payment_methods"][0]["payment_method_types"][0][
"payment_method_type"
];
}
expect(getPaymentMethodType(res_data)).to.equal(
getPaymentMethodType(response.body)
);
} else {
expect(0).to.equal(response.body["payment_methods"].length);
}
} else {
defaultErrorHandler(response, res_data);
}
});
}
);
Cypress.Commands.add(
"paymentMethodListTestTwoConnectorsForOnePaymentMethodCredit",
(res_data, globalState) => {
cy.request({
method: "GET",
url: `${globalState.get("baseUrl")}/account/payment_methods?client_secret=${globalState.get("clientSecret")}`,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"api-key": globalState.get("publishableKey"),
},
failOnStatusCode: false,
}).then((response) => {
logRequestId(response.headers["x-request-id"]);
expect(response.headers["content-type"]).to.include("application/json");
if (response.status === 200) {
expect(response.body).to.have.property("currency");
if (res_data["payment_methods"].length > 0) {
function getPaymentMethodType(obj) {
return obj["payment_methods"][0]["payment_method_types"][0][
"card_networks"
][0]["eligible_connectors"]
.slice()
.sort();
}
for (let i = 0; i < getPaymentMethodType(response.body).length; i++) {
expect(getPaymentMethodType(res_data)[i]).to.equal(
getPaymentMethodType(response.body)[i]
);
}
} else {
expect(0).to.equal(response.body["payment_methods"].length);
}
} else {
defaultErrorHandler(response, res_data);
}
});
}
);
Cypress.Commands.add(
"createPaymentIntentTest",
(

View File

@ -12,7 +12,7 @@
"prettier": "^3.3.2"
},
"devDependencies": {
"cypress": "^13.10.0",
"cypress": "^13.12.0",
"jsqr": "^1.4.0"
}
},