From 9bac41b2086f29f51cecdb3cc004fb2ee3dac455 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Fri, 30 May 2025 18:21:35 +0530 Subject: [PATCH] feat(router): add three_ds_decision_rule support in routing apis (#8132) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 90 ++++++++++++++++- api-reference/openapi_spec.json | 98 ++++++++++++++++++- crates/api_models/src/routing.rs | 48 ++++++++- crates/common_enums/src/enums.rs | 7 ++ crates/diesel_models/src/enums.rs | 1 + crates/openapi/src/openapi.rs | 5 + crates/openapi/src/openapi_v2.rs | 4 + crates/router/src/core/admin.rs | 2 + crates/router/src/core/errors.rs | 2 + crates/router/src/core/payments/routing.rs | 16 +++ crates/router/src/core/routing.rs | 45 +++++---- crates/router/src/core/routing/helpers.rs | 57 +++++++---- .../router/src/core/routing/transformers.rs | 2 + crates/router/src/routes/app.rs | 54 ++++------ crates/router/src/routes/routing.rs | 49 ++++++---- .../down.sql | 2 + .../up.sql | 2 + .../down.sql | 2 + .../up.sql | 2 + 19 files changed, 395 insertions(+), 93 deletions(-) create mode 100644 migrations/2025-05-22-191239_add-three-ds-decision-rule-to-algorithm-kind/down.sql create mode 100644 migrations/2025-05-22-191239_add-three-ds-decision-rule-to-algorithm-kind/up.sql create mode 100644 migrations/2025-05-24-205102_add-three-ds-authentication-to-transaction-type/down.sql create mode 100644 migrations/2025-05-24-205102_add-three-ds-authentication-to-transaction-type/up.sql diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index fbabc86467..bf124f4a87 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -20888,6 +20888,26 @@ } } }, + "ProgramThreeDsDecisionRule": { + "type": "object", + "required": [ + "defaultSelection", + "rules", + "metadata" + ], + "properties": { + "defaultSelection": { + "$ref": "#/components/schemas/ThreeDSDecisionRule" + }, + "rules": { + "$ref": "#/components/schemas/RuleThreeDsDecisionRule" + }, + "metadata": { + "type": "object", + "additionalProperties": {} + } + } + }, "ProxyRequest": { "type": "object", "required": [ @@ -22097,7 +22117,8 @@ "priority", "volume_split", "advanced", - "dynamic" + "dynamic", + "three_ds_decision_rule" ] }, "RoutingAlgorithmWrapper": { @@ -22254,6 +22275,28 @@ } } }, + "RuleThreeDsDecisionRule": { + "type": "object", + "required": [ + "name", + "connectorSelection", + "statements" + ], + "properties": { + "name": { + "type": "string" + }, + "connectorSelection": { + "$ref": "#/components/schemas/ThreeDSDecision" + }, + "statements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IfStatement" + } + } + } + }, "SamsungPayAmountDetails": { "type": "object", "required": [ @@ -23162,6 +23205,24 @@ "$ref": "#/components/schemas/ProgramConnectorSelection" } } + }, + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "three_ds_decision_rule" + ] + }, + "data": { + "$ref": "#/components/schemas/ProgramThreeDsDecisionRule" + } + } } ], "discriminator": { @@ -23575,6 +23636,30 @@ } } }, + "ThreeDSDecision": { + "type": "string", + "description": "Enum representing the possible outcomes of the 3DS Decision Rule Engine.", + "enum": [ + "no_three_ds", + "challenge_requested", + "challenge_preferred", + "three_ds_exemption_requested_tra", + "three_ds_exemption_requested_low_value", + "issuer_three_ds_exemption_requested" + ] + }, + "ThreeDSDecisionRule": { + "type": "object", + "description": "Struct representing the output configuration for the 3DS Decision Rule Engine.", + "required": [ + "decision" + ], + "properties": { + "decision": { + "$ref": "#/components/schemas/ThreeDSDecision" + } + } + }, "ThreeDsCompletionIndicator": { "type": "string", "enum": [ @@ -23924,7 +24009,8 @@ "type": "string", "enum": [ "payment", - "payout" + "payout", + "three_ds_authentication" ] }, "TriggeredBy": { diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 57624b229f..d043d0b164 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -24913,6 +24913,26 @@ } } }, + "ProgramThreeDsDecisionRule": { + "type": "object", + "required": [ + "defaultSelection", + "rules", + "metadata" + ], + "properties": { + "defaultSelection": { + "$ref": "#/components/schemas/ThreeDSDecisionRule" + }, + "rules": { + "$ref": "#/components/schemas/RuleThreeDsDecisionRule" + }, + "metadata": { + "type": "object", + "additionalProperties": {} + } + } + }, "RealTimePaymentData": { "oneOf": [ { @@ -26130,7 +26150,8 @@ "priority", "volume_split", "advanced", - "dynamic" + "dynamic", + "three_ds_decision_rule" ] }, "RoutingAlgorithmWrapper": { @@ -26165,6 +26186,14 @@ "profile_id": { "type": "string", "nullable": true + }, + "transaction_type": { + "allOf": [ + { + "$ref": "#/components/schemas/TransactionType" + } + ], + "nullable": true } } }, @@ -26302,6 +26331,28 @@ } } }, + "RuleThreeDsDecisionRule": { + "type": "object", + "required": [ + "name", + "connectorSelection", + "statements" + ], + "properties": { + "name": { + "type": "string" + }, + "connectorSelection": { + "$ref": "#/components/schemas/ThreeDSDecision" + }, + "statements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IfStatement" + } + } + } + }, "SamsungPayAmountDetails": { "type": "object", "required": [ @@ -27196,6 +27247,24 @@ "$ref": "#/components/schemas/ProgramConnectorSelection" } } + }, + { + "type": "object", + "required": [ + "type", + "data" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "three_ds_decision_rule" + ] + }, + "data": { + "$ref": "#/components/schemas/ProgramThreeDsDecisionRule" + } + } } ], "discriminator": { @@ -27595,6 +27664,30 @@ } } }, + "ThreeDSDecision": { + "type": "string", + "description": "Enum representing the possible outcomes of the 3DS Decision Rule Engine.", + "enum": [ + "no_three_ds", + "challenge_requested", + "challenge_preferred", + "three_ds_exemption_requested_tra", + "three_ds_exemption_requested_low_value", + "issuer_three_ds_exemption_requested" + ] + }, + "ThreeDSDecisionRule": { + "type": "object", + "description": "Struct representing the output configuration for the 3DS Decision Rule Engine.", + "required": [ + "decision" + ], + "properties": { + "decision": { + "$ref": "#/components/schemas/ThreeDSDecision" + } + } + }, "ThreeDsCompletionIndicator": { "type": "string", "enum": [ @@ -27945,7 +28038,8 @@ "type": "string", "enum": [ "payment", - "payout" + "payout", + "three_ds_authentication" ] }, "TriggeredBy": { diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 9e5fad91ac..7068da8edc 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -1,10 +1,12 @@ use std::fmt::Debug; +use common_types::three_ds_decision_rule_engine::{ThreeDSDecision, ThreeDSDecisionRule}; use common_utils::{ errors::{ParsingError, ValidationError}, ext_traits::ValueExt, pii, }; +use euclid::frontend::ast::Program; pub use euclid::{ dssa::types::EuclidAnalysable, frontend::{ @@ -62,6 +64,7 @@ pub struct RoutingConfigRequest { pub algorithm: Option, #[schema(value_type = Option)] pub profile_id: Option, + pub transaction_type: Option, } #[derive(Debug, serde::Serialize, ToSchema)] @@ -75,11 +78,18 @@ pub struct ProfileDefaultRoutingConfig { pub struct RoutingRetrieveQuery { pub limit: Option, pub offset: Option, + pub transaction_type: Option, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct RoutingActivatePayload { + pub transaction_type: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct RoutingRetrieveLinkQuery { pub profile_id: Option, + pub transaction_type: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] @@ -294,6 +304,7 @@ pub enum RoutingAlgorithmKind { VolumeSplit, Advanced, Dynamic, + ThreeDsDecisionRule, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -328,7 +339,37 @@ pub enum StaticRoutingAlgorithm { Priority(Vec), VolumeSplit(Vec), #[schema(value_type=ProgramConnectorSelection)] - Advanced(ast::Program), + Advanced(Program), + #[schema(value_type=ProgramThreeDsDecisionRule)] + ThreeDsDecisionRule(Program), +} + +#[derive(Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ProgramThreeDsDecisionRule { + pub default_selection: ThreeDSDecisionRule, + #[schema(value_type = RuleThreeDsDecisionRule)] + pub rules: Vec>, + #[schema(value_type = HashMap)] + pub metadata: std::collections::HashMap, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RuleThreeDsDecisionRule { + pub name: String, + pub connector_selection: ThreeDSDecision, + #[schema(value_type = Vec)] + pub statements: Vec, +} + +impl StaticRoutingAlgorithm { + pub fn should_validate_connectors_in_routing_config(&self) -> bool { + match self { + Self::Single(_) | Self::Priority(_) | Self::VolumeSplit(_) | Self::Advanced(_) => true, + Self::ThreeDsDecisionRule(_) => false, + } + } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] @@ -337,7 +378,8 @@ pub enum RoutingAlgorithmSerde { Single(Box), Priority(Vec), VolumeSplit(Vec), - Advanced(ast::Program), + Advanced(Program), + ThreeDsDecisionRule(Program), } impl TryFrom for StaticRoutingAlgorithm { @@ -362,6 +404,7 @@ impl TryFrom for StaticRoutingAlgorithm { RoutingAlgorithmSerde::Priority(i) => Self::Priority(i), RoutingAlgorithmSerde::VolumeSplit(i) => Self::VolumeSplit(i), RoutingAlgorithmSerde::Advanced(i) => Self::Advanced(i), + RoutingAlgorithmSerde::ThreeDsDecisionRule(i) => Self::ThreeDsDecisionRule(i), }) } } @@ -464,6 +507,7 @@ impl StaticRoutingAlgorithm { Self::Priority(_) => RoutingAlgorithmKind::Priority, Self::VolumeSplit(_) => RoutingAlgorithmKind::VolumeSplit, Self::Advanced(_) => RoutingAlgorithmKind::Advanced, + Self::ThreeDsDecisionRule(_) => RoutingAlgorithmKind::ThreeDsDecisionRule, } } } diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 55eab7935b..d6b021fd4d 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -7231,6 +7231,13 @@ pub enum TransactionType { Payment, #[cfg(feature = "payouts")] Payout, + ThreeDsAuthentication, +} + +impl TransactionType { + pub fn is_three_ds_authentication(self) -> bool { + matches!(self, Self::ThreeDsAuthentication) + } } #[derive( diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 39ce33a513..fc9417342e 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -57,6 +57,7 @@ pub enum RoutingAlgorithmKind { VolumeSplit, Advanced, Dynamic, + ThreeDsDecisionRule, } #[derive( diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 7a86b8a77f..bacbc3ee94 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -233,6 +233,8 @@ Never share your secret api keys. Keep them guarded and secure. common_types::refunds::StripeSplitRefundRequest, common_types::payments::ConnectorChargeResponseData, common_types::payments::StripeChargeResponseData, + common_types::three_ds_decision_rule_engine::ThreeDSDecisionRule, + common_types::three_ds_decision_rule_engine::ThreeDSDecision, api_models::refunds::RefundRequest, api_models::refunds::RefundType, api_models::refunds::RefundResponse, @@ -260,6 +262,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::PaymentMethodCreate, api_models::payment_methods::PaymentMethodResponse, api_models::payment_methods::CustomerPaymentMethod, + common_types::three_ds_decision_rule_engine::ThreeDSDecisionRule, api_models::payment_methods::PaymentMethodListResponse, api_models::payment_methods::ResponsePaymentMethodsEnabled, api_models::payment_methods::ResponsePaymentMethodTypes, @@ -672,6 +675,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::routing::SuccessRateSpecificityLevel, api_models::routing::ToggleDynamicRoutingQuery, api_models::routing::ToggleDynamicRoutingPath, + api_models::routing::ProgramThreeDsDecisionRule, + api_models::routing::RuleThreeDsDecisionRule, api_models::routing::RoutingVolumeSplitResponse, api_models::routing::ast::RoutableChoiceKind, api_models::enums::RoutableConnectors, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 3091467c9d..6bcc4c878a 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -195,6 +195,8 @@ Never share your secret api keys. Keep them guarded and secure. common_types::refunds::SplitRefund, common_types::payments::ConnectorChargeResponseData, common_types::payments::StripeChargeResponseData, + common_types::three_ds_decision_rule_engine::ThreeDSDecisionRule, + common_types::three_ds_decision_rule_engine::ThreeDSDecision, common_utils::request::Method, api_models::refunds::RefundsCreateRequest, api_models::refunds::RefundErrorDetails, @@ -671,6 +673,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::routing::ConnectorVolumeSplit, api_models::routing::ConnectorSelection, api_models::routing::ast::RoutableChoiceKind, + api_models::routing::ProgramThreeDsDecisionRule, + api_models::routing::RuleThreeDsDecisionRule, api_models::enums::RoutableConnectors, api_models::routing::ast::ProgramConnectorSelection, api_models::routing::ast::RuleConnectorSelection, diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index c26108fa4b..942a659dcc 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -4691,6 +4691,8 @@ impl ProfileWrapper { storage::enums::TransactionType::Payment => (Some(algorithm_id), None), #[cfg(feature = "payouts")] storage::enums::TransactionType::Payout => (None, Some(algorithm_id)), + //TODO: Handle ThreeDsAuthentication Transaction Type for Three DS Decision Rule Algorithm configuration + storage::enums::TransactionType::ThreeDsAuthentication => todo!(), }; let profile_update = domain::ProfileUpdate::RoutingAlgorithmUpdate { diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 31a2b11a04..16ba5a41a5 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -384,6 +384,8 @@ pub enum RoutingError { OpenRouterCallFailed, #[error("Error from open_router: {0}")] OpenRouterError(String), + #[error("Invalid transaction type")] + InvalidTransactionType, } #[derive(Debug, Clone, thiserror::Error)] diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 86925cff09..de1cf0e8a6 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -518,6 +518,9 @@ async fn ensure_algorithm_cached_v1( profile_id.get_string_repr() ) } + common_enums::TransactionType::ThreeDsAuthentication => { + Err(errors::RoutingError::InvalidTransactionType)? + } } }; @@ -625,6 +628,10 @@ pub async fn refresh_routing_cache_v1( CachedAlgorithm::Advanced(interpreter) } + api_models::routing::StaticRoutingAlgorithm::ThreeDsDecisionRule(_program) => { + Err(errors::RoutingError::InvalidRoutingAlgorithmStructure) + .attach_printable("Unsupported algorithm received")? + } }; let arc_cached_algorithm = Arc::new(cached_algorithm); @@ -722,6 +729,9 @@ pub async fn get_merchant_cgraph( profile_id.get_string_repr() ) } + api_enums::TransactionType::ThreeDsAuthentication => { + Err(errors::RoutingError::InvalidTransactionType)? + } } }; @@ -776,12 +786,18 @@ pub async fn refresh_cgraph_cache( merchant_connector_accounts .retain(|mca| mca.connector_type == storage_enums::ConnectorType::PayoutProcessor); } + api_enums::TransactionType::ThreeDsAuthentication => { + Err(errors::RoutingError::InvalidTransactionType)? + } }; let connector_type = match transaction_type { api_enums::TransactionType::Payment => common_enums::ConnectorType::PaymentProcessor, #[cfg(feature = "payouts")] api_enums::TransactionType::Payout => common_enums::ConnectorType::PayoutProcessor, + api_enums::TransactionType::ThreeDsAuthentication => { + Err(errors::RoutingError::InvalidTransactionType)? + } }; let merchant_connector_accounts = merchant_connector_accounts diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 1811f43d51..702763ccaf 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -145,7 +145,7 @@ pub async fn retrieve_merchant_routing_dictionary( merchant_context: domain::MerchantContext, profile_id_list: Option>, query_params: RoutingRetrieveQuery, - transaction_type: &enums::TransactionType, + transaction_type: enums::TransactionType, ) -> RouterResponse { metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE.add(1, &[]); @@ -153,7 +153,7 @@ pub async fn retrieve_merchant_routing_dictionary( .store .list_routing_algorithm_metadata_by_merchant_id_transaction_type( merchant_context.get_merchant_account().get_id(), - transaction_type, + &transaction_type, i64::from(query_params.limit.unwrap_or_default()), i64::from(query_params.offset.unwrap_or_default()), ) @@ -328,14 +328,16 @@ pub async fn create_routing_algorithm_under_profile( core_utils::validate_profile_id_from_auth_layer(authentication_profile_id, &business_profile)?; - helpers::validate_connectors_in_routing_config( - &state, - merchant_context.get_merchant_key_store(), - merchant_context.get_merchant_account().get_id(), - &profile_id, - &algorithm, - ) - .await?; + if algorithm.should_validate_connectors_in_routing_config() { + helpers::validate_connectors_in_routing_config( + &state, + merchant_context.get_merchant_key_store(), + merchant_context.get_merchant_account().get_id(), + &profile_id, + &algorithm, + ) + .await?; + } let mut decision_engine_routing_id: Option = None; @@ -471,7 +473,7 @@ pub async fn link_routing_config( merchant_context: domain::MerchantContext, authentication_profile_id: Option, algorithm_id: common_utils::id_type::RoutingId, - transaction_type: &enums::TransactionType, + transaction_type: enums::TransactionType, ) -> RouterResponse { metrics::ROUTING_LINK_CONFIG.add(1, &[]); let db = state.store.as_ref(); @@ -641,7 +643,8 @@ pub async fn link_routing_config( diesel_models::enums::RoutingAlgorithmKind::Single | diesel_models::enums::RoutingAlgorithmKind::Priority | diesel_models::enums::RoutingAlgorithmKind::Advanced - | diesel_models::enums::RoutingAlgorithmKind::VolumeSplit => { + | diesel_models::enums::RoutingAlgorithmKind::VolumeSplit + | diesel_models::enums::RoutingAlgorithmKind::ThreeDsDecisionRule => { let mut routing_ref: routing_types::RoutingAlgorithmRef = business_profile .routing_algorithm .clone() @@ -653,7 +656,7 @@ pub async fn link_routing_config( )? .unwrap_or_default(); - utils::when(routing_algorithm.algorithm_for != *transaction_type, || { + utils::when(routing_algorithm.algorithm_for != transaction_type, || { Err(errors::ApiErrorResponse::PreconditionFailed { message: format!( "Cannot use {}'s routing algorithm for {} operation", @@ -677,7 +680,7 @@ pub async fn link_routing_config( merchant_context.get_merchant_key_store(), business_profile.clone(), routing_ref, - transaction_type, + &transaction_type, ) .await?; } @@ -815,6 +818,8 @@ pub async fn unlink_routing_config_under_profile( enums::TransactionType::Payment => business_profile.routing_algorithm_id.clone(), #[cfg(feature = "payouts")] enums::TransactionType::Payout => business_profile.payout_routing_algorithm_id.clone(), + // TODO: Handle ThreeDsAuthentication Transaction Type for Three DS Decision Rule Algorithm configuration + enums::TransactionType::ThreeDsAuthentication => todo!(), }; if let Some(algorithm_id) = routing_algo_id { @@ -849,7 +854,7 @@ pub async fn unlink_routing_config( merchant_context: domain::MerchantContext, request: routing_types::RoutingConfigRequest, authentication_profile_id: Option, - transaction_type: &enums::TransactionType, + transaction_type: enums::TransactionType, ) -> RouterResponse { metrics::ROUTING_UNLINK_CONFIG.add(1, &[]); @@ -883,6 +888,9 @@ pub async fn unlink_routing_config( enums::TransactionType::Payment => business_profile.routing_algorithm.clone(), #[cfg(feature = "payouts")] enums::TransactionType::Payout => business_profile.payout_routing_algorithm.clone(), + enums::TransactionType::ThreeDsAuthentication => { + business_profile.three_ds_decision_rule_algorithm.clone() + } } .map(|val| val.parse_value("RoutingAlgorithmRef")) .transpose() @@ -916,7 +924,7 @@ pub async fn unlink_routing_config( merchant_context.get_merchant_key_store(), business_profile, routing_algorithm, - transaction_type, + &transaction_type, ) .await?; @@ -1171,7 +1179,7 @@ pub async fn retrieve_linked_routing_config( merchant_context: domain::MerchantContext, authentication_profile_id: Option, query_params: routing_types::RoutingRetrieveLinkQuery, - transaction_type: &enums::TransactionType, + transaction_type: enums::TransactionType, ) -> RouterResponse { metrics::ROUTING_RETRIEVE_LINK_CONFIG.add(1, &[]); @@ -1216,6 +1224,9 @@ pub async fn retrieve_linked_routing_config( enums::TransactionType::Payment => &business_profile.routing_algorithm, #[cfg(feature = "payouts")] enums::TransactionType::Payout => &business_profile.payout_routing_algorithm, + enums::TransactionType::ThreeDsAuthentication => { + &business_profile.three_ds_decision_rule_algorithm + } } .clone() .map(|val| val.parse_value("RoutingAlgorithmRef")) diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index a29f25331e..7ba06ff5e0 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -242,25 +242,18 @@ pub async fn update_profile_active_algorithm_ref( let profile_id = current_business_profile.get_id().to_owned(); - let routing_cache_key = cache::CacheKind::Routing( - format!( - "routing_config_{}_{}", - merchant_id.get_string_repr(), - profile_id.get_string_repr(), - ) - .into(), - ); - - let (routing_algorithm, payout_routing_algorithm) = match transaction_type { - storage::enums::TransactionType::Payment => (Some(ref_val), None), - #[cfg(feature = "payouts")] - storage::enums::TransactionType::Payout => (None, Some(ref_val)), - }; + let (routing_algorithm, payout_routing_algorithm, three_ds_decision_rule_algorithm) = + match transaction_type { + storage::enums::TransactionType::Payment => (Some(ref_val), None, None), + #[cfg(feature = "payouts")] + storage::enums::TransactionType::Payout => (None, Some(ref_val), None), + storage::enums::TransactionType::ThreeDsAuthentication => (None, None, Some(ref_val)), + }; let business_profile_update = domain::ProfileUpdate::RoutingAlgorithmUpdate { routing_algorithm, payout_routing_algorithm, - three_ds_decision_rule_algorithm: None, + three_ds_decision_rule_algorithm, }; db.update_profile_by_profile_id( @@ -273,10 +266,22 @@ pub async fn update_profile_active_algorithm_ref( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update routing algorithm ref in business profile")?; - cache::redact_from_redis_and_publish(db.get_cache_store().as_ref(), [routing_cache_key]) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to invalidate routing cache")?; + // Invalidate the routing cache for Payments and Payouts transaction types + if !transaction_type.is_three_ds_authentication() { + let routing_cache_key = cache::CacheKind::Routing( + format!( + "routing_config_{}_{}", + merchant_id.get_string_repr(), + profile_id.get_string_repr(), + ) + .into(), + ); + + cache::redact_from_redis_and_publish(db.get_cache_store().as_ref(), [routing_cache_key]) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to invalidate routing cache")?; + } Ok(()) } @@ -452,6 +457,12 @@ impl RoutingAlgorithmHelpers<'_> { check_connector_selection(&rule.connector_selection)?; } } + + routing_types::StaticRoutingAlgorithm::ThreeDsDecisionRule(_) => { + return Err(errors::ApiErrorResponse::InternalServerError).attach_printable( + "Invalid routing algorithm three_ds decision rule received", + )?; + } } Ok(()) @@ -560,6 +571,11 @@ pub async fn validate_connectors_in_routing_config( check_connector_selection(&rule.connector_selection)?; } } + + routing_types::StaticRoutingAlgorithm::ThreeDsDecisionRule(_) => { + Err(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid routing algorithm three_ds decision rule received")? + } } Ok(()) @@ -581,6 +597,9 @@ pub fn get_default_config_key( storage::enums::TransactionType::Payment => format!("routing_default_{merchant_id}"), #[cfg(feature = "payouts")] storage::enums::TransactionType::Payout => format!("routing_default_po_{merchant_id}"), + storage::enums::TransactionType::ThreeDsAuthentication => { + format!("three_ds_authentication_{merchant_id}") + } } } diff --git a/crates/router/src/core/routing/transformers.rs b/crates/router/src/core/routing/transformers.rs index 4face88799..6560e6786c 100644 --- a/crates/router/src/core/routing/transformers.rs +++ b/crates/router/src/core/routing/transformers.rs @@ -91,6 +91,7 @@ impl ForeignFrom for RoutingAlgorithmKind { storage_enums::RoutingAlgorithmKind::VolumeSplit => Self::VolumeSplit, storage_enums::RoutingAlgorithmKind::Advanced => Self::Advanced, storage_enums::RoutingAlgorithmKind::Dynamic => Self::Dynamic, + storage_enums::RoutingAlgorithmKind::ThreeDsDecisionRule => Self::ThreeDsDecisionRule, } } } @@ -103,6 +104,7 @@ impl ForeignFrom for storage_enums::RoutingAlgorithmKind { RoutingAlgorithmKind::VolumeSplit => Self::VolumeSplit, RoutingAlgorithmKind::Advanced => Self::Advanced, RoutingAlgorithmKind::Dynamic => Self::Dynamic, + RoutingAlgorithmKind::ThreeDsDecisionRule => Self::ThreeDsDecisionRule, } } } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 7e082296e7..18201380c3 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -858,43 +858,23 @@ impl Routing { .app_data(web::Data::new(state.clone())) .service( web::resource("/active").route(web::get().to(|state, req, query_params| { - routing::routing_retrieve_linked_config( - state, - req, - query_params, - &TransactionType::Payment, - ) + routing::routing_retrieve_linked_config(state, req, query_params, None) })), ) .service( web::resource("") .route( web::get().to(|state, req, path: web::Query| { - routing::list_routing_configs( - state, - req, - path, - &TransactionType::Payment, - ) + routing::list_routing_configs(state, req, path, None) }), ) .route(web::post().to(|state, req, payload| { - routing::routing_create_config( - state, - req, - payload, - TransactionType::Payment, - ) + routing::routing_create_config(state, req, payload, None) })), ) .service(web::resource("/list/profile").route(web::get().to( |state, req, query: web::Query| { - routing::list_routing_configs_for_profile( - state, - req, - query, - &TransactionType::Payment, - ) + routing::list_routing_configs_for_profile(state, req, query, None) }, ))) .service( @@ -909,7 +889,7 @@ impl Routing { ) .service( web::resource("/deactivate").route(web::post().to(|state, req, payload| { - routing::routing_unlink_config(state, req, payload, &TransactionType::Payment) + routing::routing_unlink_config(state, req, payload, None) })), ) .service( @@ -954,7 +934,7 @@ impl Routing { state, req, path, - &TransactionType::Payout, + Some(TransactionType::Payout), ) }, )) @@ -963,7 +943,7 @@ impl Routing { state, req, payload, - TransactionType::Payout, + Some(TransactionType::Payout), ) })), ) @@ -973,7 +953,7 @@ impl Routing { state, req, query, - &TransactionType::Payout, + Some(TransactionType::Payout), ) }, ))) @@ -983,7 +963,7 @@ impl Routing { state, req, query_params, - &TransactionType::Payout, + Some(TransactionType::Payout), ) }, ))) @@ -1007,8 +987,14 @@ impl Routing { ) .service( web::resource("/payouts/{algorithm_id}/activate").route(web::post().to( - |state, req, path| { - routing::routing_link_config(state, req, path, &TransactionType::Payout) + |state, req, path, payload| { + routing::routing_link_config( + state, + req, + path, + payload, + Some(TransactionType::Payout), + ) }, )), ) @@ -1018,7 +1004,7 @@ impl Routing { state, req, payload, - &TransactionType::Payout, + Some(TransactionType::Payout), ) }, ))) @@ -1053,8 +1039,8 @@ impl Routing { ) .service( web::resource("/{algorithm_id}/activate").route(web::post().to( - |state, req, path| { - routing::routing_link_config(state, req, path, &TransactionType::Payment) + |state, req, payload, path| { + routing::routing_link_config(state, req, path, payload, None) }, )), ); diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index ff8ad06e91..eddac5479e 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -22,7 +22,7 @@ pub async fn routing_create_config( state: web::Data, req: HttpRequest, json_payload: web::Json, - transaction_type: enums::TransactionType, + transaction_type: Option, ) -> impl Responder { let flow = Flow::RoutingCreateConfig; Box::pin(oss_api::server_wrap( @@ -38,8 +38,10 @@ pub async fn routing_create_config( state, merchant_context, auth.profile_id, - payload, - transaction_type, + payload.clone(), + transaction_type + .or(payload.transaction_type) + .unwrap_or(enums::TransactionType::Payment), ) }, auth::auth_type( @@ -109,7 +111,8 @@ pub async fn routing_link_config( state: web::Data, req: HttpRequest, path: web::Path, - transaction_type: &enums::TransactionType, + json_payload: web::Json, + transaction_type: Option, ) -> impl Responder { let flow = Flow::RoutingLinkConfig; Box::pin(oss_api::server_wrap( @@ -126,7 +129,9 @@ pub async fn routing_link_config( merchant_context, auth.profile_id, algorithm, - transaction_type, + transaction_type + .or(json_payload.transaction_type) + .unwrap_or(enums::TransactionType::Payment), ) }, auth::auth_type( @@ -289,7 +294,7 @@ pub async fn list_routing_configs( state: web::Data, req: HttpRequest, query: web::Query, - transaction_type: &enums::TransactionType, + transaction_type: Option, ) -> impl Responder { let flow = Flow::RoutingRetrieveDictionary; Box::pin(oss_api::server_wrap( @@ -305,8 +310,10 @@ pub async fn list_routing_configs( state, merchant_context, None, - query_params, - transaction_type, + query_params.clone(), + transaction_type + .or(query_params.transaction_type) + .unwrap_or(enums::TransactionType::Payment), ) }, auth::auth_type( @@ -330,7 +337,7 @@ pub async fn list_routing_configs_for_profile( state: web::Data, req: HttpRequest, query: web::Query, - transaction_type: &enums::TransactionType, + transaction_type: Option, ) -> impl Responder { let flow = Flow::RoutingRetrieveDictionary; Box::pin(oss_api::server_wrap( @@ -346,8 +353,10 @@ pub async fn list_routing_configs_for_profile( state, merchant_context, auth.profile_id.map(|profile_id| vec![profile_id]), - query_params, - transaction_type, + query_params.clone(), + transaction_type + .or(query_params.transaction_type) + .unwrap_or(enums::TransactionType::Payment), ) }, auth::auth_type( @@ -419,7 +428,7 @@ pub async fn routing_unlink_config( state: web::Data, req: HttpRequest, payload: web::Json, - transaction_type: &enums::TransactionType, + transaction_type: Option, ) -> impl Responder { let flow = Flow::RoutingUnlinkConfig; Box::pin(oss_api::server_wrap( @@ -434,9 +443,11 @@ pub async fn routing_unlink_config( routing::unlink_routing_config( state, merchant_context, - payload_req, + payload_req.clone(), auth.profile_id, - transaction_type, + transaction_type + .or(payload_req.transaction_type) + .unwrap_or(enums::TransactionType::Payment), ) }, auth::auth_type( @@ -941,7 +952,7 @@ pub async fn routing_retrieve_linked_config( state: web::Data, req: HttpRequest, query: web::Query, - transaction_type: &enums::TransactionType, + transaction_type: Option, ) -> impl Responder { use crate::services::authentication::AuthenticationData; let flow = Flow::RoutingRetrieveActiveConfig; @@ -961,7 +972,9 @@ pub async fn routing_retrieve_linked_config( merchant_context, auth.profile_id, query_params, - transaction_type, + transaction_type + .or(query.transaction_type) + .unwrap_or(enums::TransactionType::Payment), ) }, auth::auth_type( @@ -993,7 +1006,9 @@ pub async fn routing_retrieve_linked_config( merchant_context, auth.profile_id, query_params, - transaction_type, + transaction_type + .or(query.transaction_type) + .unwrap_or(enums::TransactionType::Payment), ) }, auth::auth_type( diff --git a/migrations/2025-05-22-191239_add-three-ds-decision-rule-to-algorithm-kind/down.sql b/migrations/2025-05-22-191239_add-three-ds-decision-rule-to-algorithm-kind/down.sql new file mode 100644 index 0000000000..c7c9cbeb40 --- /dev/null +++ b/migrations/2025-05-22-191239_add-three-ds-decision-rule-to-algorithm-kind/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; \ No newline at end of file diff --git a/migrations/2025-05-22-191239_add-three-ds-decision-rule-to-algorithm-kind/up.sql b/migrations/2025-05-22-191239_add-three-ds-decision-rule-to-algorithm-kind/up.sql new file mode 100644 index 0000000000..a7f747ba40 --- /dev/null +++ b/migrations/2025-05-22-191239_add-three-ds-decision-rule-to-algorithm-kind/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TYPE "RoutingAlgorithmKind" ADD VALUE 'three_ds_decision_rule'; diff --git a/migrations/2025-05-24-205102_add-three-ds-authentication-to-transaction-type/down.sql b/migrations/2025-05-24-205102_add-three-ds-authentication-to-transaction-type/down.sql new file mode 100644 index 0000000000..2a3866c86d --- /dev/null +++ b/migrations/2025-05-24-205102_add-three-ds-authentication-to-transaction-type/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; diff --git a/migrations/2025-05-24-205102_add-three-ds-authentication-to-transaction-type/up.sql b/migrations/2025-05-24-205102_add-three-ds-authentication-to-transaction-type/up.sql new file mode 100644 index 0000000000..3e1aff6b13 --- /dev/null +++ b/migrations/2025-05-24-205102_add-three-ds-authentication-to-transaction-type/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TYPE "TransactionType" ADD VALUE 'three_ds_authentication';