From d95a64d6c9b870bdc38aa091cf9bf660b1ea404e Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Fri, 6 Oct 2023 16:54:20 +0530 Subject: [PATCH] feat(connector): [Paypal] Implement 3DS for Cards (#2443) --- crates/router/src/connector/paypal.rs | 53 +++-- .../src/connector/paypal/transformers.rs | 214 +++++++++++++++++- 2 files changed, 242 insertions(+), 25 deletions(-) diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index b82223f3acca..9ca418aa04a9 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -8,7 +8,8 @@ use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as paypal; -use self::transformers::PaypalMeta; +use self::transformers::{PaypalAuthResponse, PaypalMeta}; +use super::utils::PaymentsCompleteAuthorizeRequestData; use crate::{ configs::settings, connector::{ @@ -391,24 +392,27 @@ impl ConnectorIntegration CustomResult { - match data.payment_method { - diesel_models::enums::PaymentMethod::Wallet - | diesel_models::enums::PaymentMethod::BankRedirect => { - let response: paypal::PaypalRedirectResponse = res - .response - .parse_struct("paypal PaymentsRedirectResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let response: PaypalAuthResponse = + res.response + .parse_struct("paypal PaypalAuthResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + match response { + PaypalAuthResponse::PaypalOrdersResponse(response) => { types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), http_code: res.status_code, }) } - _ => { - let response: paypal::PaypalOrdersResponse = res - .response - .parse_struct("paypal PaymentsOrderResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + PaypalAuthResponse::PaypalRedirectResponse(response) => { + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + PaypalAuthResponse::PaypalThreeDsResponse(response) => { types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), @@ -450,10 +454,10 @@ impl req: &types::PaymentsCompleteAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult { - let paypal_meta: PaypalMeta = to_connector_meta(req.request.connector_meta.clone())?; - let complete_authorize_url = match paypal_meta.psync_flow { - transformers::PaypalPaymentIntent::Authorize => "authorize".to_string(), - transformers::PaypalPaymentIntent::Capture => "capture".to_string(), + let complete_authorize_url = if req.request.is_auto_capture()? { + "capture".to_string() + } else { + "authorize".to_string() }; Ok(format!( "{}v2/checkout/orders/{}/{complete_authorize_url}", @@ -493,7 +497,7 @@ impl ) -> CustomResult { let response: paypal::PaypalOrdersResponse = res .response - .parse_struct("paypal PaymentsOrderResponse") + .parse_struct("paypal PaypalOrdersResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -559,6 +563,19 @@ impl ConnectorIntegration { + format!( + "v2/checkout/orders/{}", + req.request + .connector_transaction_id + .get_connector_transaction_id() + .change_context( + errors::ConnectorError::MissingConnectorTransactionID + )? + ) + } }; Ok(format!("{}{psync_url}", self.base_url(connectors))) } diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index bf40cf0a9357..932cf67addd4 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -62,6 +62,7 @@ mod webhook_headers { pub enum PaypalPaymentIntent { Capture, Authorize, + Authenticate, } #[derive(Default, Debug, Clone, Serialize, Eq, PartialEq, Deserialize)] @@ -90,6 +91,23 @@ pub struct CardRequest { name: Secret, number: Option, security_code: Option>, + attributes: Option, +} + +#[derive(Debug, Serialize)] +pub struct ThreeDsSetting { + verification: ThreeDsMethod, +} + +#[derive(Debug, Serialize)] +pub struct ThreeDsMethod { + method: ThreeDsType, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ThreeDsType { + ScaAlways, } #[derive(Debug, Serialize)] @@ -251,12 +269,22 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP let card = item.router_data.request.get_card()?; let expiry = Some(card.get_expiry_date_as_yyyymm("-")); + let attributes = match item.router_data.auth_type { + api_models::enums::AuthenticationType::ThreeDs => Some(ThreeDsSetting { + verification: ThreeDsMethod { + method: ThreeDsType::ScaAlways, + }, + }), + api_models::enums::AuthenticationType::NoThreeDs => None, + }; + let payment_source = Some(PaymentSourceItem::Card(CardRequest { billing_address: get_address_info(item.router_data.address.billing.as_ref())?, expiry, name: ccard.card_holder_name.clone(), number: Some(ccard.card_number.clone()), security_code: Some(ccard.card_cvc.clone()), + attributes, })); Ok(Self { @@ -631,7 +659,14 @@ pub struct PurchaseUnitItem { pub payments: PaymentsCollection, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalThreeDsResponse { + id: String, + status: PaypalOrderStatus, + links: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct PaypalOrdersResponse { id: String, intent: PaypalPaymentIntent, @@ -653,10 +688,21 @@ pub struct PaypalRedirectResponse { links: Vec, } +// Note: Don't change order of deserialization of variant, priority is in descending order +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum PaypalAuthResponse { + PaypalOrdersResponse(PaypalOrdersResponse), + PaypalRedirectResponse(PaypalRedirectResponse), + PaypalThreeDsResponse(PaypalThreeDsResponse), +} + +// Note: Don't change order of deserialization of variant, priority is in descending order #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum PaypalSyncResponse { PaypalOrdersSyncResponse(PaypalOrdersResponse), + PaypalThreeDsSyncResponse(PaypalThreeDsSyncResponse), PaypalRedirectSyncResponse(PaypalRedirectResponse), PaypalPaymentsSyncResponse(PaypalPaymentsSyncResponse), } @@ -669,6 +715,24 @@ pub struct PaypalPaymentsSyncResponse { supplementary_data: PaypalSupplementaryData, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalThreeDsSyncResponse { + id: String, + status: PaypalOrderStatus, + // provided to separated response of card's 3DS from other + payment_source: CardsData, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CardsData { + card: CardDetails, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CardDetails { + last_digits: String, +} + #[derive(Debug, Serialize, Deserialize)] pub struct PaypalMeta { pub authorize_id: Option, @@ -700,6 +764,7 @@ fn get_id_based_on_intent( .next()? .id, ), + PaypalPaymentIntent::Authenticate => None, } }() .ok_or_else(|| errors::ConnectorError::MissingConnectorTransactionID.into()) @@ -738,6 +803,10 @@ impl }), types::ResponseId::ConnectorTransactionId(item.response.id), ), + + PaypalPaymentIntent::Authenticate => { + Err(errors::ConnectorError::ResponseDeserializationFailed)? + } }; //payment collection will always have only one element as we only make one transaction per order. let payment_collection = &item @@ -775,10 +844,9 @@ impl } fn get_redirect_url( - item: PaypalRedirectResponse, + link_vec: Vec, ) -> CustomResult, errors::ConnectorError> { let mut link: Option = None; - let link_vec = item.links; for item2 in link_vec.iter() { if item2.rel == "payer-action" { link = item2.href.clone(); @@ -787,12 +855,24 @@ fn get_redirect_url( Ok(link) } -impl TryFrom> - for types::RouterData +impl + TryFrom< + types::ResponseRouterData< + F, + PaypalSyncResponse, + types::PaymentsSyncData, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = error_stack::Report; fn try_from( - item: types::ResponseRouterData, + item: types::ResponseRouterData< + F, + PaypalSyncResponse, + types::PaymentsSyncData, + types::PaymentsResponseData, + >, ) -> Result { match item.response { PaypalSyncResponse::PaypalOrdersSyncResponse(response) => { @@ -816,6 +896,13 @@ impl TryFrom { + Self::try_from(types::ResponseRouterData { + response, + data: item.data, + http_code: item.http_code, + }) + } } } } @@ -832,7 +919,7 @@ impl item.response.clone().status, item.response.intent.clone(), )); - let link = get_redirect_url(item.response.clone())?; + let link = get_redirect_url(item.response.links.clone())?; let connector_meta = serde_json::json!(PaypalMeta { authorize_id: None, capture_id: None, @@ -857,6 +944,119 @@ impl } } +impl + TryFrom< + types::ResponseRouterData< + F, + PaypalThreeDsSyncResponse, + types::PaymentsSyncData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PaypalThreeDsSyncResponse, + types::PaymentsSyncData, + types::PaymentsResponseData, + >, + ) -> Result { + Ok(Self { + // status is hardcoded because this try_from will only be reached in card 3ds before the completion of complete authorize flow. + // also force sync won't be hit in terminal status thus leaving us with only one status to get here. + status: storage_enums::AttemptStatus::AuthenticationPending, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + }), + ..item.data + }) + } +} + +impl + TryFrom< + types::ResponseRouterData< + F, + PaypalThreeDsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PaypalThreeDsResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + ) -> Result { + let connector_meta = serde_json::json!(PaypalMeta { + authorize_id: None, + capture_id: None, + psync_flow: PaypalPaymentIntent::Authenticate // when there is no capture or auth id present + }); + + let status = storage_enums::AttemptStatus::foreign_from(( + item.response.clone().status, + PaypalPaymentIntent::Authenticate, + )); + let link = get_redirect_url(item.response.links.clone())?; + + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + redirection_data: Some(paypal_threeds_link(( + link, + item.data.request.complete_authorize_url.clone(), + ))?), + mandate_reference: None, + connector_metadata: Some(connector_meta), + network_txn_id: None, + connector_response_reference_id: None, + }), + ..item.data + }) + } +} + +fn paypal_threeds_link( + (redirect_url, complete_auth_url): (Option, Option), +) -> CustomResult { + let mut redirect_url = + redirect_url.ok_or(errors::ConnectorError::ResponseDeserializationFailed)?; + let complete_auth_url = + complete_auth_url.ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "complete_authorize_url", + })?; + let mut form_fields = std::collections::HashMap::from_iter( + redirect_url + .query_pairs() + .map(|(key, value)| (key.to_string(), value.to_string())), + ); + + // paypal requires return url to be passed as a field along with payer_action_url + form_fields.insert(String::from("redirect_uri"), complete_auth_url); + + // Do not include query params in the endpoint + redirect_url.set_query(None); + + Ok(services::RedirectForm::Form { + endpoint: redirect_url.to_string(), + method: services::Method::Get, + form_fields, + }) +} + impl TryFrom< types::ResponseRouterData,