diff --git a/crates/api_models/src/webhooks.rs b/crates/api_models/src/webhooks.rs index 25e1f5827a..df9b1249c7 100644 --- a/crates/api_models/src/webhooks.rs +++ b/crates/api_models/src/webhooks.rs @@ -38,6 +38,7 @@ pub enum IncomingWebhookEvent { MandateActive, MandateRevoked, EndpointVerification, + ExternalAuthenticationARes, } pub enum WebhookFlow { @@ -48,6 +49,7 @@ pub enum WebhookFlow { ReturnResponse, BankTransfer, Mandate, + ExternalAuthentication, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -116,6 +118,7 @@ impl From for WebhookFlow { IncomingWebhookEvent::EndpointVerification => Self::ReturnResponse, IncomingWebhookEvent::SourceChargeable | IncomingWebhookEvent::SourceTransactionCreated => Self::BankTransfer, + IncomingWebhookEvent::ExternalAuthenticationARes => Self::ExternalAuthentication, } } } @@ -134,11 +137,18 @@ pub enum MandateIdType { ConnectorMandateId(String), } +#[derive(Clone)] +pub enum AuthenticationIdType { + AuthenticationId(String), + ConnectorAuthenticationId(String), +} + #[derive(Clone)] pub enum ObjectReferenceId { PaymentId(payments::PaymentIdType), RefundId(RefundIdType), MandateId(MandateIdType), + ExternalAuthenticationID(AuthenticationIdType), } pub struct IncomingWebhookDetails { diff --git a/crates/diesel_models/src/authentication.rs b/crates/diesel_models/src/authentication.rs index 8e7aa5f2a9..61f64dbaa3 100644 --- a/crates/diesel_models/src/authentication.rs +++ b/crates/diesel_models/src/authentication.rs @@ -40,6 +40,9 @@ pub struct Authentication { pub acs_trans_id: Option, pub three_ds_server_trans_id: Option, pub acs_signed_content: Option, + pub profile_id: String, + pub payment_id: Option, + pub merchant_connector_id: String, } impl Authentication { @@ -82,6 +85,9 @@ pub struct AuthenticationNew { pub acs_trans_id: Option, pub three_dsserver_trans_id: Option, pub acs_signed_content: Option, + pub profile_id: String, + pub payment_id: Option, + pub merchant_connector_id: String, } #[derive(Debug)] diff --git a/crates/diesel_models/src/query/authentication.rs b/crates/diesel_models/src/query/authentication.rs index 60e635ad12..86ee4a35c1 100644 --- a/crates/diesel_models/src/query/authentication.rs +++ b/crates/diesel_models/src/query/authentication.rs @@ -69,4 +69,18 @@ impl Authentication { ) .await } + + pub async fn find_authentication_by_merchant_id_connector_authentication_id( + conn: &PgPooledConn, + merchant_id: &str, + connector_authentication_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::connector_authentication_id.eq(connector_authentication_id.to_owned())), + ) + .await + } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index ad30609a77..f4255bfd73 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -111,6 +111,12 @@ diesel::table! { acs_trans_id -> Nullable, three_dsserver_trans_id -> Nullable, acs_signed_content -> Nullable, + #[max_length = 64] + profile_id -> Varchar, + #[max_length = 255] + payment_id -> Nullable, + #[max_length = 128] + merchant_connector_id -> Varchar, } } diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index a7c54ef8f3..ffb7c0c6d9 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -591,6 +591,10 @@ impl From for StripeErrorCode { object: "dispute".to_owned(), id: dispute_id, }, + errors::ApiErrorResponse::AuthenticationNotFound { id } => Self::ResourceMissing { + object: "authentication".to_owned(), + id, + }, errors::ApiErrorResponse::BusinessProfileNotFound { id } => Self::ResourceMissing { object: "business_profile".to_owned(), id, diff --git a/crates/router/src/core/authentication.rs b/crates/router/src/core/authentication.rs index 09d8ccc883..fe793d2e39 100644 --- a/crates/router/src/core/authentication.rs +++ b/crates/router/src/core/authentication.rs @@ -155,6 +155,17 @@ pub async fn perform_post_authentication( Ok(()) } +fn get_payment_id_from_pre_authentication_flow_input( + pre_authentication_flow_input: &types::PreAuthenthenticationFlowInput<'_, F>, +) -> Option { + match pre_authentication_flow_input { + types::PreAuthenthenticationFlowInput::PaymentAuthNFlow { payment_data, .. } => { + Some(payment_data.payment_intent.payment_id.clone()) + } + _ => None, + } +} + pub async fn perform_pre_authentication( state: &AppState, authentication_connector_name: String, @@ -163,10 +174,17 @@ pub async fn perform_pre_authentication( three_ds_connector_account: payments_core::helpers::MerchantConnectorAccountType, payment_connector_account: payments_core::helpers::MerchantConnectorAccountType, ) -> CustomResult<(), ApiErrorResponse> { + let payment_id = get_payment_id_from_pre_authentication_flow_input(&authentication_flow_input); let authentication = utils::create_new_authentication( state, business_profile.merchant_id.clone(), authentication_connector_name.clone(), + business_profile.profile_id.clone(), + payment_id, + three_ds_connector_account + .get_mca_id() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while finding mca_id from merchant_connector_account")?, ) .await?; match authentication_flow_input { diff --git a/crates/router/src/core/authentication/types.rs b/crates/router/src/core/authentication/types.rs index 6035d8fa37..f38ff1e9d8 100644 --- a/crates/router/src/core/authentication/types.rs +++ b/crates/router/src/core/authentication/types.rs @@ -74,3 +74,8 @@ pub struct AcquirerDetails { pub acquirer_bin: String, pub acquirer_merchant_id: String, } + +#[derive(Clone, Debug, Deserialize)] +pub struct ExternalThreeDSConnectorMetadata { + pub pull_mechanism_for_external_3ds_enabled: Option, +} diff --git a/crates/router/src/core/authentication/utils.rs b/crates/router/src/core/authentication/utils.rs index 8d6efc4d68..e9c0b3ffc6 100644 --- a/crates/router/src/core/authentication/utils.rs +++ b/crates/router/src/core/authentication/utils.rs @@ -144,6 +144,9 @@ pub async fn create_new_authentication( state: &AppState, merchant_id: String, authentication_connector: String, + profile_id: String, + payment_id: Option, + merchant_connector_id: String, ) -> RouterResult { let authentication_id = common_utils::generate_id_with_default_len(consts::AUTHENTICATION_ID_PREFIX); @@ -176,6 +179,9 @@ pub async fn create_new_authentication( acs_trans_id: None, three_dsserver_trans_id: None, acs_signed_content: None, + profile_id, + payment_id, + merchant_connector_id, }; state .store diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index dc3e19cb72..9e147053fe 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -170,6 +170,8 @@ pub enum ApiErrorResponse { ResourceIdNotFound, #[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "Mandate does not exist in our records")] MandateNotFound, + #[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "Authentication does not exist in our records")] + AuthenticationNotFound { id: String }, #[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "Failed to update mandate")] MandateUpdateFailed, #[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "API Key does not exist in our records")] diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index 7c06fc92c9..57f383d651 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -229,6 +229,9 @@ impl ErrorSwitch for ApiErrorRespon Self::DisputeNotFound { .. } => { AER::NotFound(ApiError::new("HE", 2, "Dispute does not exist in our records", None)) }, + Self::AuthenticationNotFound { .. } => { + AER::NotFound(ApiError::new("HE", 2, "Authentication does not exist in our records", None)) + }, Self::BusinessProfileNotFound { id } => { AER::NotFound(ApiError::new("HE", 2, format!("Business profile with the given id {id} does not exist"), None)) } diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index bb1efc9625..14ec842a90 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -50,7 +50,7 @@ use crate::{ api::{self, mandates::MandateResponseExt}, domain::{self, types as domain_types}, storage::{self, enums}, - transformers::{ForeignInto, ForeignTryFrom}, + transformers::{ForeignFrom, ForeignInto, ForeignTryFrom}, }, utils::{self as helper_utils, generate_id, OptionExt, ValueExt}, workflows::outgoing_webhook_retry, @@ -416,6 +416,162 @@ pub async fn get_or_update_dispute_object( } } +#[allow(clippy::too_many_arguments)] +pub async fn external_authentication_incoming_webhook_flow( + state: AppState, + req_state: ReqState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + source_verified: bool, + event_type: api_models::webhooks::IncomingWebhookEvent, + request_details: &api::IncomingWebhookRequestDetails<'_>, + connector: &(dyn api::Connector + Sync), + object_ref_id: api::ObjectReferenceId, + business_profile: diesel_models::business_profile::BusinessProfile, + merchant_connector_account: domain::MerchantConnectorAccount, +) -> CustomResult { + if source_verified { + let authentication_details = connector + .get_external_authentication_details(request_details) + .switch()?; + let trans_status = authentication_details.trans_status; + let authentication_update = storage::AuthenticationUpdate::PostAuthenticationUpdate { + authentication_status: common_enums::AuthenticationStatus::foreign_from( + trans_status.clone(), + ), + trans_status, + authentication_value: authentication_details.authentication_value, + eci: authentication_details.eci, + }; + let authentication = + if let webhooks::ObjectReferenceId::ExternalAuthenticationID(authentication_id_type) = + object_ref_id + { + match authentication_id_type { + webhooks::AuthenticationIdType::AuthenticationId(authentication_id) => state + .store + .find_authentication_by_merchant_id_authentication_id( + merchant_account.merchant_id.clone(), + authentication_id.clone(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::AuthenticationNotFound { + id: authentication_id, + }) + .attach_printable("Error while fetching authentication record"), + webhooks::AuthenticationIdType::ConnectorAuthenticationId( + connector_authentication_id, + ) => state + .store + .find_authentication_by_merchant_id_connector_authentication_id( + merchant_account.merchant_id.clone(), + connector_authentication_id.clone(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::AuthenticationNotFound { + id: connector_authentication_id, + }) + .attach_printable("Error while fetching authentication record"), + } + } else { + Err(errors::ApiErrorResponse::WebhookProcessingFailure).attach_printable( + "received a non-external-authentication id for retrieving authentication", + ) + }?; + let updated_authentication = state + .store + .update_authentication_by_merchant_id_authentication_id( + authentication, + authentication_update, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while updating authentication")?; + // Check if it's a payment authentication flow, payment_id would be there only for payment authentication flows + if let Some(payment_id) = updated_authentication.payment_id { + let is_pull_mechanism_enabled = helper_utils::check_if_pull_mechanism_for_external_3ds_enabled_from_connector_metadata(merchant_connector_account.metadata.map(|metadata| metadata.expose())); + // Merchant doesn't have pull mechanism enabled, so we have to authorize whenever we receive a ARes webhook + if !is_pull_mechanism_enabled + && event_type == webhooks::IncomingWebhookEvent::ExternalAuthenticationARes + { + let payment_confirm_req = api::PaymentsRequest { + payment_id: Some(api_models::payments::PaymentIdType::PaymentIntentId( + payment_id, + )), + merchant_id: Some(merchant_account.merchant_id.clone()), + ..Default::default() + }; + let payments_response = Box::pin(payments::payments_core::< + api::Authorize, + api::PaymentsResponse, + _, + _, + _, + Ctx, + >( + state.clone(), + req_state, + merchant_account.clone(), + key_store.clone(), + payments::PaymentConfirm, + payment_confirm_req, + services::api::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + HeaderPayload::with_source(enums::PaymentSource::ExternalAuthenticator), + )) + .await?; + match payments_response { + services::ApplicationResponse::JsonWithHeaders((payments_response, _)) => { + let payment_id = payments_response + .payment_id + .clone() + .get_required_value("payment_id") + .change_context(errors::ApiErrorResponse::WebhookProcessingFailure) + .attach_printable("payment id not received from payments core")?; + let status = payments_response.status; + let event_type: Option = + payments_response.status.foreign_into(); + // If event is NOT an UnsupportedEvent, trigger Outgoing Webhook + if let Some(outgoing_event_type) = event_type { + let primary_object_created_at = payments_response.created; + create_event_and_trigger_outgoing_webhook( + state, + merchant_account, + business_profile, + &key_store, + outgoing_event_type, + enums::EventClass::Payments, + payment_id.clone(), + enums::EventObjectType::PaymentDetails, + api::OutgoingWebhookContent::PaymentDetails(payments_response), + primary_object_created_at, + ) + .await?; + }; + let response = WebhookResponseTracker::Payment { payment_id, status }; + Ok(response) + } + _ => Err(errors::ApiErrorResponse::WebhookProcessingFailure).attach_printable( + "Did not get payment id as object reference id in webhook payments flow", + )?, + } + } else { + Ok(WebhookResponseTracker::NoEffect) + } + } else { + Ok(WebhookResponseTracker::NoEffect) + } + } else { + logger::error!( + "Webhook source verification failed for external authentication webhook flow" + ); + Err(report!( + errors::ApiErrorResponse::WebhookAuthenticationFailed + )) + } +} + pub async fn mandates_incoming_webhook_flow( state: AppState, merchant_account: domain::MerchantAccount, @@ -1365,7 +1521,7 @@ pub async fn webhooks_core { + Box::pin(external_authentication_incoming_webhook_flow::( + state.clone(), + req_state, + merchant_account, + key_store, + source_verified, + event_type, + &request_details, + connector, + object_ref_id, + business_profile, + merchant_connector_account, + )) + .await + .attach_printable("Incoming webhook flow for external authentication failed")? + } + _ => Err(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unsupported Flow Type received in incoming webhooks")?, } @@ -1704,6 +1874,39 @@ pub async fn get_payment_id( .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) } +fn get_connector_by_connector_name( + state: &AppState, + connector_name: &str, + merchant_connector_id: Option, +) -> CustomResult<(&'static (dyn api::Connector + Sync), String), errors::ApiErrorResponse> { + let authentication_connector = + api_models::enums::convert_authentication_connector(connector_name); + let (connector, connector_name) = if authentication_connector.is_some() { + let authentication_connector_data = + api::AuthenticationConnectorData::get_connector_by_name(connector_name)?; + ( + authentication_connector_data.connector, + authentication_connector_data.connector_name.to_string(), + ) + } else { + let connector_data = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + connector_name, + api::GetToken::Connector, + merchant_connector_id, + ) + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "invalid connector name received".to_string(), + }) + .attach_printable("Failed construction of ConnectorData")?; + ( + connector_data.connector, + connector_data.connector_name.to_string(), + ) + }; + Ok((*connector, connector_name)) +} + /// This function fetches the merchant connector account ( if the url used is /{merchant_connector_id}) /// if merchant connector id is not passed in the request, then this will return None for mca async fn fetch_optional_mca_and_connector( @@ -1712,7 +1915,11 @@ async fn fetch_optional_mca_and_connector( connector_name_or_mca_id: &str, key_store: &domain::MerchantKeyStore, ) -> CustomResult< - (Option, api::ConnectorData), + ( + Option, + &'static (dyn api::Connector + Sync), + String, + ), errors::ApiErrorResponse, > { let db = &state.store; @@ -1730,33 +1937,18 @@ async fn fetch_optional_mca_and_connector( .attach_printable( "error while fetching merchant_connector_account from connector_id", )?; - - let connector = api::ConnectorData::get_connector_by_name( - &state.conf.connectors, + let (connector, connector_name) = get_connector_by_connector_name( + state, &mca.connector_name, - api::GetToken::Connector, Some(mca.merchant_connector_id.clone()), - ) - .change_context(errors::ApiErrorResponse::InvalidRequestData { - message: "invalid connector name received".to_string(), - }) - .attach_printable("Failed construction of ConnectorData")?; + )?; - Ok((Some(mca), connector)) + Ok((Some(mca), connector, connector_name)) } else { // Merchant connector account is already being queried, it is safe to set connector id as None - let connector = api::ConnectorData::get_connector_by_name( - &state.conf.connectors, - connector_name_or_mca_id, - api::GetToken::Connector, - None, - ) - .change_context(errors::ApiErrorResponse::InvalidRequestData { - message: "invalid connector name received".to_string(), - }) - .attach_printable("Failed construction of ConnectorData")?; - - Ok((None, connector)) + let (connector, connector_name) = + get_connector_by_connector_name(state, connector_name_or_mca_id, None)?; + Ok((None, connector, connector_name)) } } diff --git a/crates/router/src/db/authentication.rs b/crates/router/src/db/authentication.rs index a2c7f05005..0f4aef679c 100644 --- a/crates/router/src/db/authentication.rs +++ b/crates/router/src/db/authentication.rs @@ -22,6 +22,12 @@ pub trait AuthenticationInterface { authentication_id: String, ) -> CustomResult; + async fn find_authentication_by_merchant_id_connector_authentication_id( + &self, + merchant_id: String, + connector_authentication_id: String, + ) -> CustomResult; + async fn update_authentication_by_merchant_id_authentication_id( &self, previous_state: storage::Authentication, @@ -59,6 +65,21 @@ impl AuthenticationInterface for Store { .map_err(|error| report!(errors::StorageError::from(error))) } + async fn find_authentication_by_merchant_id_connector_authentication_id( + &self, + merchant_id: String, + connector_authentication_id: String, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + storage::Authentication::find_authentication_by_merchant_id_connector_authentication_id( + &conn, + &merchant_id, + &connector_authentication_id, + ) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + } + #[instrument(skip_all)] async fn update_authentication_by_merchant_id_authentication_id( &self, @@ -124,6 +145,9 @@ impl AuthenticationInterface for MockDb { acs_trans_id: authentication.acs_trans_id, three_ds_server_trans_id: authentication.three_dsserver_trans_id, acs_signed_content: authentication.acs_signed_content, + profile_id: authentication.profile_id, + payment_id: authentication.payment_id, + merchant_connector_id: authentication.merchant_connector_id, }; authentications.push(authentication.clone()); Ok(authentication) @@ -145,6 +169,14 @@ impl AuthenticationInterface for MockDb { ).cloned() } + async fn find_authentication_by_merchant_id_connector_authentication_id( + &self, + _merchant_id: String, + _connector_authentication_id: String, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + async fn update_authentication_by_merchant_id_authentication_id( &self, previous_state: storage::Authentication, diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index c32bcf6f6a..a831f5cbd7 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -2645,6 +2645,19 @@ impl AuthenticationInterface for KafkaStore { .await } + async fn find_authentication_by_merchant_id_connector_authentication_id( + &self, + merchant_id: String, + connector_authentication_id: String, + ) -> CustomResult { + self.diesel_store + .find_authentication_by_merchant_id_connector_authentication_id( + merchant_id, + connector_authentication_id, + ) + .await + } + async fn update_authentication_by_merchant_id_authentication_id( &self, previous_state: storage::Authentication, diff --git a/crates/router/src/types/api/authentication.rs b/crates/router/src/types/api/authentication.rs index dbca3c1e68..dddb9c7987 100644 --- a/crates/router/src/types/api/authentication.rs +++ b/crates/router/src/types/api/authentication.rs @@ -47,6 +47,13 @@ pub enum MessageCategory { NonPayment, } +#[derive(Clone, serde::Deserialize, Debug, serde::Serialize, PartialEq, Eq)] +pub struct ExternalAuthenticationPayload { + pub trans_status: common_enums::TransactionStatus, + pub authentication_value: Option, + pub eci: Option, +} + pub trait ConnectorAuthentication: services::ConnectorIntegration< Authentication, diff --git a/crates/router/src/types/api/webhooks.rs b/crates/router/src/types/api/webhooks.rs index 52f5300d9b..dbc7ea2108 100644 --- a/crates/router/src/types/api/webhooks.rs +++ b/crates/router/src/types/api/webhooks.rs @@ -270,4 +270,14 @@ pub trait IncomingWebhook: ConnectorCommon + Sync { ) -> CustomResult { Err(errors::ConnectorError::NotImplemented("get_dispute_details method".to_string()).into()) } + + fn get_external_authentication_details( + &self, + _request: &IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented( + "get_external_authentication_details method".to_string(), + ) + .into()) + } } diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 0abe5ea936..85ef0cfe6b 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -38,6 +38,7 @@ pub use self::ext_traits::{OptionExt, ValidateCall}; use crate::{ consts, core::{ + authentication::types::ExternalThreeDSConnectorMetadata, errors::{self, CustomResult, RouterResult, StorageErrorExt}, utils, webhooks as webhooks_core, }, @@ -323,6 +324,98 @@ pub async fn find_payment_intent_from_mandate_id_type( .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) } +pub async fn find_mca_from_authentication_id_type( + db: &dyn StorageInterface, + authentication_id_type: webhooks::AuthenticationIdType, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, +) -> CustomResult { + let authentication = match authentication_id_type { + webhooks::AuthenticationIdType::AuthenticationId(authentication_id) => db + .find_authentication_by_merchant_id_authentication_id( + merchant_account.merchant_id.clone(), + authentication_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError)?, + webhooks::AuthenticationIdType::ConnectorAuthenticationId(connector_authentication_id) => { + db.find_authentication_by_merchant_id_connector_authentication_id( + merchant_account.merchant_id.clone(), + connector_authentication_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError)? + } + }; + db.find_by_merchant_connector_account_merchant_id_merchant_connector_id( + &merchant_account.merchant_id, + &authentication.merchant_connector_id, + key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: authentication.merchant_connector_id.to_string(), + }) +} + +pub async fn get_mca_from_payment_intent( + db: &dyn StorageInterface, + merchant_account: &domain::MerchantAccount, + payment_intent: PaymentIntent, + key_store: &domain::MerchantKeyStore, + connector_name: &str, +) -> CustomResult { + let payment_attempt = db + .find_payment_attempt_by_attempt_id_merchant_id( + &payment_intent.active_attempt.get_id(), + &merchant_account.merchant_id, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + match payment_attempt.merchant_connector_id { + Some(merchant_connector_id) => db + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + &merchant_account.merchant_id, + &merchant_connector_id, + key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_connector_id, + }), + None => { + let profile_id = match payment_intent.profile_id { + Some(profile_id) => profile_id, + None => utils::get_profile_id_from_business_details( + payment_intent.business_country, + payment_intent.business_label.as_ref(), + merchant_account, + payment_intent.profile_id.as_ref(), + db, + false, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent")?, + }; + + db.find_merchant_connector_account_by_profile_id_connector_name( + &profile_id, + connector_name, + key_store, + ) + .await + .to_not_found_response( + errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: format!("profile_id {profile_id} and connector_name {connector_name}"), + }, + ) + } + } +} + pub async fn get_mca_from_object_reference_id( db: &dyn StorageInterface, object_reference_id: webhooks::ObjectReferenceId, @@ -330,8 +423,6 @@ pub async fn get_mca_from_object_reference_id( connector_name: &str, key_store: &domain::MerchantKeyStore, ) -> CustomResult { - let merchant_id = merchant_account.merchant_id.clone(); - match merchant_account.default_profile.as_ref() { Some(profile_id) => db .find_merchant_connector_account_by_profile_id_connector_name( @@ -343,78 +434,55 @@ pub async fn get_mca_from_object_reference_id( .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { id: format!("profile_id {profile_id} and connector_name {connector_name}"), }), - _ => { - let payment_intent = match object_reference_id { - webhooks::ObjectReferenceId::PaymentId(payment_id_type) => { + _ => match object_reference_id { + webhooks::ObjectReferenceId::PaymentId(payment_id_type) => { + get_mca_from_payment_intent( + db, + merchant_account, find_payment_intent_from_payment_id_type(db, payment_id_type, merchant_account) - .await? - } - webhooks::ObjectReferenceId::RefundId(refund_id_type) => { + .await?, + key_store, + connector_name, + ) + .await + } + webhooks::ObjectReferenceId::RefundId(refund_id_type) => { + get_mca_from_payment_intent( + db, + merchant_account, find_payment_intent_from_refund_id_type( db, refund_id_type, merchant_account, connector_name, ) - .await? - } - webhooks::ObjectReferenceId::MandateId(mandate_id_type) => { - find_payment_intent_from_mandate_id_type(db, mandate_id_type, merchant_account) - .await? - } - }; - - let payment_attempt = db - .find_payment_attempt_by_attempt_id_merchant_id( - &payment_intent.active_attempt.get_id(), - &merchant_id, - merchant_account.storage_scheme, + .await?, + key_store, + connector_name, ) .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - - match payment_attempt.merchant_connector_id { - Some(merchant_connector_id) => db - .find_by_merchant_connector_account_merchant_id_merchant_connector_id( - &merchant_id, - &merchant_connector_id, - key_store, - ) - .await - .to_not_found_response( - errors::ApiErrorResponse::MerchantConnectorAccountNotFound { - id: merchant_connector_id, - }, - ), - None => { - let profile_id = utils::get_profile_id_from_business_details( - payment_intent.business_country, - payment_intent.business_label.as_ref(), - merchant_account, - payment_intent.profile_id.as_ref(), - db, - false, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("profile_id is not set in payment_intent")?; - - db.find_merchant_connector_account_by_profile_id_connector_name( - &profile_id, - connector_name, - key_store, - ) - .await - .to_not_found_response( - errors::ApiErrorResponse::MerchantConnectorAccountNotFound { - id: format!( - "profile_id {profile_id} and connector_name {connector_name}" - ), - }, - ) - } } - } + webhooks::ObjectReferenceId::MandateId(mandate_id_type) => { + get_mca_from_payment_intent( + db, + merchant_account, + find_payment_intent_from_mandate_id_type(db, mandate_id_type, merchant_account) + .await?, + key_store, + connector_name, + ) + .await + } + webhooks::ObjectReferenceId::ExternalAuthenticationID(authentication_id_type) => { + find_mca_from_authentication_id_type( + db, + authentication_id_type, + merchant_account, + key_store, + ) + .await + } + }, } } @@ -737,6 +805,18 @@ pub fn add_apple_pay_payment_status_metrics( } } +pub fn check_if_pull_mechanism_for_external_3ds_enabled_from_connector_metadata( + metadata: Option, +) -> bool { + let external_three_ds_connector_metadata: Option = metadata + .parse_value("ExternalThreeDSConnectorMetadata") + .map_err(|err| logger::warn!(parsing_error=?err,"Error while parsing ExternalThreeDSConnectorMetadata")) + .ok(); + external_three_ds_connector_metadata + .and_then(|metadata| metadata.pull_mechanism_for_external_3ds_enabled) + .unwrap_or(true) +} + #[allow(clippy::too_many_arguments)] pub async fn trigger_payments_webhook( merchant_account: domain::MerchantAccount, diff --git a/migrations/2024-04-03-090257_add_profile_id_in_authentication_table/down.sql b/migrations/2024-04-03-090257_add_profile_id_in_authentication_table/down.sql new file mode 100644 index 0000000000..c4318fdafd --- /dev/null +++ b/migrations/2024-04-03-090257_add_profile_id_in_authentication_table/down.sql @@ -0,0 +1 @@ +ALTER TABLE authentication DROP COLUMN IF EXISTS profile_id; \ No newline at end of file diff --git a/migrations/2024-04-03-090257_add_profile_id_in_authentication_table/up.sql b/migrations/2024-04-03-090257_add_profile_id_in_authentication_table/up.sql new file mode 100644 index 0000000000..e843f4777a --- /dev/null +++ b/migrations/2024-04-03-090257_add_profile_id_in_authentication_table/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE authentication ADD COLUMN profile_id VARCHAR(64) NOT NULL; diff --git a/migrations/2024-04-04-080427_add_payment_id_in_authentication_table/down.sql b/migrations/2024-04-04-080427_add_payment_id_in_authentication_table/down.sql new file mode 100644 index 0000000000..3282167351 --- /dev/null +++ b/migrations/2024-04-04-080427_add_payment_id_in_authentication_table/down.sql @@ -0,0 +1 @@ +ALTER TABLE authentication DROP COLUMN IF EXISTS payment_id; \ No newline at end of file diff --git a/migrations/2024-04-04-080427_add_payment_id_in_authentication_table/up.sql b/migrations/2024-04-04-080427_add_payment_id_in_authentication_table/up.sql new file mode 100644 index 0000000000..4b7291a41e --- /dev/null +++ b/migrations/2024-04-04-080427_add_payment_id_in_authentication_table/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE authentication ADD COLUMN payment_id VARCHAR(255); \ No newline at end of file diff --git a/migrations/2024-04-15-101918_add_merchant_connector_id_in_authentication_table/down.sql b/migrations/2024-04-15-101918_add_merchant_connector_id_in_authentication_table/down.sql new file mode 100644 index 0000000000..bbc6169b49 --- /dev/null +++ b/migrations/2024-04-15-101918_add_merchant_connector_id_in_authentication_table/down.sql @@ -0,0 +1 @@ +ALTER TABLE authentication DROP COLUMN IF EXISTS merchant_connector_id; \ No newline at end of file diff --git a/migrations/2024-04-15-101918_add_merchant_connector_id_in_authentication_table/up.sql b/migrations/2024-04-15-101918_add_merchant_connector_id_in_authentication_table/up.sql new file mode 100644 index 0000000000..ae083060bf --- /dev/null +++ b/migrations/2024-04-15-101918_add_merchant_connector_id_in_authentication_table/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE authentication ADD COLUMN merchant_connector_id VARCHAR(128) NOT NULL;