Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): [Paypal] Add Preprocessing flow to CompleteAuthorize for Card 3DS Auth Verification #2757

Merged
merged 8 commits into from
Nov 28, 2023
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 @@ -924,6 +924,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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check if this is required

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are being used for the propagation of error response when the respective field is none

#[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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check if this is required

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are being used for the propagation of error response when the respective field is none

#[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
Loading