diff --git a/Cargo.lock b/Cargo.lock index f65a0fd4be0..dd3644d580a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -459,6 +459,7 @@ dependencies = [ "common_enums", "common_types", "common_utils", + "deserialize_form_style_query_parameter", "error-stack 0.4.1", "euclid", "masking", @@ -2566,6 +2567,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "deserialize_form_style_query_parameter" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f5980bcca58eff485f9b5208f325e15cf5e6b23bfe878fae13f9576e6b6e62" +dependencies = [ + "serde", +] + [[package]] name = "deunicode" version = "1.6.1" diff --git a/api-reference/v2/openapi_spec_v2.json b/api-reference/v2/openapi_spec_v2.json index 64add77b784..605cd8bfcfd 100644 --- a/api-reference/v2/openapi_spec_v2.json +++ b/api-reference/v2/openapi_spec_v2.json @@ -12587,6 +12587,78 @@ } } }, + "ListMethodsForPaymentMethodsRequest": { + "type": "object", + "properties": { + "client_secret": { + "type": "string", + "description": "This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK", + "example": "secret_k2uj3he2893eiu2d", + "nullable": true, + "maxLength": 30, + "minLength": 30 + }, + "accepted_countries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CountryAlpha2" + }, + "description": "The two-letter ISO currency code", + "example": [ + "US", + "UK", + "IN" + ], + "nullable": true + }, + "amount": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "accepted_currencies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Currency" + }, + "description": "The three-letter ISO currency code", + "example": [ + "USD", + "EUR" + ], + "nullable": true + }, + "recurring_enabled": { + "type": "boolean", + "description": "Indicates whether the payment method supports recurring payments. Optional.", + "example": true, + "nullable": true + }, + "card_networks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CardNetwork" + }, + "description": "Indicates whether the payment method is eligible for card netwotks", + "example": [ + "visa", + "mastercard" + ], + "nullable": true + }, + "limit": { + "type": "integer", + "format": "int64", + "description": "Indicates the limit of last used payment methods", + "example": 1, + "nullable": true + } + }, + "additionalProperties": false + }, "LocalBankTransferAdditionalData": { "type": "object", "properties": { @@ -17085,78 +17157,6 @@ } ] }, - "PaymentMethodListRequest": { - "type": "object", - "properties": { - "client_secret": { - "type": "string", - "description": "This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK", - "example": "secret_k2uj3he2893eiu2d", - "nullable": true, - "maxLength": 30, - "minLength": 30 - }, - "accepted_countries": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CountryAlpha2" - }, - "description": "The two-letter ISO currency code", - "example": [ - "US", - "UK", - "IN" - ], - "nullable": true - }, - "amount": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "accepted_currencies": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Currency" - }, - "description": "The three-letter ISO currency code", - "example": [ - "USD", - "EUR" - ], - "nullable": true - }, - "recurring_enabled": { - "type": "boolean", - "description": "Indicates whether the payment method supports recurring payments. Optional.", - "example": true, - "nullable": true - }, - "card_networks": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CardNetwork" - }, - "description": "Indicates whether the payment method is eligible for card netwotks", - "example": [ - "visa", - "mastercard" - ], - "nullable": true - }, - "limit": { - "type": "integer", - "format": "int64", - "description": "Indicates the limit of last used payment methods", - "example": 1, - "nullable": true - } - }, - "additionalProperties": false - }, "PaymentMethodListResponseForPayments": { "type": "object", "required": [ @@ -22358,7 +22358,8 @@ "type": "object", "required": [ "payment_method_type", - "payment_method_subtype" + "payment_method_subtype", + "required_fields" ], "properties": { "payment_method_type": { @@ -22376,12 +22377,7 @@ "nullable": true }, "required_fields": { - "allOf": [ - { - "$ref": "#/components/schemas/RequiredFieldInfo" - } - ], - "nullable": true + "$ref": "#/components/schemas/RequiredFieldInfo" }, "surcharge_details": { "allOf": [ diff --git a/config/payment_required_fields_v2.toml b/config/payment_required_fields_v2.toml index fe1e6e050d2..c76a85748b7 100644 --- a/config/payment_required_fields_v2.toml +++ b/config/payment_required_fields_v2.toml @@ -2139,19 +2139,19 @@ non_mandate = [] # Payment method type: Sepa [required_fields.bank_debit.sepa.fields.Stripe] common = [ - { required_field = "payment_method_data.billing.address.first_name", display_name = "billing_address_first_name", field_type = "text" }, - { required_field = "payment_method_data.billing.address.last_name", display_name = "billing_address_last_name", field_type = "text" }, - { required_field = "payment_method_data.bank_debit.sepa.iban", display_name = "iban", field_type = "text" }, - { required_field = "email", display_name = "email", field_type = "text" } + { required_field = "payment_method_data.billing.address.first_name", display_name = "owner_name", field_type = "user_billing_name" }, + { required_field = "payment_method_data.billing.address.last_name", display_name = "owner_name", field_type = "user_billing_name" }, + { required_field = "payment_method_data.bank_debit.sepa_bank_debit.iban", display_name = "iban", field_type = "user_iban" }, + { required_field = "email", display_name = "email", field_type = "user_iban" } ] mandate = [] non_mandate = [] [required_fields.bank_debit.sepa.fields.Adyen] common = [ - { required_field = "payment_method_data.billing.address.first_name", display_name = "billing_address_first_name", field_type = "text" }, - { required_field = "payment_method_data.billing.address.last_name", display_name = "billing_address_last_name", field_type = "text" }, - { required_field = "payment_method_data.bank_debit.sepa.iban", display_name = "iban", field_type = "text" } + { required_field = "payment_method_data.billing.address.first_name", display_name = "owner_name", field_type = "user_billing_name" }, + { required_field = "payment_method_data.billing.address.last_name", display_name = "owner_name", field_type = "user_billing_name" }, + { required_field = "payment_method_data.bank_debit.sepa_bank_debit.iban", display_name = "iban", field_type = "user_iban" } ] mandate = [] non_mandate = [] @@ -2210,10 +2210,10 @@ non_mandate = [] [required_fields.bank_debit.becs.fields.Adyen] common = [ - { required_field = "payment_method_data.billing.address.first_name", display_name = "billing_address_first_name", field_type = "text" }, - { required_field = "payment_method_data.billing.address.last_name", display_name = "billing_address_last_name", field_type = "text" }, - { required_field = "payment_method_data.bank_debit.becs.account_number", display_name = "account_number", field_type = "text" }, - { required_field = "payment_method_data.bank_debit.bacs.sort_code", display_name = "sort_code", field_type = "text" } # Corrected from becs.sort_code as per V1 + { required_field = "payment_method_data.billing.address.first_name", display_name = "owner_name", field_type = "user_billing_name" }, + { required_field = "payment_method_data.billing.address.last_name", display_name = "owner_name", field_type = "user_billing_name" }, + { required_field = "ppayment_method_data.bank_debit.becs_bank_debit.account_number", display_name = "bank_account_number", field_type = "user_bank_account_number" }, + { required_field = "payment_method_data.bank_debit.becs_bank_debit.sort_code", display_name = "bank_sort_code", field_type = "user_bank_sort_code" } # Corrected from becs.sort_code as per V1 ] mandate = [] non_mandate = [] diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index 35c23003c47..cac70d26649 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -36,6 +36,7 @@ time = { version = "0.3.41", features = ["serde", "serde-well-known", "std"] } url = { version = "2.5.4", features = ["serde"] } utoipa = { version = "4.2.3", features = ["preserve_order", "preserve_path_order"] } nutype = { version = "0.4.3", features = ["serde"] } +deserialize_form_style_query_parameter = "0.2.2" # First party crates cards = { version = "0.1.0", path = "../cards" } diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index ec4c8667f45..2b867f6db99 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -6,10 +6,25 @@ use super::{ PaymentsCreateIntentRequest, PaymentsGetIntentRequest, PaymentsIntentResponse, PaymentsRequest, }; #[cfg(feature = "v2")] -use crate::payment_methods::PaymentMethodListResponseForSession; +use crate::payment_methods::{ + ListMethodsForPaymentMethodsRequest, PaymentMethodListResponseForSession, +}; +use crate::{ + payment_methods::{ + self, ListCountriesCurrenciesRequest, ListCountriesCurrenciesResponse, + PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, + PaymentMethodCollectLinkResponse, PaymentMethodMigrateResponse, PaymentMethodResponse, + PaymentMethodUpdate, + }, + payments::{ + self, PaymentListConstraints, PaymentListFilters, PaymentListFiltersV2, + PaymentListResponse, PaymentsAggregateResponse, PaymentsSessionResponse, + RedirectionResponse, + }, +}; #[cfg(feature = "v1")] use crate::{ - payment_methods::PaymentMethodListResponse, + payment_methods::{PaymentMethodListRequest, PaymentMethodListResponse}, payments::{ ExtendedCardInfoResponse, PaymentIdType, PaymentListFilterConstraints, PaymentListResponseV2, PaymentsApproveRequest, PaymentsCancelRequest, @@ -22,19 +37,6 @@ use crate::{ PaymentsStartRequest, PaymentsUpdateMetadataRequest, PaymentsUpdateMetadataResponse, }, }; -use crate::{ - payment_methods::{ - self, ListCountriesCurrenciesRequest, ListCountriesCurrenciesResponse, - PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, - PaymentMethodCollectLinkResponse, PaymentMethodListRequest, PaymentMethodMigrateResponse, - PaymentMethodResponse, PaymentMethodUpdate, - }, - payments::{ - self, PaymentListConstraints, PaymentListFilters, PaymentListFiltersV2, - PaymentListResponse, PaymentsAggregateResponse, PaymentsSessionResponse, - RedirectionResponse, - }, -}; #[cfg(feature = "v1")] impl ApiEventMetric for PaymentsRetrieveRequest { @@ -305,6 +307,7 @@ impl ApiEventMetric for payment_methods::PaymentMethodDeleteResponse { impl ApiEventMetric for payment_methods::CustomerPaymentMethodsListResponse {} +#[cfg(feature = "v1")] impl ApiEventMetric for PaymentMethodListRequest { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::PaymentMethodList { @@ -317,6 +320,19 @@ impl ApiEventMetric for PaymentMethodListRequest { } } +#[cfg(feature = "v2")] +impl ApiEventMetric for ListMethodsForPaymentMethodsRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethodList { + payment_id: self + .client_secret + .as_ref() + .and_then(|cs| cs.rsplit_once("_secret_")) + .map(|(pid, _)| pid.to_string()), + }) + } +} + impl ApiEventMetric for ListCountriesCurrenciesRequest {} impl ApiEventMetric for ListCountriesCurrenciesResponse {} diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index a70dceb962d..b76423ecd8a 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -1778,7 +1778,7 @@ impl<'de> serde::Deserialize<'de> for PaymentMethodListRequest { //List Payment Method #[derive(Debug, Clone, serde::Serialize, Default, ToSchema)] #[serde(deny_unknown_fields)] -pub struct PaymentMethodListRequest { +pub struct ListMethodsForPaymentMethodsRequest { /// This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK #[schema(max_length = 30, min_length = 30, example = "secret_k2uj3he2893eiu2d")] pub client_secret: Option, @@ -1809,7 +1809,7 @@ pub struct PaymentMethodListRequest { } #[cfg(feature = "v2")] -impl<'de> serde::Deserialize<'de> for PaymentMethodListRequest { +impl<'de> serde::Deserialize<'de> for ListMethodsForPaymentMethodsRequest { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -1817,7 +1817,7 @@ impl<'de> serde::Deserialize<'de> for PaymentMethodListRequest { struct FieldVisitor; impl<'de> de::Visitor<'de> for FieldVisitor { - type Value = PaymentMethodListRequest; + type Value = ListMethodsForPaymentMethodsRequest; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str("Failed while deserializing as map") @@ -1827,7 +1827,7 @@ impl<'de> serde::Deserialize<'de> for PaymentMethodListRequest { where A: de::MapAccess<'de>, { - let mut output = PaymentMethodListRequest::default(); + let mut output = ListMethodsForPaymentMethodsRequest::default(); while let Some(key) = map.next_key()? { match key { diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 3697c555e7e..1f648b302b7 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -26,6 +26,8 @@ use common_utils::{ pii::{self, Email}, types::{MinorUnit, StringMajorUnit}, }; +#[cfg(feature = "v2")] +use deserialize_form_style_query_parameter::option_form_vec_deserialize; use error_stack::ResultExt; use masking::{PeekInterface, Secret, WithType}; use router_derive::Setter; @@ -7701,7 +7703,38 @@ pub enum SdkType { #[cfg(feature = "v2")] #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)] -pub struct PaymentMethodsListRequest {} +pub struct ListMethodsForPaymentsRequest { + /// This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK + #[schema(max_length = 30, min_length = 30, example = "secret_k2uj3he2893eiu2d")] + pub client_secret: Option, + + /// The two-letter ISO currency code + #[serde(deserialize_with = "option_form_vec_deserialize", default)] + #[schema(value_type = Option>, example = json!(["US", "UK", "IN"]))] + pub accepted_countries: Option>, + + /// Filter by amount + #[schema(example = 60)] + pub amount: Option, + + /// The three-letter ISO currency code + #[serde(deserialize_with = "option_form_vec_deserialize", default)] + #[schema(value_type = Option>,example = json!(["USD", "EUR"]))] + pub accepted_currencies: Option>, + + /// Indicates whether the payment method supports recurring payments. Optional. + #[schema(example = true)] + pub recurring_enabled: Option, + + /// Indicates whether the payment method is eligible for card networks + #[serde(deserialize_with = "option_form_vec_deserialize", default)] + #[schema(value_type = Option>, example = json!(["visa", "mastercard"]))] + pub card_networks: Option>, + + /// Indicates the limit of last used payment methods + #[schema(example = 1)] + pub limit: Option, +} #[cfg(feature = "v2")] #[derive(Debug, serde::Serialize, ToSchema)] @@ -7737,8 +7770,8 @@ pub struct ResponsePaymentMethodTypesForPayments { /// Required fields for the payment_method_type. /// This is the union of all the required fields for the payment method type enabled in all the connectors. - #[schema(value_type = Option)] - pub required_fields: Option>, + #[schema(value_type = RequiredFieldInfo)] + pub required_fields: Vec, /// surcharge details for this payment method type if exists #[schema(value_type = Option)] diff --git a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs index 1e460ee7306..b7c3dda6045 100644 --- a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs +++ b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs @@ -291,6 +291,7 @@ pub struct PaymentMethodsEnabledForConnector { pub payment_methods_enabled: common_types::payment_methods::RequestPaymentMethodTypes, pub payment_method: common_enums::PaymentMethod, pub connector: common_enums::connector_enums::Connector, + pub merchant_connector_id: id_type::MerchantConnectorAccountId, } #[cfg(feature = "v2")] @@ -365,29 +366,40 @@ impl FlattenedPaymentMethodsEnabled { .into_iter() .map(|connector| { ( - connector.payment_methods_enabled.unwrap_or_default(), + connector + .payment_methods_enabled + .clone() + .unwrap_or_default(), connector.connector_name, + connector.get_id(), ) }) - .flat_map(|(payment_method_enabled, connector_name)| { - payment_method_enabled - .into_iter() - .flat_map(move |payment_method| { - let request_payment_methods_enabled = - payment_method.payment_method_subtypes.unwrap_or_default(); - let length = request_payment_methods_enabled.len(); - request_payment_methods_enabled.into_iter().zip( - std::iter::repeat((connector_name, payment_method.payment_method_type)) + .flat_map( + |(payment_method_enabled, connector, merchant_connector_id)| { + payment_method_enabled + .into_iter() + .flat_map(move |payment_method| { + let request_payment_methods_enabled = + payment_method.payment_method_subtypes.unwrap_or_default(); + let length = request_payment_methods_enabled.len(); + request_payment_methods_enabled.into_iter().zip( + std::iter::repeat(( + connector, + merchant_connector_id.clone(), + payment_method.payment_method_type, + )) .take(length), - ) - }) - }) + ) + }) + }, + ) .map( - |(request_payment_methods, (connector_name, payment_method))| { + |(request_payment_methods, (connector, merchant_connector_id, payment_method))| { PaymentMethodsEnabledForConnector { payment_methods_enabled: request_payment_methods, - connector: connector_name, + connector, payment_method, + merchant_connector_id, } }, ) diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index ac8d5d728a2..91854d95a15 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -239,7 +239,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::PaymentMethodResponseData, api_models::payment_methods::CustomerPaymentMethodResponseItem, api_models::payment_methods::PaymentMethodResponseItem, - api_models::payment_methods::PaymentMethodListRequest, + api_models::payment_methods::ListMethodsForPaymentMethodsRequest, api_models::payment_methods::PaymentMethodListResponseForSession, api_models::payment_methods::CustomerPaymentMethodsListResponse, api_models::payment_methods::ResponsePaymentMethodsEnabled, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 8f29e05e154..1de486d0a7e 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -41,9 +41,10 @@ use common_utils::{ }; use diesel_models::payment_method; use error_stack::{report, ResultExt}; -#[cfg(feature = "v1")] -use euclid::dssa::graph::{AnalysisContext, CgraphExt}; -use euclid::frontend::dir; +use euclid::{ + dssa::graph::{AnalysisContext, CgraphExt}, + frontend::dir, +}; use hyperswitch_constraint_graph as cgraph; #[cfg(feature = "v1")] use hyperswitch_domain_models::customer::CustomerUpdate; @@ -3962,25 +3963,6 @@ pub async fn filter_payment_methods( Ok(()) } -// v2 type for PaymentMethodListRequest will not have the installment_payment_enabled field, -// need to re-evaluate filter logic -#[cfg(feature = "v2")] -#[allow(clippy::too_many_arguments)] -pub async fn filter_payment_methods( - _graph: &cgraph::ConstraintGraph, - _mca_id: String, - _payment_methods: &[Secret], - _req: &mut api::PaymentMethodListRequest, - _resp: &mut [ResponsePaymentMethodIntermediate], - _payment_intent: Option<&storage::PaymentIntent>, - _payment_attempt: Option<&storage::PaymentAttempt>, - _address: Option<&domain::Address>, - _connector: String, - _configs: &settings::Settings, -) -> errors::CustomResult<(), errors::ApiErrorResponse> { - todo!() -} - fn filter_amount_based( payment_method: &RequestPaymentMethodTypes, amount: Option, diff --git a/crates/router/src/core/payments/payment_methods.rs b/crates/router/src/core/payments/payment_methods.rs index e21b3b96105..a5f8646f8ff 100644 --- a/crates/router/src/core/payments/payment_methods.rs +++ b/crates/router/src/core/payments/payment_methods.rs @@ -1,14 +1,25 @@ //! Contains functions of payment methods that are used in payments //! one of such functions is `list_payment_methods` -use std::collections::{BTreeMap, HashSet}; +use std::{ + collections::{BTreeMap, HashSet}, + str::FromStr, +}; -use common_utils::{ext_traits::OptionExt, id_type}; +use common_utils::{ + ext_traits::{OptionExt, ValueExt}, + id_type, +}; use error_stack::ResultExt; +use hyperswitch_interfaces::secrets_interface::secret_state::RawSecret; use super::errors; use crate::{ - core::payment_methods, db::errors::StorageErrorExt, logger, routes, settings, types::domain, + configs::settings, + core::{payment_methods, payments::helpers}, + db::errors::StorageErrorExt, + logger, routes, + types::{self, api, domain, storage}, }; #[cfg(feature = "v2")] @@ -17,7 +28,7 @@ pub async fn list_payment_methods( merchant_context: domain::MerchantContext, profile: domain::Profile, payment_id: id_type::GlobalPaymentId, - _req: api_models::payments::PaymentMethodsListRequest, + req: api_models::payments::ListMethodsForPaymentsRequest, header_payload: &hyperswitch_domain_models::payments::HeaderPayload, ) -> errors::RouterResponse { let db = &*state.store; @@ -59,10 +70,16 @@ pub async fn list_payment_methods( }; let response = - FlattenedPaymentMethodsEnabled(hyperswitch_domain_models::merchant_connector_account::FlattenedPaymentMethodsEnabled::from_payment_connectors_list(payment_connector_accounts)) - .perform_filtering() + FlattenedPaymentMethodsEnabled(hyperswitch_domain_models::merchant_connector_account::FlattenedPaymentMethodsEnabled::from_payment_connectors_list(payment_connector_accounts)) + .perform_filtering( + &state, + &merchant_context, + profile.get_id(), + &req, + &payment_intent, + ).await? .merge_and_transform() - .get_required_fields(RequiredFieldsInput::new()) + .get_required_fields(RequiredFieldsInput::new(state.conf.required_fields.clone(), payment_intent.setup_future_usage)) .perform_surcharge_calculation() .populate_pm_subtype_specific_data(&state.conf.bank_config) .generate_response(customer_payment_methods); @@ -73,24 +90,56 @@ pub async fn list_payment_methods( } /// Container for the inputs required for the required fields -struct RequiredFieldsInput {} +struct RequiredFieldsInput { + required_fields_config: settings::RequiredFields, + setup_future_usage: common_enums::FutureUsage, +} impl RequiredFieldsInput { - fn new() -> Self { - Self {} + fn new( + required_fields_config: settings::RequiredFields, + setup_future_usage: common_enums::FutureUsage, + ) -> Self { + Self { + required_fields_config, + setup_future_usage, + } } } -struct FlattenedPaymentMethodsEnabled( - hyperswitch_domain_models::merchant_connector_account::FlattenedPaymentMethodsEnabled, -); +trait GetRequiredFields { + fn get_required_fields( + &self, + payment_method_enabled: &MergedEnabledPaymentMethod, + ) -> Option<&settings::RequiredFieldFinal>; +} -impl FlattenedPaymentMethodsEnabled { - fn perform_filtering(self) -> FilteredPaymentMethodsEnabled { - FilteredPaymentMethodsEnabled(self.0.payment_methods_enabled) +impl GetRequiredFields for settings::RequiredFields { + fn get_required_fields( + &self, + payment_method_enabled: &MergedEnabledPaymentMethod, + ) -> Option<&settings::RequiredFieldFinal> { + self.0 + .get(&payment_method_enabled.payment_method_type) + .and_then(|required_fields_for_payment_method| { + required_fields_for_payment_method + .0 + .get(&payment_method_enabled.payment_method_subtype) + }) + .map(|connector_fields| &connector_fields.fields) + .and_then(|connector_hashmap| { + payment_method_enabled + .connectors + .first() + .and_then(|connector| connector_hashmap.get(connector)) + }) } } +struct FlattenedPaymentMethodsEnabled( + hyperswitch_domain_models::merchant_connector_account::FlattenedPaymentMethodsEnabled, +); + /// Container for the filtered payment methods struct FilteredPaymentMethodsEnabled( Vec, @@ -156,20 +205,61 @@ struct MergedEnabledPaymentMethodTypes(Vec); impl MergedEnabledPaymentMethodTypes { fn get_required_fields( self, - _input: RequiredFieldsInput, + input: RequiredFieldsInput, ) -> RequiredFieldsForEnabledPaymentMethodTypes { + let required_fields_config = input.required_fields_config; + let is_cit_transaction = input.setup_future_usage == common_enums::FutureUsage::OffSession; + let required_fields_info = self .0 .into_iter() - .map( - |payment_methods_enabled| RequiredFieldsForEnabledPaymentMethod { - required_field: None, + .map(|payment_methods_enabled| { + let required_fields = + required_fields_config.get_required_fields(&payment_methods_enabled); + + let required_fields = required_fields + .map(|required_fields| { + let common_required_fields = required_fields + .common + .iter() + .flatten() + .map(ToOwned::to_owned); + + // Collect mandate required fields because this is for zero auth mandates only + let mandate_required_fields = required_fields + .mandate + .iter() + .flatten() + .map(ToOwned::to_owned); + + // Collect non-mandate required fields because this is for zero auth mandates only + let non_mandate_required_fields = required_fields + .non_mandate + .iter() + .flatten() + .map(ToOwned::to_owned); + + // Combine mandate and non-mandate required fields based on setup_future_usage + if is_cit_transaction { + common_required_fields + .chain(non_mandate_required_fields) + .collect::>() + } else { + common_required_fields + .chain(mandate_required_fields) + .collect::>() + } + }) + .unwrap_or_default(); + + RequiredFieldsForEnabledPaymentMethod { + required_fields, payment_method_type: payment_methods_enabled.payment_method_type, payment_method_subtype: payment_methods_enabled.payment_method_subtype, payment_experience: payment_methods_enabled.payment_experience, connectors: payment_methods_enabled.connectors, - }, - ) + } + }) .collect(); RequiredFieldsForEnabledPaymentMethodTypes(required_fields_info) @@ -178,7 +268,7 @@ impl MergedEnabledPaymentMethodTypes { /// Element container to hold the filtered payment methods with required fields struct RequiredFieldsForEnabledPaymentMethod { - required_field: Option>, + required_fields: Vec, payment_method_subtype: common_enums::PaymentMethodType, payment_method_type: common_enums::PaymentMethod, payment_experience: Option>, @@ -199,7 +289,7 @@ impl RequiredFieldsForEnabledPaymentMethodTypes { .map( |payment_methods_enabled| RequiredFieldsAndSurchargeForEnabledPaymentMethodType { payment_method_type: payment_methods_enabled.payment_method_type, - required_field: payment_methods_enabled.required_field, + required_fields: payment_methods_enabled.required_fields, payment_method_subtype: payment_methods_enabled.payment_method_subtype, payment_experience: payment_methods_enabled.payment_experience, surcharge: None, @@ -214,7 +304,7 @@ impl RequiredFieldsForEnabledPaymentMethodTypes { /// Element Container to hold the filtered payment methods enabled with required fields and surcharge struct RequiredFieldsAndSurchargeForEnabledPaymentMethodType { - required_field: Option>, + required_fields: Vec, payment_method_subtype: common_enums::PaymentMethodType, payment_method_type: common_enums::PaymentMethod, payment_experience: Option>, @@ -292,7 +382,7 @@ impl RequiredFieldsAndSurchargeForEnabledPaymentMethodTypes { payment_method_type: payment_methods_enabled.payment_method_type, payment_method_subtype: payment_methods_enabled.payment_method_subtype, payment_experience: payment_methods_enabled.payment_experience, - required_field: payment_methods_enabled.required_field, + required_fields: payment_methods_enabled.required_fields, surcharge: payment_methods_enabled.surcharge, pm_subtype_specific_data: get_pm_subtype_specific_data( bank_config, @@ -312,7 +402,7 @@ impl RequiredFieldsAndSurchargeForEnabledPaymentMethodTypes { /// Element Container to hold the filtered payment methods enabled with required fields, surcharge and subtype specific data struct RequiredFieldsAndSurchargeWithExtraInfoForEnabledPaymentMethodType { - required_field: Option>, + required_fields: Vec, payment_method_subtype: common_enums::PaymentMethodType, payment_method_type: common_enums::PaymentMethod, payment_experience: Option>, @@ -340,7 +430,7 @@ impl RequiredFieldsAndSurchargeWithExtraInfoForEnabledPaymentMethodTypes { payment_method_type: payment_methods_enabled.payment_method_type, payment_method_subtype: payment_methods_enabled.payment_method_subtype, payment_experience: payment_methods_enabled.payment_experience, - required_fields: payment_methods_enabled.required_field, + required_fields: payment_methods_enabled.required_fields, surcharge_details: payment_methods_enabled.surcharge, extra_information: payment_methods_enabled.pm_subtype_specific_data, } @@ -354,6 +444,292 @@ impl RequiredFieldsAndSurchargeWithExtraInfoForEnabledPaymentMethodTypes { } } +impl FlattenedPaymentMethodsEnabled { + async fn perform_filtering( + self, + state: &routes::SessionState, + merchant_context: &domain::MerchantContext, + profile_id: &id_type::ProfileId, + req: &api_models::payments::ListMethodsForPaymentsRequest, + payment_intent: &hyperswitch_domain_models::payments::PaymentIntent, + ) -> errors::RouterResult { + let billing_address = payment_intent + .billing_address + .clone() + .and_then(|address| address.into_inner().address); + + let mut response: Vec = vec![]; + + for payment_method_enabled_details in self.0.payment_methods_enabled { + filter_payment_methods( + payment_method_enabled_details, + req, + &mut response, + Some(payment_intent), + billing_address.as_ref(), + &state.conf, + ) + .await?; + } + + Ok(FilteredPaymentMethodsEnabled(response)) + } +} + +// note: v2 type for ListMethodsForPaymentMethodsRequest will not have the installment_payment_enabled field, +#[cfg(feature = "v2")] +#[allow(clippy::too_many_arguments)] +pub async fn filter_payment_methods( + payment_method_type_details: hyperswitch_domain_models::merchant_connector_account::PaymentMethodsEnabledForConnector, + req: &api_models::payments::ListMethodsForPaymentsRequest, + resp: &mut Vec< + hyperswitch_domain_models::merchant_connector_account::PaymentMethodsEnabledForConnector, + >, + payment_intent: Option<&storage::PaymentIntent>, + address: Option<&hyperswitch_domain_models::address::AddressDetails>, + configs: &settings::Settings, +) -> errors::CustomResult<(), errors::ApiErrorResponse> { + let payment_method = payment_method_type_details.payment_method; + let mut payment_method_object = payment_method_type_details.payment_methods_enabled.clone(); + + // filter based on request parameters + let request_based_filter = + filter_recurring_based(&payment_method_object, req.recurring_enabled) + && filter_amount_based(&payment_method_object, req.amount) + && filter_card_network_based( + payment_method_object.card_networks.as_ref(), + req.card_networks.as_ref(), + payment_method_object.payment_method_subtype, + ); + + // filter based on payment intent + let intent_based_filter = if let Some(payment_intent) = payment_intent { + filter_country_based(address, &payment_method_object) + && filter_currency_based( + payment_intent.amount_details.currency, + &payment_method_object, + ) + && filter_amount_based( + &payment_method_object, + Some(payment_intent.amount_details.calculate_net_amount()), + ) + && filter_zero_mandate_based(configs, payment_intent, &payment_method_type_details) + && filter_allowed_payment_method_types_based( + payment_intent.allowed_payment_method_types.as_ref(), + payment_method_object.payment_method_subtype, + ) + } else { + true + }; + + // filter based on payment method type configuration + let config_based_filter = filter_config_based( + configs, + &payment_method_type_details.connector.to_string(), + payment_method_object.payment_method_subtype, + payment_intent, + &mut payment_method_object.card_networks, + address.and_then(|inner| inner.country), + payment_intent.map(|value| value.amount_details.currency), + ); + + // if all filters pass, add the payment method type details to the response + if request_based_filter && intent_based_filter && config_based_filter { + resp.push(payment_method_type_details); + } + + Ok(()) +} + +// filter based on country supported by payment method type +// return true if the intent's country is null or if the country is in the accepted countries list +fn filter_country_based( + address: Option<&hyperswitch_domain_models::address::AddressDetails>, + pm: &common_types::payment_methods::RequestPaymentMethodTypes, +) -> bool { + address.map_or(true, |address| { + address.country.as_ref().map_or(true, |country| { + pm.accepted_countries.as_ref().map_or(true, |ac| match ac { + common_types::payment_methods::AcceptedCountries::EnableOnly(acc) => { + acc.contains(country) + } + common_types::payment_methods::AcceptedCountries::DisableOnly(den) => { + !den.contains(country) + } + common_types::payment_methods::AcceptedCountries::AllAccepted => true, + }) + }) + }) +} + +// filter based on currency supported by payment method type +// return true if the intent's currency is null or if the currency is in the accepted currencies list +fn filter_currency_based( + currency: common_enums::Currency, + pm: &common_types::payment_methods::RequestPaymentMethodTypes, +) -> bool { + pm.accepted_currencies.as_ref().map_or(true, |ac| match ac { + common_types::payment_methods::AcceptedCurrencies::EnableOnly(acc) => { + acc.contains(¤cy) + } + common_types::payment_methods::AcceptedCurrencies::DisableOnly(den) => { + !den.contains(¤cy) + } + common_types::payment_methods::AcceptedCurrencies::AllAccepted => true, + }) +} + +// filter based on payment method type configuration +// return true if the payment method type is in the configuration for the connector +// return true if the configuration is not available for the connector +fn filter_config_based<'a>( + config: &'a settings::Settings, + connector: &'a str, + payment_method_type: common_enums::PaymentMethodType, + payment_intent: Option<&storage::PaymentIntent>, + card_network: &mut Option>, + country: Option, + currency: Option, +) -> bool { + config + .pm_filters + .0 + .get(connector) + .or_else(|| config.pm_filters.0.get("default")) + .and_then(|inner| match payment_method_type { + common_enums::PaymentMethodType::Credit | common_enums::PaymentMethodType::Debit => { + inner + .0 + .get(&settings::PaymentMethodFilterKey::PaymentMethodType( + payment_method_type, + )) + .map(|value| filter_config_country_currency_based(value, country, currency)) + } + payment_method_type => inner + .0 + .get(&settings::PaymentMethodFilterKey::PaymentMethodType( + payment_method_type, + )) + .map(|value| filter_config_country_currency_based(value, country, currency)), + }) + .unwrap_or(true) +} + +// filter country and currency based on config for payment method type +// return true if the country and currency are in the accepted countries and currencies list +fn filter_config_country_currency_based( + item: &settings::CurrencyCountryFlowFilter, + country: Option, + currency: Option, +) -> 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) +} + +// filter based on recurring enabled parameter of request +// return true if recurring_enabled is null or if it matches the payment method's recurring_enabled +fn filter_recurring_based( + payment_method: &common_types::payment_methods::RequestPaymentMethodTypes, + recurring_enabled: Option, +) -> bool { + recurring_enabled.map_or(true, |enabled| { + payment_method.recurring_enabled == Some(enabled) + }) +} + +// filter based on valid amount range of payment method type +// return true if the amount is within the payment method's minimum and maximum amount range +// return true if the amount is null or zero +fn filter_amount_based( + payment_method: &common_types::payment_methods::RequestPaymentMethodTypes, + amount: Option, +) -> bool { + let min_check = amount + .and_then(|amt| payment_method.minimum_amount.map(|min_amt| amt >= min_amt)) + .unwrap_or(true); + let max_check = amount + .and_then(|amt| payment_method.maximum_amount.map(|max_amt| amt <= max_amt)) + .unwrap_or(true); + (min_check && max_check) || amount == Some(types::MinorUnit::zero()) +} + +// return true if the intent is a zero mandate intent and the payment method is supported for zero mandates +// return false if the intent is a zero mandate intent and the payment method is not supported for zero mandates +// return true if the intent is not a zero mandate intent +fn filter_zero_mandate_based( + configs: &settings::Settings, + payment_intent: &storage::PaymentIntent, + payment_method_type_details: &hyperswitch_domain_models::merchant_connector_account::PaymentMethodsEnabledForConnector, +) -> bool { + if payment_intent.setup_future_usage == common_enums::FutureUsage::OffSession + && payment_intent.amount_details.calculate_net_amount() == types::MinorUnit::zero() + { + configs + .zero_mandates + .supported_payment_methods + .0 + .get(&payment_method_type_details.payment_method) + .and_then(|supported_pm_for_mandates| { + supported_pm_for_mandates + .0 + .get( + &payment_method_type_details + .payment_methods_enabled + .payment_method_subtype, + ) + .map(|supported_connector_for_mandates| { + supported_connector_for_mandates + .connector_list + .contains(&payment_method_type_details.connector) + }) + }) + .unwrap_or(false) + } else { + true + } +} + +// filter based on allowed payment method types +// return true if the allowed types are null or if the payment method type is in the allowed types list +fn filter_allowed_payment_method_types_based( + allowed_types: Option<&Vec>, + payment_method_type: api_models::enums::PaymentMethodType, +) -> bool { + allowed_types.map_or(true, |pm| pm.contains(&payment_method_type)) +} + +// filter based on card networks +// return true if the payment method type's card networks are a subset of the request's card networks +// return true if the card networks are not specified in the request +fn filter_card_network_based( + pm_card_networks: Option<&Vec>, + request_card_networks: Option<&Vec>, + pm_type: api_models::enums::PaymentMethodType, +) -> bool { + match pm_type { + api_models::enums::PaymentMethodType::Credit + | api_models::enums::PaymentMethodType::Debit => { + match (pm_card_networks, request_card_networks) { + (Some(pm_card_networks), Some(request_card_networks)) => request_card_networks + .iter() + .all(|card_network| pm_card_networks.contains(card_network)), + (None, Some(_)) => false, + _ => true, + } + } + _ => true, + } +} + /// Validate if payment methods list can be performed on the current status of payment intent fn validate_payment_status_for_payment_method_list( intent_status: common_enums::IntentStatus, diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 8dac786ff33..367ece308c3 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -612,7 +612,7 @@ pub async fn list_customer_payment_method_api( state: web::Data, customer_id: web::Path, req: HttpRequest, - query_payload: web::Query, + query_payload: web::Query, ) -> HttpResponse { let flow = Flow::CustomerPaymentMethodsList; let payload = query_payload.into_inner(); @@ -956,6 +956,7 @@ pub async fn default_payment_method_set_api( .await } +#[cfg(feature = "v1")] #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 74e6c580ff4..1c80f3fa932 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -3302,7 +3302,7 @@ pub async fn list_payment_methods( state: web::Data, req: actix_web::HttpRequest, path: web::Path, - query_payload: web::Query, + query_payload: web::Query, ) -> impl Responder { use crate::db::domain::merchant_context; diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index fabfe3e38d2..f7e708a5a87 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -1103,6 +1103,7 @@ impl Authenticate for api_models::payments::PaymentsRequest { } } +#[cfg(feature = "v1")] impl Authenticate for api_models::payment_methods::PaymentMethodListRequest { fn get_client_secret(&self) -> Option<&String> { self.client_secret.as_ref() diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 564c9f9d3ed..b82a46f267e 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -1,13 +1,13 @@ use std::str::FromStr; use actix_web::http::header::HeaderMap; -#[cfg(feature = "v1")] -use api_models::payment_methods::PaymentMethodCreate; #[cfg(feature = "v2")] use api_models::payment_methods::PaymentMethodIntentConfirm; +#[cfg(feature = "v1")] +use api_models::payment_methods::{PaymentMethodCreate, PaymentMethodListRequest}; +use api_models::payments; #[cfg(feature = "payouts")] use api_models::payouts; -use api_models::{payment_methods::PaymentMethodListRequest, payments}; use async_trait::async_trait; use common_enums::TokenPurpose; use common_utils::{date_time, fp_utils, id_type}; @@ -4168,6 +4168,7 @@ impl ClientSecretFetch for payments::PaymentsRetrieveRequest { } } +#[cfg(feature = "v1")] impl ClientSecretFetch for PaymentMethodListRequest { fn get_client_secret(&self) -> Option<&String> { self.client_secret.as_ref() diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index 4c094af5acc..df3f4d466c3 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -7,10 +7,10 @@ pub use api_models::payment_methods::{ NetworkTokenDetailsResponse, NetworkTokenResponse, PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate, PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodIntentConfirm, - PaymentMethodIntentCreate, PaymentMethodListData, PaymentMethodListRequest, - PaymentMethodListResponseForSession, PaymentMethodMigrate, PaymentMethodMigrateResponse, - PaymentMethodResponse, PaymentMethodResponseData, PaymentMethodUpdate, PaymentMethodUpdateData, - PaymentMethodsData, TokenDataResponse, TokenDetailsResponse, TokenizePayloadEncrypted, + PaymentMethodIntentCreate, PaymentMethodListData, PaymentMethodListResponseForSession, + PaymentMethodMigrate, PaymentMethodMigrateResponse, PaymentMethodResponse, + PaymentMethodResponseData, PaymentMethodUpdate, PaymentMethodUpdateData, PaymentMethodsData, + RequestPaymentMethodTypes, TokenDataResponse, TokenDetailsResponse, TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, TotalPaymentMethodCountResponse, };