diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 4e50bc924b33..9ab19b295570 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -30,6 +30,7 @@ use crate::{ types::{ self, api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource}, + storage::enums as storage_enums, transformers::ForeignFrom, ConnectorAuthType, ErrorResponse, Response, }, @@ -506,6 +507,161 @@ impl ConnectorIntegration for Paypal +{ + fn get_headers( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_url( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let order_id = req + .request + .connector_transaction_id + .to_owned() + .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}v2/checkout/orders/{}?fields=payment_source", + self.base_url(connectors), + order_id, + )) + } + + fn build_request( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsPreProcessingType::get_headers( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsPreProcessingRouterData, + res: Response, + ) -> CustomResult { + let response: paypal::PaypalPreProcessingResponse = res + .response + .parse_struct("paypal PaypalPreProcessingResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + // permutation for status to continue payment + match ( + response + .payment_source + .card + .authentication_result + .three_d_secure + .enrollment_status + .as_ref(), + response + .payment_source + .card + .authentication_result + .three_d_secure + .authentication_status + .as_ref(), + response + .payment_source + .card + .authentication_result + .liability_shift + .clone(), + ) { + ( + Some(paypal::EnrollementStatus::Ready), + Some(paypal::AuthenticationStatus::Success), + paypal::LiabilityShift::Possible, + ) + | ( + Some(paypal::EnrollementStatus::Ready), + Some(paypal::AuthenticationStatus::Attempted), + paypal::LiabilityShift::Possible, + ) + | (Some(paypal::EnrollementStatus::NotReady), None, paypal::LiabilityShift::No) + | (Some(paypal::EnrollementStatus::Unavailable), None, paypal::LiabilityShift::No) + | (Some(paypal::EnrollementStatus::Bypassed), None, paypal::LiabilityShift::No) => { + Ok(types::PaymentsPreProcessingRouterData { + status: storage_enums::AttemptStatus::AuthenticationSuccessful, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + }), + ..data.clone() + }) + } + _ => Ok(types::PaymentsPreProcessingRouterData { + response: Err(ErrorResponse { + attempt_status: Some(enums::AttemptStatus::Failure), + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + connector_transaction_id: None, + reason: Some(format!("{} Connector Responsded with LiabilityShift: {:?}, EnrollmentStatus: {:?}, and AuthenticationStatus: {:?}", + consts::CANNOT_CONTINUE_AUTH, + response + .payment_source + .card + .authentication_result + .liability_shift, + response + .payment_source + .card + .authentication_result + .three_d_secure + .enrollment_status + .unwrap_or(paypal::EnrollementStatus::Null), + response + .payment_source + .card + .authentication_result + .three_d_secure + .authentication_status + .unwrap_or(paypal::AuthenticationStatus::Null), + )), + status_code: res.status_code, + }), + ..data.clone() + }), + } + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + impl ConnectorIntegration< CompleteAuthorize, diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index d023077ff008..b5e13f3b8f43 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -924,6 +924,74 @@ pub struct PaypalThreeDsResponse { links: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalPreProcessingResponse { + pub payment_source: CardParams, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CardParams { + pub card: AuthResult, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthResult { + pub authentication_result: PaypalThreeDsParams, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalThreeDsParams { + pub liability_shift: LiabilityShift, + pub three_d_secure: ThreeDsCheck, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ThreeDsCheck { + pub enrollment_status: Option, + pub authentication_status: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum LiabilityShift { + Possible, + No, + Unknown, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum EnrollementStatus { + Null, + #[serde(rename = "Y")] + Ready, + #[serde(rename = "N")] + NotReady, + #[serde(rename = "U")] + Unavailable, + #[serde(rename = "B")] + Bypassed, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AuthenticationStatus { + Null, + #[serde(rename = "Y")] + Success, + #[serde(rename = "N")] + Failed, + #[serde(rename = "R")] + Rejected, + #[serde(rename = "A")] + Attempted, + #[serde(rename = "U")] + Unable, + #[serde(rename = "C")] + ChallengeRequired, + #[serde(rename = "I")] + InfoOnly, + #[serde(rename = "D")] + Decoupled, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PaypalOrdersResponse { id: String, diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index c5490ee00e63..8937764409f8 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -28,6 +28,8 @@ pub(crate) const NO_ERROR_MESSAGE: &str = "No error message"; pub(crate) const NO_ERROR_CODE: &str = "No error code"; pub(crate) const UNSUPPORTED_ERROR_MESSAGE: &str = "Unsupported response type"; pub(crate) const CONNECTOR_UNAUTHORIZED_ERROR: &str = "Authentication Error from the connector"; +pub(crate) const CANNOT_CONTINUE_AUTH: &str = + "Cannot continue with Authorization due to failed Liability Shift."; // General purpose base64 engines pub(crate) const BASE64_ENGINE: base64::engine::GeneralPurpose = diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 1c40ef81f497..4fe7ea848008 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1418,7 +1418,21 @@ where (router_data, should_continue_payment) } } - _ => (router_data, should_continue_payment), + _ => { + // 3DS validation for paypal cards after verification (authorize call) + if connector.connector_name == router_types::Connector::Paypal + && payment_data.payment_attempt.payment_method + == Some(storage_enums::PaymentMethod::Card) + && matches!(format!("{operation:?}").as_str(), "CompleteAuthorize") + { + router_data = router_data.preprocessing_steps(state, connector).await?; + let is_error_in_response = router_data.response.is_err(); + // If is_error_in_response is true, should_continue_payment should be false, we should throw the error + (router_data, !is_error_in_response) + } else { + (router_data, should_continue_payment) + } + } }; Ok(router_data_and_should_continue_payment) diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 46eaca26f7cc..d983cd19bdb5 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -863,7 +863,6 @@ default_imp_for_pre_processing_steps!( connector::Opayo, connector::Opennode, connector::Payeezy, - connector::Paypal, connector::Payu, connector::Powertranz, connector::Prophetpay, diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 04bd7f0b4338..4ef23f481a2c 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -417,6 +417,30 @@ impl TryFrom for types::PaymentsPreProcessingData complete_authorize_url: data.complete_authorize_url, browser_info: data.browser_info, surcharge_details: data.surcharge_details, + connector_transaction_id: None, + }) + } +} + +impl TryFrom for types::PaymentsPreProcessingData { + type Error = error_stack::Report; + + fn try_from(data: types::CompleteAuthorizeData) -> Result { + Ok(Self { + payment_method_data: data.payment_method_data, + amount: Some(data.amount), + email: data.email, + currency: Some(data.currency), + payment_method_type: None, + setup_mandate_details: data.setup_mandate_details, + capture_method: data.capture_method, + order_details: None, + router_return_url: None, + webhook_url: None, + complete_authorize_url: None, + browser_info: data.browser_info, + surcharge_details: None, + connector_transaction_id: data.connector_transaction_id, }) } } diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs index 44d8728fd4d2..2d52a145feae 100644 --- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -6,7 +6,7 @@ use crate::{ errors::{self, ConnectorErrorExt, RouterResult}, payments::{self, access_token, helpers, transformers, PaymentData}, }, - routes::AppState, + routes::{metrics, AppState}, services, types::{self, api, domain}, utils::OptionExt, @@ -144,6 +144,76 @@ impl Feature Ok((request, true)) } + + async fn preprocessing_steps<'a>( + self, + state: &AppState, + connector: &api::ConnectorData, + ) -> RouterResult { + complete_authorize_preprocessing_steps(state, &self, true, connector).await + } +} + +pub async fn complete_authorize_preprocessing_steps( + state: &AppState, + router_data: &types::RouterData, + confirm: bool, + connector: &api::ConnectorData, +) -> RouterResult> { + if confirm { + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::PreProcessing, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + let preprocessing_request_data = + types::PaymentsPreProcessingData::try_from(router_data.request.to_owned())?; + + let preprocessing_response_data: Result = + Err(types::ErrorResponse::default()); + + let preprocessing_router_data = + payments::helpers::router_data_type_conversion::<_, api::PreProcessing, _, _, _, _>( + router_data.clone(), + preprocessing_request_data, + preprocessing_response_data, + ); + + let resp = services::execute_connector_processing_step( + state, + connector_integration, + &preprocessing_router_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await + .to_payment_failed_response()?; + + metrics::PREPROCESSING_STEPS_COUNT.add( + &metrics::CONTEXT, + 1, + &[ + metrics::request::add_attributes("connector", connector.connector_name.to_string()), + metrics::request::add_attributes( + "payment_method", + router_data.payment_method.to_string(), + ), + ], + ); + + let authorize_router_data = + payments::helpers::router_data_type_conversion::<_, F, _, _, _, _>( + resp.clone(), + router_data.request.to_owned(), + resp.response, + ); + + Ok(authorize_router_data) + } else { + Ok(router_data.clone()) + } } impl TryFrom for types::PaymentMethodTokenizationData { diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index f395c023128c..000bbb0fc00b 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1428,6 +1428,7 @@ impl TryFrom> for types::PaymentsPreProce complete_authorize_url, browser_info, surcharge_details: payment_data.surcharge_details, + connector_transaction_id: payment_data.payment_attempt.connector_transaction_id, }) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 8c9d030965c9..db126b81451d 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -442,6 +442,7 @@ pub struct PaymentsPreProcessingData { pub complete_authorize_url: Option, pub surcharge_details: Option, pub browser_info: Option, + pub connector_transaction_id: Option, } #[derive(Debug, Clone)]