feat(router): add external authentication webhooks flow (#4339)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Sai Harsha Vardhan
2024-04-16 15:54:46 +05:30
committed by GitHub
parent d4dbaadb06
commit 00cd96d097
22 changed files with 512 additions and 95 deletions

View File

@ -38,6 +38,7 @@ pub enum IncomingWebhookEvent {
MandateActive, MandateActive,
MandateRevoked, MandateRevoked,
EndpointVerification, EndpointVerification,
ExternalAuthenticationARes,
} }
pub enum WebhookFlow { pub enum WebhookFlow {
@ -48,6 +49,7 @@ pub enum WebhookFlow {
ReturnResponse, ReturnResponse,
BankTransfer, BankTransfer,
Mandate, Mandate,
ExternalAuthentication,
} }
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
@ -116,6 +118,7 @@ impl From<IncomingWebhookEvent> for WebhookFlow {
IncomingWebhookEvent::EndpointVerification => Self::ReturnResponse, IncomingWebhookEvent::EndpointVerification => Self::ReturnResponse,
IncomingWebhookEvent::SourceChargeable IncomingWebhookEvent::SourceChargeable
| IncomingWebhookEvent::SourceTransactionCreated => Self::BankTransfer, | IncomingWebhookEvent::SourceTransactionCreated => Self::BankTransfer,
IncomingWebhookEvent::ExternalAuthenticationARes => Self::ExternalAuthentication,
} }
} }
} }
@ -134,11 +137,18 @@ pub enum MandateIdType {
ConnectorMandateId(String), ConnectorMandateId(String),
} }
#[derive(Clone)]
pub enum AuthenticationIdType {
AuthenticationId(String),
ConnectorAuthenticationId(String),
}
#[derive(Clone)] #[derive(Clone)]
pub enum ObjectReferenceId { pub enum ObjectReferenceId {
PaymentId(payments::PaymentIdType), PaymentId(payments::PaymentIdType),
RefundId(RefundIdType), RefundId(RefundIdType),
MandateId(MandateIdType), MandateId(MandateIdType),
ExternalAuthenticationID(AuthenticationIdType),
} }
pub struct IncomingWebhookDetails { pub struct IncomingWebhookDetails {

View File

@ -40,6 +40,9 @@ pub struct Authentication {
pub acs_trans_id: Option<String>, pub acs_trans_id: Option<String>,
pub three_ds_server_trans_id: Option<String>, pub three_ds_server_trans_id: Option<String>,
pub acs_signed_content: Option<String>, pub acs_signed_content: Option<String>,
pub profile_id: String,
pub payment_id: Option<String>,
pub merchant_connector_id: String,
} }
impl Authentication { impl Authentication {
@ -82,6 +85,9 @@ pub struct AuthenticationNew {
pub acs_trans_id: Option<String>, pub acs_trans_id: Option<String>,
pub three_dsserver_trans_id: Option<String>, pub three_dsserver_trans_id: Option<String>,
pub acs_signed_content: Option<String>, pub acs_signed_content: Option<String>,
pub profile_id: String,
pub payment_id: Option<String>,
pub merchant_connector_id: String,
} }
#[derive(Debug)] #[derive(Debug)]

View File

@ -69,4 +69,18 @@ impl Authentication {
) )
.await .await
} }
pub async fn find_authentication_by_merchant_id_connector_authentication_id(
conn: &PgPooledConn,
merchant_id: &str,
connector_authentication_id: &str,
) -> StorageResult<Self> {
generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
conn,
dsl::merchant_id
.eq(merchant_id.to_owned())
.and(dsl::connector_authentication_id.eq(connector_authentication_id.to_owned())),
)
.await
}
} }

View File

@ -111,6 +111,12 @@ diesel::table! {
acs_trans_id -> Nullable<Varchar>, acs_trans_id -> Nullable<Varchar>,
three_dsserver_trans_id -> Nullable<Varchar>, three_dsserver_trans_id -> Nullable<Varchar>,
acs_signed_content -> Nullable<Varchar>, acs_signed_content -> Nullable<Varchar>,
#[max_length = 64]
profile_id -> Varchar,
#[max_length = 255]
payment_id -> Nullable<Varchar>,
#[max_length = 128]
merchant_connector_id -> Varchar,
} }
} }

