diff --git a/.typos.toml b/.typos.toml index 4e663dc448..b52d93dcb1 100644 --- a/.typos.toml +++ b/.typos.toml @@ -8,10 +8,11 @@ PaymentVas = "PaymentVas" HypoNoeLbFurNiederosterreichUWien = "HypoNoeLbFurNiederosterreichUWien" [default.extend-words] -aci = "aci" # Name of a connector -encrypter = "encrypter" # Used by the `ring` crate -nin = "nin" # National identification number, a field used by PayU connector +aci = "aci" # Name of a connector +encrypter = "encrypter" # Used by the `ring` crate +nin = "nin" # National identification number, a field used by PayU connector substituters = "substituters" # Present in `flake.nix` +FO = "FO" # Faroe Islands (the) country code [files] extend-exclude = [ diff --git a/config/Development.toml b/config/Development.toml index 95e341e72a..996a06b775 100644 --- a/config/Development.toml +++ b/config/Development.toml @@ -140,3 +140,40 @@ stream = "SCHEDULER_STREAM" [scheduler.consumer] disabled = false consumer_group = "SCHEDULER_GROUP" + +[pm_filters.stripe] +google_pay = { country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" } +apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US" } +klarna = { country = "AT,BE,DK,FI,FR,DE,IE,IT,NL,NO,ES,SE,GB,US", currency = "EUR,USD,GBP,DKK,SEK,NOK" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "US,CA,GB,AU,NZ,FR,ES", currency = "USD,CAD,GBP,AUD,NZD,EUR" } +giropay = { country = "DE", currency = "EUR" } +eps = { country = "AT", currency = "EUR" } +sofort = { country = "AT,BE,DE,IT,NL,ES", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } + +[pm_filters.adyen] +google_pay = { country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" } +apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US", currency = "AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } +paypal = { currency = "AUD,BRL,CAD,CZK,DKK,EUR,HKD,HUF,INR,JPY,MYR,MXN,NZD,NOK,PHP,PLN,RUB,GBP,SGD,SEK,CHF,THB,USD" } +klarna = { country = "AT,BE,DK,FI,FR,DE,IE,IT,NL,NO,ES,SE,GB,US,CA", currency = "USD,GBP,EUR,CHF,DKK,SEK,NOK,AUD,PLN,CAD" } +affirm = { country = "US", currency = "USD" } +afterpay_clearpay = { country = "US,CA,GB,AU,NZ,FR,ES", currency = "GBP" } +giropay = { country = "DE", currency = "EUR" } +eps = { country = "AT", currency = "EUR" } +sofort = { country = "ES,GB,SE,AT,NL,DE,CH,BE,FR,FI,IT,PL", currency = "EUR" } +ideal = { country = "NL", currency = "EUR" } + +[pm_filters.braintree] +paypal = { currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,NZD,NOK,PHP,PLN,GBP,RUB,SGD,SEK,CHF,THB,USD" } + +[pm_filters.klarna] +klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "AUD,EUR,EUR,CAD,CZK,DKK,EUR,EUR,EUR,EUR,EUR,EUR,EUR,NZD,NOK,PLN,EUR,EUR,SEK,CHF,GBP,USD" } + +[pm_filters.authorizedotnet] +google_pay = { currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } +paypal = { currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } + +[pm_filters.worldpay] +google_pay = { country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" } +apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US" } diff --git a/config/config.example.toml b/config/config.example.toml index b6bfd1dd1c..2ee0c3c730 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -204,3 +204,11 @@ num_partitions = 64 # Specifies the number of partitions the stream w max_read_count = 100 # Specifies the maximum number of entries that would be read from redis stream in one call shutdown_interval = 1000 # Specifies how much time to wait, while waiting for threads to complete execution (in milliseconds) loop_interval = 500 # Specifies how much time to wait after checking all the possible streams in completed (in milliseconds) + +# Filteration logic for list payment method, allowing use to limit payment methods based on the requirement country and currency +[pm_filters.stripe] +# ^--- This can be any connector (can be multiple) +paypal = { currency = "USD,INR", country = "US" } +# ^ ^------- comma-separated values +# ^------------------------------- any valid payment method type (can be multiple) +# If either currency or country isn't provided then, all possible values are accepted diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index e6ba7a8c2f..87e0e5b159 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -1,10 +1,14 @@ -use std::path::PathBuf; +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, + str::FromStr, +}; use common_utils::ext_traits::ConfigExt; use config::{Environment, File}; use redis_interface::RedisSettings; pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry}; -use serde::Deserialize; +use serde::{Deserialize, Deserializer}; use crate::{ core::errors::{ApplicationError, ApplicationResult}, @@ -51,6 +55,64 @@ pub struct Settings { pub drainer: DrainerSettings, pub jwekey: Jwekey, pub webhooks: WebhooksSettings, + pub pm_filters: ConnectorFilters, +} + +#[derive(Debug, Deserialize, Clone, Default)] +#[serde(transparent)] +pub struct ConnectorFilters(pub HashMap); + +#[derive(Debug, Deserialize, Clone, Default)] +#[serde(transparent)] +pub struct PaymentMethodFilters( + pub HashMap, +); + +#[derive(Debug, Deserialize, Clone, Default)] +#[serde(default)] +pub struct CurrencyCountryFilter { + #[serde(deserialize_with = "currency_set_deser")] + pub currency: Option>, + #[serde(deserialize_with = "string_set_deser")] + pub country: Option>, +} + +fn string_set_deser<'a, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'a>, +{ + let value = >::deserialize(deserializer)?; + Ok(value.and_then(|inner| { + let list = inner + .trim() + .split(',') + .map(|value| value.to_string()) + .collect::>(); + match list.len() { + 0 => None, + _ => Some(list), + } + })) +} + +fn currency_set_deser<'a, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'a>, +{ + let value = >::deserialize(deserializer)?; + Ok(value.and_then(|inner| { + let list = inner + .trim() + .split(',') + .flat_map(api_models::enums::Currency::from_str) + .collect::>(); + match list.len() { + 0 => None, + _ => Some(list), + } + })) } #[derive(Debug, Deserialize, Clone)] diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 27251ee19e..435c641bcc 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -14,6 +14,7 @@ use error_stack::{report, ResultExt}; use router_env::{instrument, tracing}; use crate::{ + configs::settings, core::{ errors::{self, StorageErrorExt}, payment_methods::{transformers as payment_methods, vault}, @@ -348,10 +349,13 @@ pub async fn delete_card<'a>( } pub async fn list_payment_methods( - db: &dyn db::StorageInterface, + state: &routes::AppState, merchant_account: storage::MerchantAccount, mut req: api::ListPaymentMethodRequest, ) -> errors::RouterResponse { + let db = &*state.store; + let pm_config_mapping = &state.conf.pm_filters; + let payment_intent = helpers::verify_client_secret( db, merchant_account.storage_scheme, @@ -410,6 +414,7 @@ pub async fn list_payment_methods( payment_attempt.as_ref(), address.as_ref(), mca.connector_name, + pm_config_mapping, ) .await?; } @@ -567,6 +572,7 @@ pub async fn list_payment_methods( ))) } +#[allow(clippy::too_many_arguments)] async fn filter_payment_methods( payment_methods: Vec, req: &mut api::ListPaymentMethodRequest, @@ -575,6 +581,7 @@ async fn filter_payment_methods( payment_attempt: Option<&storage::PaymentAttempt>, address: Option<&storage::Address>, connector: String, + config: &settings::ConnectorFilters, ) -> errors::CustomResult<(), errors::ApiErrorResponse> { for payment_method in payment_methods.into_iter() { let parse_result = serde_json::from_value::(payment_method); @@ -627,6 +634,16 @@ async fn filter_payment_methods( true }; + let filter5 = filter_pm_based_on_config( + config, + &connector, + &payment_method_object.payment_method_type, + address.and_then(|inner| inner.country.clone()), + payment_attempt + .and_then(|value| value.currency) + .map(|value| value.foreign_into()), + ); + let connector = connector.clone(); let response_pm_type = ResponsePaymentMethodIntermediate::new( @@ -635,7 +652,7 @@ async fn filter_payment_methods( payment_method, ); - if filter && filter2 && filter3 && filter4 { + if filter && filter2 && filter3 && filter4 && filter5 { resp.push(response_pm_type); } } @@ -645,6 +662,33 @@ async fn filter_payment_methods( Ok(()) } +fn filter_pm_based_on_config<'a>( + config: &'a crate::configs::settings::ConnectorFilters, + connector: &'a str, + payment_method_type: &'a api_enums::PaymentMethodType, + country: Option, + currency: Option, +) -> bool { + config + .0 + .get(connector) + .and_then(|inner| inner.0.get(payment_method_type)) + .map(|value| { + let condition1 = value + .country + .as_ref() + .zip(country) + .map(|(lhs, rhs)| lhs.contains(&rhs)); + let condition2 = value + .currency + .as_ref() + .zip(currency) + .map(|(lhs, rhs)| lhs.contains(&rhs)); + condition1.unwrap_or(true) && condition2.unwrap_or(true) + }) + .unwrap_or(true) +} + fn filter_pm_card_network_based( pm_card_networks: Option<&Vec>, request_card_networks: Option<&Vec>, diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index ea957422f4..90dc4f07d5 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -82,9 +82,7 @@ pub async fn list_payment_method_api( state.get_ref(), &req, payload, - |state, merchant_account, req| { - cards::list_payment_methods(&*state.store, merchant_account, req) - }, + cards::list_payment_methods, &*auth, ) .await