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,
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<IncomingWebhookEvent> 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 {

View File

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

View File

@ -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<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>,
three_dsserver_trans_id -> 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(),
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,

View File

@ -155,6 +155,17 @@ pub async fn perform_post_authentication<F: Clone + Send>(
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>(
state: &AppState,
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,
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 {

View File

@ -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<bool>,
}

View File

@ -144,6 +144,9 @@ pub async fn create_new_authentication(
state: &AppState,
merchant_id: String,
authentication_connector: String,
profile_id: String,
payment_id: Option<String>,
merchant_connector_id: String,
) -> RouterResult<storage::Authentication> {
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

View File

@ -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")]

View File

@ -229,6 +229,9 @@ impl ErrorSwitch<api_models::errors::types::ApiErrorResponse> 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))
}

View File

@ -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<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(
state: AppState,
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
// `webhooks source secret` is a secret shared between the merchant and connector
// 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,
&merchant_account,
connector_name_or_mca_id,
@ -1373,10 +1529,6 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType, Ctx: PaymentMethodRetr
)
.await?;
let connector_name = connector.connector_name.to_string();
let connector = connector.connector;
let decoded_body = connector
.decode_webhook_body(
&*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")?;
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)
.change_context(errors::ParsingError::EncodeError("byte-vec"))
.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,
webhook_details,
source_verified,
*connector,
connector,
&request_details,
event_type,
))
@ -1639,6 +1791,24 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType, Ctx: PaymentMethodRetr
.await
.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)
.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<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})
/// 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<domain::MerchantConnectorAccount>, api::ConnectorData),
(
Option<domain::MerchantConnectorAccount>,
&'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))
}
}

View File

@ -22,6 +22,12 @@ pub trait AuthenticationInterface {
authentication_id: String,
) -> 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(
&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<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)]
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<storage::Authentication, errors::StorageError> {
Err(errors::StorageError::MockDbError)?
}
async fn update_authentication_by_merchant_id_authentication_id(
&self,
previous_state: storage::Authentication,

View File

@ -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<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(
&self,
previous_state: storage::Authentication,

View File

@ -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<String>,
pub eci: Option<String>,
}
pub trait ConnectorAuthentication:
services::ConnectorIntegration<
Authentication,

View File

@ -270,4 +270,14 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
) -> CustomResult<super::disputes::DisputePayload, errors::ConnectorError> {
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::{
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<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(
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<domain::MerchantConnectorAccount, errors::ApiErrorResponse> {
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<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)]
pub async fn trigger_payments_webhook<F, Op>(
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;