View File

@ -591,6 +591,10 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
object: "dispute".to_owned(), object: "dispute".to_owned(),
id: dispute_id, id: dispute_id,
}, },
errors::ApiErrorResponse::AuthenticationNotFound { id } => Self::ResourceMissing {
object: "authentication".to_owned(),
id,
},
errors::ApiErrorResponse::BusinessProfileNotFound { id } => Self::ResourceMissing { errors::ApiErrorResponse::BusinessProfileNotFound { id } => Self::ResourceMissing {
object: "business_profile".to_owned(), object: "business_profile".to_owned(),
id, id,

View File

@ -155,6 +155,17 @@ pub async fn perform_post_authentication<F: Clone + Send>(
Ok(()) Ok(())
} }
fn get_payment_id_from_pre_authentication_flow_input<F: Clone + Send>(
pre_authentication_flow_input: &types::PreAuthenthenticationFlowInput<'_, F>,
) -> Option<String> {
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<F: Clone + Send>( pub async fn perform_pre_authentication<F: Clone + Send>(
state: &AppState, state: &AppState,
authentication_connector_name: String, authentication_connector_name: String,
@ -163,10 +174,17 @@ pub async fn perform_pre_authentication<F: Clone + Send>(
three_ds_connector_account: payments_core::helpers::MerchantConnectorAccountType, three_ds_connector_account: payments_core::helpers::MerchantConnectorAccountType,
payment_connector_account: payments_core::helpers::MerchantConnectorAccountType, payment_connector_account: payments_core::helpers::MerchantConnectorAccountType,
) -> CustomResult<(), ApiErrorResponse> { ) -> CustomResult<(), ApiErrorResponse> {
let payment_id = get_payment_id_from_pre_authentication_flow_input(&authentication_flow_input);
let authentication = utils::create_new_authentication( let authentication = utils::create_new_authentication(
state, state,
business_profile.merchant_id.clone(), business_profile.merchant_id.clone(),
authentication_connector_name.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?; .await?;
match authentication_flow_input { match authentication_flow_input {

View File

@ -74,3 +74,8 @@ pub struct AcquirerDetails {
pub acquirer_bin: String, pub acquirer_bin: String,
pub acquirer_merchant_id: String, pub acquirer_merchant_id: String,
} }
#[derive(Clone, Debug, Deserialize)]
pub struct ExternalThreeDSConnectorMetadata {
pub pull_mechanism_for_external_3ds_enabled: Option<bool>,
}

View File

@ -144,6 +144,9 @@ pub async fn create_new_authentication(
state: &AppState, state: &AppState,
merchant_id: String, merchant_id: String,
authentication_connector: String, authentication_connector: String,
profile_id: String,
payment_id: Option<String>,
merchant_connector_id: String,
) -> RouterResult<storage::Authentication> { ) -> RouterResult<storage::Authentication> {
let authentication_id = let authentication_id =
common_utils::generate_id_with_default_len(consts::AUTHENTICATION_ID_PREFIX); 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, acs_trans_id: None,
three_dsserver_trans_id: None, three_dsserver_trans_id: None,
acs_signed_content: None, acs_signed_content: None,
profile_id,
payment_id,
merchant_connector_id,
}; };
state state
.store .store

View File

@ -170,6 +170,8 @@ pub enum ApiErrorResponse {
ResourceIdNotFound, ResourceIdNotFound,
#[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "Mandate does not exist in our records")] #[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "Mandate does not exist in our records")]
MandateNotFound, 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")] #[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "Failed to update mandate")]
MandateUpdateFailed, MandateUpdateFailed,
#[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "API Key does not exist in our records")] #[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "API Key does not exist in our records")]

View File

@ -229,6 +229,9 @@ impl ErrorSwitch<api_models::errors::types::ApiErrorResponse> for ApiErrorRespon
Self::DisputeNotFound { .. } => { Self::DisputeNotFound { .. } => {
AER::NotFound(ApiError::new("HE", 2, "Dispute does not exist in our records", None)) 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 } => { Self::BusinessProfileNotFound { id } => {
AER::NotFound(ApiError::new("HE", 2, format!("Business profile with the given id {id} does not exist"), None)) AER::NotFound(ApiError::new("HE", 2, format!("Business profile with the given id {id} does not exist"), None))
} }

View File

@ -50,7 +50,7 @@ use crate::{
api::{self, mandates::MandateResponseExt}, api::{self, mandates::MandateResponseExt},
domain::{self, types as domain_types}, domain::{self, types as domain_types},
storage::{self, enums}, storage::{self, enums},
transformers::{ForeignInto, ForeignTryFrom}, transformers::{ForeignFrom, ForeignInto, ForeignTryFrom},
}, },
utils::{self as helper_utils, generate_id, OptionExt, ValueExt}, utils::{self as helper_utils, generate_id, OptionExt, ValueExt},
workflows::outgoing_webhook_retry, 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<Ctx: PaymentMethodRetrieve>(
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<WebhookResponseTracker, errors::ApiErrorResponse> {
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<enums::EventType> =
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( pub async fn mandates_incoming_webhook_flow(
state: AppState, state: AppState,
merchant_account: domain::MerchantAccount, merchant_account: domain::MerchantAccount,
@ -1365,7 +1521,7 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType, Ctx: PaymentMethodRetr
// Fetch the merchant connector account to get the webhooks source secret // Fetch the merchant connector account to get the webhooks source secret
// `webhooks source secret` is a secret shared between the merchant and connector // `webhooks source secret` is a secret shared between the merchant and connector
// This is used for source verification and webhooks integrity // This is used for source verification and webhooks integrity
let (merchant_connector_account, connector) = fetch_optional_mca_and_connector( let (merchant_connector_account, connector, connector_name) = fetch_optional_mca_and_connector(
&state, &state,
&merchant_account, &merchant_account,
connector_name_or_mca_id, connector_name_or_mca_id,
@ -1373,10 +1529,6 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType, Ctx: PaymentMethodRetr
) )
.await?; .await?;
let connector_name = connector.connector_name.to_string();
let connector = connector.connector;
let decoded_body = connector let decoded_body = connector
.decode_webhook_body( .decode_webhook_body(
&*state.clone().store, &*state.clone().store,
@ -1548,7 +1700,7 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType, Ctx: PaymentMethodRetr
.attach_printable("Could not find resource object in incoming webhook body")?; .attach_printable("Could not find resource object in incoming webhook body")?;
let webhook_details = api::IncomingWebhookDetails { let webhook_details = api::IncomingWebhookDetails {
object_reference_id: object_ref_id, object_reference_id: object_ref_id.clone(),
resource_object: serde_json::to_vec(&event_object) resource_object: serde_json::to_vec(&event_object)
.change_context(errors::ParsingError::EncodeError("byte-vec")) .change_context(errors::ParsingError::EncodeError("byte-vec"))
.attach_printable("Unable to convert webhook payload to a value") .attach_printable("Unable to convert webhook payload to a value")
@ -1606,7 +1758,7 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType, Ctx: PaymentMethodRetr
key_store, key_store,
webhook_details, webhook_details,
source_verified, source_verified,
*connector, connector,
&request_details, &request_details,
event_type, event_type,
)) ))
@ -1639,6 +1791,24 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType, Ctx: PaymentMethodRetr
.await .await
.attach_printable("Incoming webhook flow for mandates failed")?, .attach_printable("Incoming webhook flow for mandates failed")?,
api::WebhookFlow::ExternalAuthentication => {
Box::pin(external_authentication_incoming_webhook_flow::<Ctx>(
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) _ => Err(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unsupported Flow Type received in incoming webhooks")?, .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) .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)
} }
fn get_connector_by_connector_name(
state: &AppState,
connector_name: &str,
merchant_connector_id: Option<String>,
) -> 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}) /// 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 /// if merchant connector id is not passed in the request, then this will return None for mca
async fn fetch_optional_mca_and_connector( async fn fetch_optional_mca_and_connector(
@ -1712,7 +1915,11 @@ async fn fetch_optional_mca_and_connector(
connector_name_or_mca_id: &str, connector_name_or_mca_id: &str,
key_store: &domain::MerchantKeyStore, key_store: &domain::MerchantKeyStore,
) -> CustomResult< ) -> CustomResult<
(Option<domain::MerchantConnectorAccount>, api::ConnectorData), (
Option<domain::MerchantConnectorAccount>,
&'static (dyn api::Connector + Sync),
String,
),
errors::ApiErrorResponse, errors::ApiErrorResponse,
> { > {
let db = &state.store; let db = &state.store;
@ -1730,33 +1937,18 @@ async fn fetch_optional_mca_and_connector(
.attach_printable( .attach_printable(
"error while fetching merchant_connector_account from connector_id", "error while fetching merchant_connector_account from connector_id",
)?; )?;
let (connector, connector_name) = get_connector_by_connector_name(
let connector = api::ConnectorData::get_connector_by_name( state,
&state.conf.connectors,
&mca.connector_name, &mca.connector_name,
api::GetToken::Connector,
Some(mca.merchant_connector_id.clone()), 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 { } else {
// Merchant connector account is already being queried, it is safe to set connector id as None // Merchant connector account is already being queried, it is safe to set connector id as None
let connector = api::ConnectorData::get_connector_by_name( let (connector, connector_name) =
&state.conf.connectors, get_connector_by_connector_name(state, connector_name_or_mca_id, None)?;
connector_name_or_mca_id, Ok((None, connector, connector_name))
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))
} }
} }

View File

@ -22,6 +22,12 @@ pub trait AuthenticationInterface {
authentication_id: String, authentication_id: String,
) -> CustomResult<storage::Authentication, errors::StorageError>; ) -> CustomResult<storage::Authentication, errors::StorageError>;
async fn find_authentication_by_merchant_id_connector_authentication_id(
&self,
merchant_id: String,
connector_authentication_id: String,
) -> CustomResult<storage::Authentication, errors::StorageError>;
async fn update_authentication_by_merchant_id_authentication_id( async fn update_authentication_by_merchant_id_authentication_id(
&self, &self,
previous_state: storage::Authentication, previous_state: storage::Authentication,
@ -59,6 +65,21 @@ impl AuthenticationInterface for Store {
.map_err(|error| report!(errors::StorageError::from(error))) .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<storage::Authentication, errors::StorageError> {
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)] #[instrument(skip_all)]
async fn update_authentication_by_merchant_id_authentication_id( async fn update_authentication_by_merchant_id_authentication_id(
&self, &self,
@ -124,6 +145,9 @@ impl AuthenticationInterface for MockDb {
acs_trans_id: authentication.acs_trans_id, acs_trans_id: authentication.acs_trans_id,
three_ds_server_trans_id: authentication.three_dsserver_trans_id, three_ds_server_trans_id: authentication.three_dsserver_trans_id,
acs_signed_content: authentication.acs_signed_content, 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()); authentications.push(authentication.clone());
Ok(authentication) Ok(authentication)
@ -145,6 +169,14 @@ impl AuthenticationInterface for MockDb {
).cloned() ).cloned()
} }
async fn find_authentication_by_merchant_id_connector_authentication_id(
&self,
_merchant_id: String,
_connector_authentication_id: String,
) -> CustomResult<storage::Authentication, errors::StorageError> {
Err(errors::StorageError::MockDbError)?
}
async fn update_authentication_by_merchant_id_authentication_id( async fn update_authentication_by_merchant_id_authentication_id(
&self, &self,
previous_state: storage::Authentication, previous_state: storage::Authentication,

View File

@ -2645,6 +2645,19 @@ impl AuthenticationInterface for KafkaStore {
.await .await
} }
async fn find_authentication_by_merchant_id_connector_authentication_id(
&self,
merchant_id: String,
connector_authentication_id: String,
) -> CustomResult<storage::Authentication, errors::StorageError> {
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( async fn update_authentication_by_merchant_id_authentication_id(
&self, &self,
previous_state: storage::Authentication, previous_state: storage::Authentication,

View File

@ -47,6 +47,13 @@ pub enum MessageCategory {
NonPayment, NonPayment,
} }
#[derive(Clone, serde::Deserialize, Debug, serde::Serialize, PartialEq, Eq)]
pub struct ExternalAuthenticationPayload {
pub trans_status: common_enums::TransactionStatus,
pub authentication_value: Option<String>,
pub eci: Option<String>,
}
pub trait ConnectorAuthentication: pub trait ConnectorAuthentication:
services::ConnectorIntegration< services::ConnectorIntegration<
Authentication, Authentication,

View File

@ -270,4 +270,14 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
) -> CustomResult<super::disputes::DisputePayload, errors::ConnectorError> { ) -> CustomResult<super::disputes::DisputePayload, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_dispute_details method".to_string()).into()) Err(errors::ConnectorError::NotImplemented("get_dispute_details method".to_string()).into())
} }
fn get_external_authentication_details(
&self,
_request: &IncomingWebhookRequestDetails<'_>,
) -> CustomResult<super::ExternalAuthenticationPayload, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented(
"get_external_authentication_details method".to_string(),
)
.into())
}
} }

View File

@ -38,6 +38,7 @@ pub use self::ext_traits::{OptionExt, ValidateCall};
use crate::{ use crate::{
consts, consts,
core::{ core::{
authentication::types::ExternalThreeDSConnectorMetadata,
errors::{self, CustomResult, RouterResult, StorageErrorExt}, errors::{self, CustomResult, RouterResult, StorageErrorExt},
utils, webhooks as webhooks_core, 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) .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<domain::MerchantConnectorAccount, errors::ApiErrorResponse> {
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<domain::MerchantConnectorAccount, errors::ApiErrorResponse> {
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( pub async fn get_mca_from_object_reference_id(
db: &dyn StorageInterface, db: &dyn StorageInterface,
object_reference_id: webhooks::ObjectReferenceId, object_reference_id: webhooks::ObjectReferenceId,
@ -330,8 +423,6 @@ pub async fn get_mca_from_object_reference_id(
connector_name: &str, connector_name: &str,
key_store: &domain::MerchantKeyStore, key_store: &domain::MerchantKeyStore,
) -> CustomResult<domain::MerchantConnectorAccount, errors::ApiErrorResponse> { ) -> CustomResult<domain::MerchantConnectorAccount, errors::ApiErrorResponse> {
let merchant_id = merchant_account.merchant_id.clone();
match merchant_account.default_profile.as_ref() { match merchant_account.default_profile.as_ref() {
Some(profile_id) => db Some(profile_id) => db
.find_merchant_connector_account_by_profile_id_connector_name( .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 { .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
id: format!("profile_id {profile_id} and connector_name {connector_name}"), id: format!("profile_id {profile_id} and connector_name {connector_name}"),
}), }),
_ => { _ => match object_reference_id {
let payment_intent = match object_reference_id { webhooks::ObjectReferenceId::PaymentId(payment_id_type) => {
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) find_payment_intent_from_payment_id_type(db, payment_id_type, merchant_account)
.await? .await?,
} key_store,
webhooks::ObjectReferenceId::RefundId(refund_id_type) => { connector_name,
)
.await
}
webhooks::ObjectReferenceId::RefundId(refund_id_type) => {
get_mca_from_payment_intent(
db,
merchant_account,
find_payment_intent_from_refund_id_type( find_payment_intent_from_refund_id_type(
db, db,
refund_id_type, refund_id_type,
merchant_account, merchant_account,
connector_name, connector_name,
) )
.await? .await?,
} key_store,
webhooks::ObjectReferenceId::MandateId(mandate_id_type) => { connector_name,
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 .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<Value>,
) -> bool {
let external_three_ds_connector_metadata: Option<ExternalThreeDSConnectorMetadata> = 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)] #[allow(clippy::too_many_arguments)]
pub async fn trigger_payments_webhook<F, Op>( pub async fn trigger_payments_webhook<F, Op>(
merchant_account: domain::MerchantAccount, merchant_account: domain::MerchantAccount,

View File

@ -0,0 +1 @@
ALTER TABLE authentication DROP COLUMN IF EXISTS profile_id;

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE authentication ADD COLUMN profile_id VARCHAR(64) NOT NULL;

View File

@ -0,0 +1 @@
ALTER TABLE authentication DROP COLUMN IF EXISTS payment_id;

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE authentication ADD COLUMN payment_id VARCHAR(255);

View File

@ -0,0 +1 @@
ALTER TABLE authentication DROP COLUMN IF EXISTS merchant_connector_id;

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE authentication ADD COLUMN merchant_connector_id VARCHAR(128) NOT NULL;