Skip to content

Commit

Permalink
feat(core): [Paypal] Add Preprocessing flow to CompleteAuthorize for …
Browse files Browse the repository at this point in the history
…Card 3DS Auth Verification (#2757)
  • Loading branch information
Sakilmostak authored Nov 28, 2023
1 parent e7ad3a4 commit 77fc92c
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 3 deletions.
156 changes: 156 additions & 0 deletions crates/router/src/connector/paypal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use crate::{
types::{
self,
api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource},
storage::enums as storage_enums,
transformers::ForeignFrom,
ConnectorAuthType, ErrorResponse, Response,
},
Expand Down Expand Up @@ -506,6 +507,161 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
}
}

impl api::PaymentsPreProcessing for Paypal {}

impl
ConnectorIntegration<
api::PreProcessing,
types::PaymentsPreProcessingData,
types::PaymentsResponseData,
> for Paypal
{
fn get_headers(
&self,
req: &types::PaymentsPreProcessingRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}

fn get_url(
&self,
req: &types::PaymentsPreProcessingRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
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<Option<services::Request>, 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<types::PaymentsPreProcessingRouterData, errors::ConnectorError> {
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<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}

impl
ConnectorIntegration<
CompleteAuthorize,
Expand Down
68 changes: 68 additions & 0 deletions crates/router/src/connector/paypal/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,74 @@ pub struct PaypalThreeDsResponse {
links: Vec<PaypalLinks>,
}

#[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<EnrollementStatus>,
pub authentication_status: Option<AuthenticationStatus>,
}

#[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,
Expand Down
2 changes: 2 additions & 0 deletions crates/router/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
16 changes: 15 additions & 1 deletion crates/router/src/core/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion crates/router/src/core/payments/flows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,6 @@ default_imp_for_pre_processing_steps!(
connector::Opayo,
connector::Opennode,
connector::Payeezy,
connector::Paypal,
connector::Payu,
connector::Powertranz,
connector::Prophetpay,
Expand Down
24 changes: 24 additions & 0 deletions crates/router/src/core/payments/flows/authorize_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,30 @@ impl TryFrom<types::PaymentsAuthorizeData> 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<types::CompleteAuthorizeData> for types::PaymentsPreProcessingData {
type Error = error_stack::Report<errors::ApiErrorResponse>;

fn try_from(data: types::CompleteAuthorizeData) -> Result<Self, Self::Error> {
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,
})
}
}
Loading

0 comments on commit 77fc92c

Please sign in to comment.