diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 9263ca4a44..b6bb325400 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -19,6 +19,7 @@ use crate::{ types::{ self, api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt}, + storage::enums as storage_enums, ErrorResponse, Response, }, utils::{self, BytesExt}, @@ -44,10 +45,19 @@ impl api::RefundSync for Paypal {} impl Paypal { pub fn connector_transaction_id( &self, + payment_method: Option, connector_meta: &Option, ) -> CustomResult, errors::ConnectorError> { - let meta: PaypalMeta = to_connector_meta(connector_meta.clone())?; - Ok(meta.authorize_id) + match payment_method { + Some(storage_models::enums::PaymentMethod::Wallet) => { + let meta: PaypalMeta = to_connector_meta(connector_meta.clone())?; + Ok(Some(meta.order_id)) + } + _ => { + let meta: PaypalMeta = to_connector_meta(connector_meta.clone())?; + Ok(meta.authorize_id) + } + } } pub fn get_order_error_response( @@ -187,13 +197,11 @@ impl types::PaymentsResponseData, > for Paypal { - // Not Implemented (R) } impl ConnectorIntegration for Paypal { - //TODO: implement sessions flow } impl ConnectorIntegration @@ -317,7 +325,7 @@ impl ConnectorIntegration CustomResult { - Ok(format!("{}v2/checkout/orders", self.base_url(connectors),)) + Ok(format!("{}v2/checkout/orders", self.base_url(connectors))) } fn get_request_body( @@ -355,15 +363,30 @@ impl ConnectorIntegration CustomResult { - let response: paypal::PaypalOrdersResponse = res - .response - .parse_struct("Paypal PaymentsAuthorizeResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) + match data.payment_method { + storage_models::enums::PaymentMethod::Wallet => { + let response: paypal::PaypalRedirectResponse = res + .response + .parse_struct("paypal PaymentsRedirectResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + 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)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + } } fn get_error_response( @@ -381,6 +404,74 @@ impl types::PaymentsResponseData, > for Paypal { + fn get_headers( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let paypal_meta: PaypalMeta = to_connector_meta(req.request.connector_meta.clone())?; + Ok(format!( + "{}v2/checkout/orders/{}/capture", + self.base_url(connectors), + paypal_meta.order_id + )) + } + + fn build_request( + &self, + req: &types::PaymentsCompleteAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCompleteAuthorizeType::get_url( + self, req, connectors, + )?) + .headers(types::PaymentsCompleteAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsCompleteAuthorizeType::get_request_body( + self, req, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCompleteAuthorizeRouterData, + res: Response, + ) -> CustomResult { + let response: paypal::PaypalOrdersResponse = res + .response + .parse_struct("paypal PaymentsOrderResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } } impl ConnectorIntegration @@ -403,22 +494,31 @@ impl ConnectorIntegration CustomResult { - let capture_id = req - .request - .connector_transaction_id - .get_connector_transaction_id() - .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; let paypal_meta: PaypalMeta = to_connector_meta(req.request.connector_meta.clone())?; - let psync_url = match paypal_meta.psync_flow { - transformers::PaypalPaymentIntent::Authorize => format!( - "v2/payments/authorizations/{}", - paypal_meta.authorize_id.unwrap_or_default() - ), - transformers::PaypalPaymentIntent::Capture => { - format!("v2/payments/captures/{}", capture_id) + match req.payment_method { + storage_models::enums::PaymentMethod::Wallet => Ok(format!( + "{}v2/checkout/orders/{}", + self.base_url(connectors), + paypal_meta.order_id + )), + _ => { + let capture_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + let psync_url = match paypal_meta.psync_flow { + transformers::PaypalPaymentIntent::Authorize => format!( + "v2/payments/authorizations/{}", + paypal_meta.authorize_id.unwrap_or_default() + ), + transformers::PaypalPaymentIntent::Capture => { + format!("v2/payments/captures/{}", capture_id) + } + }; + Ok(format!("{}{}", self.base_url(connectors), psync_url)) } - }; - Ok(format!("{}{}", self.base_url(connectors), psync_url,)) + } } fn build_request( @@ -440,15 +540,30 @@ impl ConnectorIntegration CustomResult { - let response: paypal::PaypalPaymentsSyncResponse = res - .response - .parse_struct("paypal PaymentsSyncResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) + match data.payment_method { + storage_models::enums::PaymentMethod::Wallet => { + let response: paypal::PaypalOrdersResponse = res + .response + .parse_struct("paypal PaymentsOrderResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + _ => { + let response: paypal::PaypalPaymentsSyncResponse = res + .response + .parse_struct("paypal PaymentsSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + } } fn get_error_response( diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 30d2002d8c..012dcb1256 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -1,6 +1,7 @@ use common_utils::errors::CustomResult; use masking::Secret; use serde::{Deserialize, Serialize}; +use url::Url; use crate::{ connector::utils::{ @@ -8,7 +9,7 @@ use crate::{ PaymentsAuthorizeRequestData, }, core::errors, - pii, + pii, services, types::{self, api, storage::enums as storage_enums, transformers::ForeignFrom}, }; @@ -47,10 +48,28 @@ pub struct CardRequest { security_code: Option>, } +#[derive(Debug, Serialize)] +pub struct RedirectRequest { + name: Secret, + country_code: api_models::enums::CountryCode, + experience_context: ContextStruct, +} + +#[derive(Debug, Serialize)] +pub struct ContextStruct { + return_url: Option, +} + +#[derive(Debug, Serialize)] +pub struct PaypalRedirectionRequest { + experience_context: ContextStruct, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "lowercase")] pub enum PaymentSourceItem { Card(CardRequest), + Paypal(PaypalRedirectionRequest), } #[derive(Debug, Serialize)] @@ -111,6 +130,35 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaypalPaymentsRequest { payment_source, }) } + api::PaymentMethodData::Wallet(ref wallet_data) => match wallet_data { + api_models::payments::WalletData::PaypalRedirect(_) => { + let intent = PaypalPaymentIntent::Capture; + let amount = OrderAmount { + currency_code: item.request.currency, + value: item.request.amount.to_string(), + }; + let reference_id = item.attempt_id.clone(); + let purchase_units = vec![PurchaseUnitRequest { + reference_id, + amount, + }]; + let payment_source = + Some(PaymentSourceItem::Paypal(PaypalRedirectionRequest { + experience_context: ContextStruct { + return_url: item.request.complete_authorize_url.clone(), + }, + })); + + Ok(Self { + intent, + purchase_units, + payment_source, + }) + } + _ => Err(errors::ConnectorError::NotImplemented( + "Payment Method".to_string(), + ))?, + }, _ => Err(errors::ConnectorError::NotImplemented("Payment Method".to_string()).into()), } } @@ -198,10 +246,9 @@ impl ForeignFrom<(PaypalOrderStatus, PaypalPaymentIntent)> for storage_enums::At } } PaypalOrderStatus::Voided => Self::Voided, - PaypalOrderStatus::Created | PaypalOrderStatus::Saved | PaypalOrderStatus::Approved => { - Self::Pending - } - PaypalOrderStatus::PayerActionRequired => Self::Authorizing, + PaypalOrderStatus::Created | PaypalOrderStatus::Saved => Self::Pending, + PaypalOrderStatus::Approved => Self::AuthenticationSuccessful, + PaypalOrderStatus::PayerActionRequired => Self::AuthenticationPending, } } } @@ -235,6 +282,20 @@ pub struct PaypalOrdersResponse { purchase_units: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalLinks { + href: Option, + rel: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalRedirectResponse { + id: String, + intent: PaypalPaymentIntent, + status: PaypalOrderStatus, + links: Vec, +} + #[derive(Debug, Serialize, Deserialize)] pub struct PaypalPaymentsSyncResponse { id: String, @@ -312,11 +373,13 @@ impl types::ResponseId::NoResponseId, ), }; + let status = storage_enums::AttemptStatus::foreign_from(( + item.response.status, + item.response.intent, + )); + Ok(Self { - status: storage_enums::AttemptStatus::foreign_from(( - item.response.status, - item.response.intent, - )), + status, response: Ok(types::PaymentsResponseData::TransactionResponse { resource_id: capture_id, redirection_data: None, @@ -328,6 +391,54 @@ impl } } +fn get_redirect_url( + item: PaypalRedirectResponse, +) -> 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(); + } + } + Ok(link) +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + let status = storage_enums::AttemptStatus::foreign_from(( + item.response.clone().status, + item.response.intent.clone(), + )); + let link = get_redirect_url(item.response.clone())?; + let connector_meta = serde_json::json!(PaypalMeta { + authorize_id: None, + order_id: item.response.id, + psync_flow: item.response.intent + }); + + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: Some(services::RedirectForm::from(( + link.ok_or(errors::ConnectorError::ResponseDeserializationFailed)?, + services::Method::Get, + ))), + mandate_reference: None, + connector_metadata: Some(connector_meta), + }), + ..item.data + }) + } +} + impl TryFrom< types::ResponseRouterData, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index f38e262b8c..c465f68360 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -568,7 +568,12 @@ impl api::ConnectorTransactionId for Paypal { &self, payment_attempt: storage::PaymentAttempt, ) -> Result, errors::ApiErrorResponse> { - let metadata = Self::connector_transaction_id(self, &payment_attempt.connector_metadata); + let payment_method = payment_attempt.payment_method; + let metadata = Self::connector_transaction_id( + self, + payment_method, + &payment_attempt.connector_metadata, + ); match metadata { Ok(data) => Ok(data), _ => Err(errors::ApiErrorResponse::ResourceIdNotFound), @@ -577,7 +582,7 @@ impl api::ConnectorTransactionId for Paypal { } impl TryFrom> for types::PaymentsCaptureData { - type Error = errors::ApiErrorResponse; + type Error = error_stack::Report; fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { let payment_data = additional_data.payment_data; @@ -585,12 +590,7 @@ impl TryFrom> for types::PaymentsCaptureD &additional_data.state.conf.connectors, &additional_data.connector_name, api::GetToken::Connector, - ); - let connectors = match connector { - Ok(conn) => *conn.connector, - _ => Err(errors::ApiErrorResponse::ResourceIdNotFound)?, - }; - + )?; let amount_to_capture: i64 = payment_data .payment_attempt .amount_to_capture @@ -598,7 +598,8 @@ impl TryFrom> for types::PaymentsCaptureD Ok(Self { amount_to_capture, currency: payment_data.currency, - connector_transaction_id: connectors + connector_transaction_id: connector + .connector .connector_transaction_id(payment_data.payment_attempt.clone())? .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, payment_amount: payment_data.amount.into(), @@ -608,7 +609,7 @@ impl TryFrom> for types::PaymentsCaptureD } impl TryFrom> for types::PaymentsCancelData { - type Error = errors::ApiErrorResponse; + type Error = error_stack::Report; fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { let payment_data = additional_data.payment_data; @@ -616,15 +617,12 @@ impl TryFrom> for types::PaymentsCancelDa &additional_data.state.conf.connectors, &additional_data.connector_name, api::GetToken::Connector, - ); - let connectors = match connector { - Ok(conn) => *conn.connector, - _ => Err(errors::ApiErrorResponse::ResourceIdNotFound)?, - }; + )?; Ok(Self { amount: Some(payment_data.amount.into()), currency: Some(payment_data.currency), - connector_transaction_id: connectors + connector_transaction_id: connector + .connector .connector_transaction_id(payment_data.payment_attempt.clone())? .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, cancellation_reason: payment_data.payment_attempt.cancellation_reason, @@ -693,6 +691,7 @@ impl TryFrom> for types::CompleteAuthoriz let browser_info: Option = payment_data .payment_attempt .browser_info + .clone() .map(|b| b.parse_value("BrowserInformation")) .transpose() .change_context(errors::ApiErrorResponse::InvalidDataValue { diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index b28a89c8f8..77dd82cc39 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -4,7 +4,7 @@ function find_prev_connector() { git checkout $self cp $self $self.tmp # add new connector to existing list and sort it - connectors=(aci adyen airwallex applepay authorizedotnet bambora bluesnap braintree checkout coinbase cybersource dlocal fiserv forte globalpay klarna mollie multisafepay nexinets nuvei opennode payeezy payu rapyd shift4 stripe trustpay worldline worldpay "$1") + connectors=(aci adyen airwallex applepay authorizedotnet bambora bluesnap braintree checkout coinbase cybersource dlocal fiserv forte globalpay klarna mollie multisafepay nexinets nuvei opennode paypal payeezy payu rapyd shift4 stripe trustpay worldline worldpay "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res=`echo ${sorted[@]}` sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp