diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index ba795d4059..a5dc3a83f2 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -15042,7 +15042,8 @@ "created", "payment_method_type", "payment_method_subtype", - "merchant_connector_id" + "merchant_connector_id", + "applied_authentication_type" ], "properties": { "id": { @@ -15141,6 +15142,17 @@ } ], "nullable": true + }, + "authentication_type": { + "allOf": [ + { + "$ref": "#/components/schemas/AuthenticationType" + } + ], + "nullable": true + }, + "applied_authentication_type": { + "$ref": "#/components/schemas/AuthenticationType" } } }, @@ -15499,7 +15511,6 @@ "client_secret", "profile_id", "capture_method", - "authentication_type", "customer_id", "customer_present", "setup_future_usage", @@ -15551,7 +15562,7 @@ "$ref": "#/components/schemas/AuthenticationType" } ], - "default": "no_three_ds" + "nullable": true }, "billing": { "allOf": [ diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 820992025c..cc70520751 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -438,8 +438,9 @@ pub struct PaymentsIntentResponse { #[schema(value_type = CaptureMethod, example = "automatic")] pub capture_method: api_enums::CaptureMethod, - #[schema(value_type = AuthenticationType, example = "no_three_ds", default = "no_three_ds")] - pub authentication_type: api_enums::AuthenticationType, + /// The authentication type for the payment + #[schema(value_type = Option, example = "no_three_ds")] + pub authentication_type: Option, /// The billing details of the payment. This address will be used for invoicing. #[schema(value_type = Option
)] @@ -5270,6 +5271,14 @@ pub struct PaymentsConfirmIntentResponse { /// Error details for the payment if any pub error: Option, + + /// The transaction authentication can be set to undergo payer authentication. By default, the authentication will be marked as NO_THREE_DS + #[schema(value_type = Option, example = "no_three_ds")] + pub authentication_type: Option, + + /// The authentication type applied for the payment + #[schema(value_type = AuthenticationType, example = "no_three_ds")] + pub applied_authentication_type: api_enums::AuthenticationType, } /// Token information that can be used to initiate transactions by the merchant. diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index b3b3f44f3c..2391422ee0 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -782,7 +782,7 @@ pub enum PaymentAttemptUpdate { #[diesel(table_name = payment_attempt)] pub struct PaymentAttemptUpdateInternal { pub status: Option, - // authentication_type: Option, + pub authentication_type: Option, pub error_message: Option, pub connector_payment_id: Option, // payment_method_id: Option, diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 939d9fee0e..2457448f21 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -351,7 +351,7 @@ pub struct PaymentIntent { /// Capture method for the payment pub capture_method: storage_enums::CaptureMethod, /// Authentication type that is requested by the merchant for this payment. - pub authentication_type: common_enums::AuthenticationType, + pub authentication_type: Option, /// This contains the pre routing results that are done when routing is done during listing the payment methods. pub prerouting_algorithm: Option, /// The organization id for the payment. This is derived from the merchant account @@ -498,7 +498,7 @@ impl PaymentIntent { .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) .attach_printable("Unable to decode shipping address")?, capture_method: request.capture_method.unwrap_or_default(), - authentication_type: request.authentication_type.unwrap_or_default(), + authentication_type: request.authentication_type, prerouting_algorithm: None, organization_id: merchant_account.organization_id.clone(), enable_payment_link: request.payment_link_enabled.unwrap_or_default(), diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index 133d04ceb6..ee8b24c27b 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -489,6 +489,7 @@ impl PaymentAttempt { .transpose() .change_context(errors::api_error_response::ApiErrorResponse::InternalServerError) .attach_printable("Unable to decode billing address")?; + let authentication_type = payment_intent.authentication_type.unwrap_or_default(); Ok(Self { payment_id: payment_intent.id.clone(), @@ -498,7 +499,7 @@ impl PaymentAttempt { // This will be decided by the routing algorithm and updated in update trackers // right before calling the connector connector: None, - authentication_type: payment_intent.authentication_type, + authentication_type, created_at: now, modified_at: now, last_synced: None, @@ -1453,6 +1454,7 @@ pub enum PaymentAttemptUpdate { updated_by: String, connector: String, merchant_connector_id: id_type::MerchantConnectorAccountId, + authentication_type: storage_enums::AuthenticationType, }, /// Update the payment attempt on confirming the intent, after calling the connector on success response ConfirmIntentResponse(Box), @@ -2108,6 +2110,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal updated_by, connector, merchant_connector_id, + authentication_type, } => Self { status: Some(status), error_message: None, @@ -2126,6 +2129,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal amount_capturable: None, amount_to_capture: None, connector_token_details: None, + authentication_type: Some(authentication_type), }, PaymentAttemptUpdate::ErrorUpdate { status, @@ -2151,6 +2155,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal amount_capturable, amount_to_capture: None, connector_token_details: None, + authentication_type: None, }, PaymentAttemptUpdate::ConfirmIntentResponse(confirm_intent_response_update) => { let ConfirmIntentResponseUpdate { @@ -2181,6 +2186,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector_metadata, amount_to_capture: None, connector_token_details, + authentication_type: None, } } PaymentAttemptUpdate::SyncUpdate { @@ -2205,6 +2211,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector_metadata: None, amount_to_capture: None, connector_token_details: None, + authentication_type: None, }, PaymentAttemptUpdate::CaptureUpdate { status, @@ -2228,6 +2235,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal redirection_data: None, connector_metadata: None, connector_token_details: None, + authentication_type: None, }, PaymentAttemptUpdate::PreCaptureUpdate { amount_to_capture, @@ -2250,6 +2258,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector_metadata: None, amount_capturable: None, connector_token_details: None, + authentication_type: None, }, } } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 2406e5f4b1..cd16fbcb25 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -1387,7 +1387,7 @@ impl behaviour::Conversion for PaymentIntent { shipping_address: shipping_address.map(Encryption::from), capture_method: Some(capture_method), id, - authentication_type: Some(authentication_type), + authentication_type, prerouting_algorithm, merchant_reference_id, surcharge_amount: amount_details.surcharge_amount, @@ -1521,7 +1521,7 @@ impl behaviour::Conversion for PaymentIntent { id: storage_model.id, merchant_reference_id: storage_model.merchant_reference_id, organization_id: storage_model.organization_id, - authentication_type: storage_model.authentication_type.unwrap_or_default(), + authentication_type: storage_model.authentication_type, prerouting_algorithm: storage_model.prerouting_algorithm, enable_payment_link: storage_model.enable_payment_link.into(), apply_mit_exemption: storage_model.apply_mit_exemption.into(), @@ -1592,7 +1592,7 @@ impl behaviour::Conversion for PaymentIntent { capture_method: Some(self.capture_method), id: self.id, merchant_reference_id: self.merchant_reference_id, - authentication_type: Some(self.authentication_type), + authentication_type: self.authentication_type, prerouting_algorithm: self.prerouting_algorithm, surcharge_amount: amount_details.surcharge_amount, tax_on_surcharge: amount_details.tax_on_surcharge, diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index b3f1eab9d9..0e915dd770 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -2025,7 +2025,7 @@ impl DefaultFallbackRoutingConfigUpdate<'_> { }; if default_routing_config_for_profile.contains(&choice.clone()) { default_routing_config_for_profile.retain(|mca| { - (mca.merchant_connector_id.as_ref() != Some(self.merchant_connector_id)) + mca.merchant_connector_id.as_ref() != Some(self.merchant_connector_id) }); profile_wrapper diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 1b4d00882a..c81a40216c 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -176,6 +176,13 @@ where .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) .attach_printable("Failed while fetching/creating customer")?; + operation + .to_domain()? + .run_decision_manager(state, &mut payment_data, &profile) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to run decision manager")?; + let connector = operation .to_domain()? .perform_routing( @@ -1152,17 +1159,30 @@ where // TODO: Move to business profile surcharge column #[instrument(skip_all)] #[cfg(feature = "v2")] -pub async fn call_decision_manager( +pub fn call_decision_manager( state: &SessionState, - merchant_account: &domain::MerchantAccount, - _business_profile: &domain::Profile, - payment_data: &D, + record: common_types::payments::DecisionManagerRecord, + payment_data: &PaymentConfirmData, ) -> RouterResult> where F: Clone, - D: OperationSessionGetters, { - todo!() + let payment_method_data = payment_data.get_payment_method_data(); + let payment_dsl_data = core_routing::PaymentsDslInput::new( + None, + payment_data.get_payment_attempt(), + payment_data.get_payment_intent(), + payment_method_data, + payment_data.get_address(), + None, + payment_data.get_currency(), + ); + + let output = perform_decision_management(record, &payment_dsl_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the conditional config")?; + + Ok(output.override_3ds) } #[cfg(feature = "v2")] diff --git a/crates/router/src/core/payments/conditional_configs.rs b/crates/router/src/core/payments/conditional_configs.rs index d511c0fd6a..55e5c657b1 100644 --- a/crates/router/src/core/payments/conditional_configs.rs +++ b/crates/router/src/core/payments/conditional_configs.rs @@ -6,6 +6,8 @@ use router_env::{instrument, tracing}; use storage_impl::redis::cache::{self, DECISION_MANAGER_CACHE}; use super::routing::make_dsl_input; +#[cfg(feature = "v2")] +use crate::{core::errors::RouterResult, types::domain}; use crate::{ core::{errors, errors::ConditionalConfigError as ConfigError, routing as core_routing}, routes, @@ -13,6 +15,7 @@ use crate::{ pub type ConditionalConfigResult = errors::CustomResult; #[instrument(skip_all)] +#[cfg(feature = "v1")] pub async fn perform_decision_management( state: &routes::SessionState, algorithm_ref: routing::RoutingAlgorithmRef, @@ -57,6 +60,23 @@ pub async fn perform_decision_management( execute_dsl_and_get_conditional_config(backend_input, &interpreter) } +#[cfg(feature = "v2")] +pub fn perform_decision_management( + record: common_types::payments::DecisionManagerRecord, + payment_data: &core_routing::PaymentsDslInput<'_>, +) -> RouterResult { + let interpreter = backend::VirInterpreterBackend::with_program(record.program) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error initializing DSL interpreter backend")?; + + let backend_input = make_dsl_input(payment_data) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error constructing DSL input")?; + execute_dsl_and_get_conditional_config(backend_input, &interpreter) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error executing DSL") +} + pub fn execute_dsl_and_get_conditional_config( backend_input: dsl_inputs::BackendInput, interpreter: &backend::VirInterpreterBackend, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 911530ba8e..e345abbf9c 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -231,6 +231,17 @@ pub trait Domain: Send + Sync { storage_scheme: enums::MerchantStorageScheme, ) -> CustomResult<(BoxedOperation<'a, F, R, D>, Option), errors::StorageError>; + #[cfg(feature = "v2")] + /// This will run the decision manager for the payment + async fn run_decision_manager<'a>( + &'a self, + state: &SessionState, + payment_data: &mut D, + business_profile: &domain::Profile, + ) -> CustomResult<(), errors::ApiErrorResponse> { + Ok(()) + } + #[allow(clippy::too_many_arguments)] async fn make_pm_data<'a>( &'a self, diff --git a/crates/router/src/core/payments/operations/payment_confirm_intent.rs b/crates/router/src/core/payments/operations/payment_confirm_intent.rs index 71d99d03e4..043ee3378b 100644 --- a/crates/router/src/core/payments/operations/payment_confirm_intent.rs +++ b/crates/router/src/core/payments/operations/payment_confirm_intent.rs @@ -17,9 +17,10 @@ use crate::{ admin, errors::{self, CustomResult, RouterResult, StorageErrorExt}, payments::{ - self, helpers, + self, call_decision_manager, helpers, operations::{self, ValidateStatusForOperation}, - populate_surcharge_details, CustomerDetails, PaymentAddress, PaymentData, + populate_surcharge_details, CustomerDetails, OperationSessionSetters, PaymentAddress, + PaymentData, }, utils as core_utils, }, @@ -290,6 +291,30 @@ impl Domain( + &'a self, + state: &SessionState, + payment_data: &mut PaymentConfirmData, + business_profile: &domain::Profile, + ) -> CustomResult<(), errors::ApiErrorResponse> { + let authentication_type = payment_data.payment_intent.authentication_type; + + let authentication_type = match business_profile.three_ds_decision_manager_config.as_ref() { + Some(three_ds_decision_manager_config) => call_decision_manager( + state, + three_ds_decision_manager_config.clone(), + payment_data, + )?, + None => authentication_type, + }; + + if let Some(auth_type) = authentication_type { + payment_data.payment_attempt.authentication_type = auth_type; + } + + Ok(()) + } + #[instrument(skip_all)] async fn make_pm_data<'a>( &'a self, @@ -397,11 +422,14 @@ impl UpdateTracker, PaymentsConfirmInt active_attempt_id: payment_data.payment_attempt.id.clone(), }; + let authentication_type = payment_data.payment_attempt.authentication_type; + let payment_attempt_update = hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::ConfirmIntent { status: attempt_status, updated_by: storage_scheme.to_string(), connector, merchant_connector_id, + authentication_type, }; let updated_payment_intent = db diff --git a/crates/router/src/core/payments/operations/payment_update_intent.rs b/crates/router/src/core/payments/operations/payment_update_intent.rs index 4782d237e2..2931c11b07 100644 --- a/crates/router/src/core/payments/operations/payment_update_intent.rs +++ b/crates/router/src/core/payments/operations/payment_update_intent.rs @@ -259,7 +259,7 @@ impl GetTracker, PaymentsUpda .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to decode shipping address")?, capture_method: capture_method.unwrap_or(payment_intent.capture_method), - authentication_type: authentication_type.unwrap_or(payment_intent.authentication_type), + authentication_type: authentication_type.or(payment_intent.authentication_type), payment_link_config: payment_link_config .map(ApiModelToDieselModelConvertor::convert_from) .or(payment_intent.payment_link_config), @@ -324,7 +324,7 @@ impl UpdateTracker, PaymentsUpdateIn tax_on_surcharge: intent.amount_details.tax_on_surcharge, routing_algorithm_id: intent.routing_algorithm_id, capture_method: Some(intent.capture_method), - authentication_type: Some(intent.authentication_type), + authentication_type: intent.authentication_type, billing_address: intent.billing_address, shipping_address: intent.shipping_address, customer_present: Some(intent.customer_present), diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 41be7e6e2d..cf5cd35fb9 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -173,7 +173,112 @@ pub fn make_dsl_input_for_payouts( pub fn make_dsl_input( payments_dsl_input: &routing::PaymentsDslInput<'_>, ) -> RoutingResult { - todo!() + let mandate_data = dsl_inputs::MandateData { + mandate_acceptance_type: payments_dsl_input.setup_mandate.as_ref().and_then( + |mandate_data| { + mandate_data + .customer_acceptance + .as_ref() + .map(|customer_accept| match customer_accept.acceptance_type { + hyperswitch_domain_models::mandates::AcceptanceType::Online => { + euclid_enums::MandateAcceptanceType::Online + } + hyperswitch_domain_models::mandates::AcceptanceType::Offline => { + euclid_enums::MandateAcceptanceType::Offline + } + }) + }, + ), + mandate_type: payments_dsl_input + .setup_mandate + .as_ref() + .and_then(|mandate_data| { + mandate_data + .mandate_type + .clone() + .map(|mandate_type| match mandate_type { + hyperswitch_domain_models::mandates::MandateDataType::SingleUse(_) => { + euclid_enums::MandateType::SingleUse + } + hyperswitch_domain_models::mandates::MandateDataType::MultiUse(_) => { + euclid_enums::MandateType::MultiUse + } + }) + }), + payment_type: Some( + if payments_dsl_input + .recurring_details + .as_ref() + .is_some_and(|data| { + matches!( + data, + api_models::mandates::RecurringDetails::ProcessorPaymentToken(_) + ) + }) + { + euclid_enums::PaymentType::PptMandate + } else { + payments_dsl_input.setup_mandate.map_or_else( + || euclid_enums::PaymentType::NonMandate, + |_| euclid_enums::PaymentType::SetupMandate, + ) + }, + ), + }; + let payment_method_input = dsl_inputs::PaymentMethodInput { + payment_method: Some(payments_dsl_input.payment_attempt.payment_method_type), + payment_method_type: Some(payments_dsl_input.payment_attempt.payment_method_subtype), + card_network: payments_dsl_input + .payment_method_data + .as_ref() + .and_then(|pm_data| match pm_data { + domain::PaymentMethodData::Card(card) => card.card_network.clone(), + + _ => None, + }), + }; + + let payment_input = dsl_inputs::PaymentInput { + amount: payments_dsl_input + .payment_attempt + .amount_details + .get_net_amount(), + card_bin: payments_dsl_input.payment_method_data.as_ref().and_then( + |pm_data| match pm_data { + domain::PaymentMethodData::Card(card) => Some(card.card_number.get_card_isin()), + _ => None, + }, + ), + currency: payments_dsl_input.currency, + authentication_type: Some(payments_dsl_input.payment_attempt.authentication_type), + capture_method: Some(payments_dsl_input.payment_intent.capture_method), + business_country: None, + billing_country: payments_dsl_input + .address + .get_payment_method_billing() + .and_then(|billing_address| billing_address.address.as_ref()) + .and_then(|address_details| address_details.country) + .map(api_enums::Country::from_alpha2), + business_label: None, + setup_future_usage: Some(payments_dsl_input.payment_intent.setup_future_usage), + }; + + let metadata = payments_dsl_input + .payment_intent + .metadata + .clone() + .map(|value| value.parse_value("routing_parameters")) + .transpose() + .change_context(errors::RoutingError::MetadataParsingError) + .attach_printable("Unable to parse routing_parameters from metadata of payment_intent") + .unwrap_or(None); + + Ok(dsl_inputs::BackendInput { + metadata, + payment: payment_input, + payment_method: payment_method_input, + mandate: mandate_data, + }) } #[cfg(feature = "v1")] @@ -185,7 +290,7 @@ pub fn make_dsl_input( |mandate_data| { mandate_data .customer_acceptance - .clone() + .as_ref() .map(|cat| match cat.acceptance_type { hyperswitch_domain_models::mandates::AcceptanceType::Online => { euclid_enums::MandateAcceptanceType::Online diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index ebf837792d..3075c9f26d 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -787,7 +787,10 @@ pub async fn construct_payment_router_data_for_sdk_session<'a>( .map(ToOwned::to_owned), // TODO: Create unified address address: hyperswitch_domain_models::payment_address::PaymentAddress::default(), - auth_type: payment_data.payment_intent.authentication_type, + auth_type: payment_data + .payment_intent + .authentication_type + .unwrap_or_default(), connector_meta_data: merchant_connector_account.get_metadata(), connector_wallets_details: None, request, @@ -1647,6 +1650,8 @@ where merchant_connector_id, browser_info: None, error, + authentication_type: payment_intent.authentication_type, + applied_authentication_type: payment_attempt.authentication_type, }; Ok(services::ApplicationResponse::JsonWithHeaders((