diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index e514ebbed2fc..0e8cff8c0569 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -5,10 +5,10 @@ use base64::Engine; use common_utils::ext_traits::ByteSliceExt; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; -use masking::PeekInterface; +use masking::{ExposeInterface, PeekInterface, Secret}; use transformers as paypal; -use self::transformers::{PaypalAuthResponse, PaypalMeta, PaypalWebhookEventType}; +use self::transformers::{auth_headers, PaypalAuthResponse, PaypalMeta, PaypalWebhookEventType}; use super::utils::PaymentsCompleteAuthorizeRequestData; use crate::{ configs::settings, @@ -31,7 +31,7 @@ use crate::{ self, api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource}, transformers::ForeignFrom, - ErrorResponse, Response, + ConnectorAuthType, ErrorResponse, Response, }, utils::{self, BytesExt}, }; @@ -110,8 +110,8 @@ where .clone() .ok_or(errors::ConnectorError::FailedToObtainAuthType)?; let key = &req.attempt_id; - - Ok(vec![ + let auth = paypal::PaypalAuthType::try_from(&req.connector_auth_type)?; + let mut headers = vec![ ( headers::CONTENT_TYPE.to_string(), self.get_content_type().to_string().into(), @@ -121,17 +121,57 @@ where format!("Bearer {}", access_token.token.peek()).into_masked(), ), ( - "Prefer".to_string(), + auth_headers::PREFER.to_string(), "return=representation".to_string().into(), ), ( - "PayPal-Request-Id".to_string(), + auth_headers::PAYPAL_REQUEST_ID.to_string(), key.to_string().into_masked(), ), - ]) + ]; + if let Ok(paypal::PaypalConnectorCredentials::PartnerIntegration(credentials)) = + auth.get_credentials() + { + let auth_assertion_header = + construct_auth_assertion_header(&credentials.payer_id, &credentials.client_id); + headers.extend(vec![ + ( + auth_headers::PAYPAL_AUTH_ASSERTION.to_string(), + auth_assertion_header.to_string().into_masked(), + ), + ( + auth_headers::PAYPAL_PARTNER_ATTRIBUTION_ID.to_string(), + "HyperSwitchPPCP_SP".to_string().into(), + ), + ]) + } else { + headers.extend(vec![( + auth_headers::PAYPAL_PARTNER_ATTRIBUTION_ID.to_string(), + "HyperSwitchlegacy_Ecom".to_string().into(), + )]) + } + Ok(headers) } } +fn construct_auth_assertion_header( + payer_id: &Secret, + client_id: &Secret, +) -> String { + let algorithm = consts::BASE64_ENGINE + .encode("{\"alg\":\"none\"}") + .to_string(); + let merchant_credentials = format!( + "{{\"iss\":\"{}\",\"payer_id\":\"{}\"}}", + client_id.clone().expose(), + payer_id.clone().expose() + ); + let encoded_credentials = consts::BASE64_ENGINE + .encode(merchant_credentials) + .to_string(); + format!("{algorithm}.{encoded_credentials}.") +} + impl ConnectorCommon for Paypal { fn id(&self) -> &'static str { "paypal" @@ -151,14 +191,14 @@ impl ConnectorCommon for Paypal { fn get_auth_header( &self, - auth_type: &types::ConnectorAuthType, + auth_type: &ConnectorAuthType, ) -> CustomResult)>, errors::ConnectorError> { - let auth: paypal::PaypalAuthType = auth_type - .try_into() - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let auth = paypal::PaypalAuthType::try_from(auth_type)?; + let credentials = auth.get_credentials()?; + Ok(vec![( headers::AUTHORIZATION.to_string(), - auth.api_key.into_masked(), + credentials.get_client_secret().into_masked(), )]) } @@ -260,15 +300,9 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { - let auth: paypal::PaypalAuthType = (&req.connector_auth_type) - .try_into() - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - - let auth_id = auth - .key1 - .zip(auth.api_key) - .map(|(key1, api_key)| format!("{}:{}", key1, api_key)); - let auth_val = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_id.peek())); + let auth = paypal::PaypalAuthType::try_from(&req.connector_auth_type)?; + let credentials = auth.get_credentials()?; + let auth_val = credentials.generate_authorization_value(); Ok(vec![ ( @@ -998,15 +1032,9 @@ impl >, _connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let auth: paypal::PaypalAuthType = (&req.connector_auth_type) - .try_into() - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - - let auth_id = auth - .key1 - .zip(auth.api_key) - .map(|(key1, api_key)| format!("{}:{}", key1, api_key)); - let auth_val = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_id.peek())); + let auth = paypal::PaypalAuthType::try_from(&req.connector_auth_type)?; + let credentials = auth.get_credentials()?; + let auth_val = credentials.generate_authorization_value(); Ok(vec![ ( diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 5468c6bb8061..d023077ff008 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -1,7 +1,8 @@ use api_models::{enums, payments::BankRedirectData}; +use base64::Engine; use common_utils::errors::CustomResult; use error_stack::{IntoReport, ResultExt}; -use masking::Secret; +use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use url::Url; @@ -11,10 +12,11 @@ use crate::{ self, to_connector_meta, AccessTokenRequestInfo, AddressDetailsData, BankRedirectBillingData, CardData, PaymentsAuthorizeRequestData, }, + consts, core::errors, services, types::{ - self, api, storage::enums as storage_enums, transformers::ForeignFrom, + self, api, storage::enums as storage_enums, transformers::ForeignFrom, ConnectorAuthType, VerifyWebhookSourceResponseData, }, }; @@ -57,6 +59,12 @@ mod webhook_headers { pub const PAYPAL_CERT_URL: &str = "paypal-cert-url"; pub const PAYPAL_AUTH_ALGO: &str = "paypal-auth-algo"; } +pub mod auth_headers { + pub const PAYPAL_PARTNER_ATTRIBUTION_ID: &str = "PayPal-Partner-Attribution-Id"; + pub const PREFER: &str = "Prefer"; + pub const PAYPAL_REQUEST_ID: &str = "PayPal-Request-Id"; + pub const PAYPAL_AUTH_ASSERTION: &str = "PayPal-Auth-Assertion"; +} #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "UPPERCASE")] @@ -72,19 +80,111 @@ pub struct OrderAmount { pub value: String, } +#[derive(Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct OrderRequestAmount { + pub currency_code: storage_enums::Currency, + pub value: String, + pub breakdown: AmountBreakdown, +} + +impl From<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for OrderRequestAmount { + fn from(item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>) -> Self { + Self { + currency_code: item.router_data.request.currency, + value: item.amount.to_owned(), + breakdown: AmountBreakdown { + item_total: OrderAmount { + currency_code: item.router_data.request.currency, + value: item.amount.to_owned(), + }, + }, + } + } +} + +#[derive(Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct AmountBreakdown { + item_total: OrderAmount, +} + #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct PurchaseUnitRequest { reference_id: Option, //reference for an item in purchase_units invoice_id: Option, //The API caller-provided external invoice number for this order. Appears in both the payer's transaction history and the emails that the payer receives. custom_id: Option, //Used to reconcile client transactions with PayPal transactions. - amount: OrderAmount, + amount: OrderRequestAmount, + #[serde(skip_serializing_if = "Option::is_none")] + payee: Option, + shipping: Option, + items: Vec, } -#[derive(Debug, Serialize)] +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct Payee { + merchant_id: Secret, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct ItemDetails { + name: String, + quantity: u16, + unit_amount: OrderAmount, +} + +impl From<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for ItemDetails { + fn from(item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>) -> Self { + Self { + name: format!( + "Payment for invoice {}", + item.router_data.connector_request_reference_id + ), + quantity: 1, + unit_amount: OrderAmount { + currency_code: item.router_data.request.currency, + value: item.amount.to_string(), + }, + } + } +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct Address { address_line_1: Option>, postal_code: Option>, country_code: api_models::enums::CountryAlpha2, + admin_area_2: Option, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct ShippingAddress { + address: Option
, + name: Option, +} + +impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for ShippingAddress { + type Error = error_stack::Report; + + fn try_from( + item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>, + ) -> Result { + Ok(Self { + address: get_address_info(item.router_data.address.shipping.as_ref())?, + name: Some(ShippingName { + full_name: item + .router_data + .address + .shipping + .as_ref() + .and_then(|inner_data| inner_data.address.as_ref()) + .and_then(|inner_data| inner_data.first_name.clone()), + }), + }) + } +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct ShippingName { + full_name: Option>, } #[derive(Debug, Serialize)] @@ -124,6 +224,22 @@ pub struct RedirectRequest { pub struct ContextStruct { return_url: Option, cancel_url: Option, + user_action: Option, + shipping_preference: ShippingPreference, +} + +#[derive(Debug, Serialize)] +pub enum UserAction { + #[serde(rename = "PAY_NOW")] + PayNow, +} + +#[derive(Debug, Serialize)] +pub enum ShippingPreference { + #[serde(rename = "SET_PROVIDED_ADDRESS")] + SetProvidedAddress, + #[serde(rename = "GET_FROM_FILE")] + GetFromFile, } #[derive(Debug, Serialize)] @@ -158,6 +274,7 @@ fn get_address_info( country_code: address.get_country()?.to_owned(), address_line_1: address.line1.clone(), postal_code: address.zip.clone(), + admin_area_2: address.city.clone(), }), None => None, }; @@ -180,6 +297,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::Giropay { @@ -194,6 +317,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::Ideal { @@ -208,6 +337,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::Sofort { @@ -220,6 +355,12 @@ fn get_payment_source( experience_context: ContextStruct { return_url: item.request.complete_authorize_url.clone(), cancel_url: item.request.complete_authorize_url.clone(), + shipping_preference: if item.address.shipping.is_some() { + ShippingPreference::SetProvidedAddress + } else { + ShippingPreference::GetFromFile + }, + user_action: Some(UserAction::PayNow), }, })), BankRedirectData::BancontactCard { .. } @@ -247,11 +388,24 @@ fn get_payment_source( } } +fn get_payee(auth_type: &PaypalAuthType) -> Option { + auth_type + .get_credentials() + .ok() + .and_then(|credentials| credentials.get_payer_id()) + .map(|payer_id| Payee { + merchant_id: payer_id, + }) +} + impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalPaymentsRequest { type Error = error_stack::Report; fn try_from( item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { + let paypal_auth: PaypalAuthType = + PaypalAuthType::try_from(&item.router_data.connector_auth_type)?; + let payee = get_payee(&paypal_auth); match item.router_data.request.payment_method_data { api_models::payments::PaymentMethodData::Card(ref ccard) => { let intent = if item.router_data.request.is_auto_capture()? { @@ -259,18 +413,20 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP } else { PaypalPaymentIntent::Authorize }; - let amount = OrderAmount { - currency_code: item.router_data.request.currency, - value: item.amount.to_owned(), - }; + let amount = OrderRequestAmount::from(item); let connector_request_reference_id = item.router_data.connector_request_reference_id.clone(); + let shipping_address = ShippingAddress::try_from(item)?; + let item_details = vec![ItemDetails::from(item)]; let purchase_units = vec![PurchaseUnitRequest { reference_id: Some(connector_request_reference_id.clone()), custom_id: Some(connector_request_reference_id.clone()), invoice_id: Some(connector_request_reference_id), amount, + payee, + shipping: Some(shipping_address), + items: item_details, }]; let card = item.router_data.request.get_card()?; let expiry = Some(card.get_expiry_date_as_yyyymm("-")); @@ -306,25 +462,29 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP } else { PaypalPaymentIntent::Authorize }; - let amount = OrderAmount { - currency_code: item.router_data.request.currency, - value: item.amount.to_owned(), - }; + let amount = OrderRequestAmount::from(item); let connector_req_reference_id = item.router_data.connector_request_reference_id.clone(); + let shipping_address = ShippingAddress::try_from(item)?; + let item_details = vec![ItemDetails::from(item)]; let purchase_units = vec![PurchaseUnitRequest { reference_id: Some(connector_req_reference_id.clone()), custom_id: Some(connector_req_reference_id.clone()), invoice_id: Some(connector_req_reference_id), amount, + payee, + shipping: Some(shipping_address), + items: item_details, }]; let payment_source = Some(PaymentSourceItem::Paypal(PaypalRedirectionRequest { experience_context: ContextStruct { return_url: item.router_data.request.complete_authorize_url.clone(), cancel_url: item.router_data.request.complete_authorize_url.clone(), + shipping_preference: ShippingPreference::SetProvidedAddress, + user_action: Some(UserAction::PayNow), }, })); @@ -374,18 +534,20 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP connector: "Paypal".to_string(), })? }; - let amount = OrderAmount { - currency_code: item.router_data.request.currency, - value: item.amount.to_owned(), - }; + let amount = OrderRequestAmount::from(item); let connector_req_reference_id = item.router_data.connector_request_reference_id.clone(); + let shipping_address = ShippingAddress::try_from(item)?; + let item_details = vec![ItemDetails::from(item)]; let purchase_units = vec![PurchaseUnitRequest { reference_id: Some(connector_req_reference_id.clone()), custom_id: Some(connector_req_reference_id.clone()), invoice_id: Some(connector_req_reference_id), amount, + payee, + shipping: Some(shipping_address), + items: item_details, }]; let payment_source = Some(get_payment_source(item.router_data, bank_redirection_data)?); @@ -604,19 +766,98 @@ impl TryFrom, - pub(super) key1: Secret, +pub enum PaypalAuthType { + TemporaryAuth, + AuthWithDetails(PaypalConnectorCredentials), +} + +#[derive(Debug)] +pub enum PaypalConnectorCredentials { + StandardIntegration(StandardFlowCredentials), + PartnerIntegration(PartnerFlowCredentials), } -impl TryFrom<&types::ConnectorAuthType> for PaypalAuthType { +impl PaypalConnectorCredentials { + pub fn get_client_id(&self) -> Secret { + match self { + Self::StandardIntegration(item) => item.client_id.clone(), + Self::PartnerIntegration(item) => item.client_id.clone(), + } + } + + pub fn get_client_secret(&self) -> Secret { + match self { + Self::StandardIntegration(item) => item.client_secret.clone(), + Self::PartnerIntegration(item) => item.client_secret.clone(), + } + } + + pub fn get_payer_id(&self) -> Option> { + match self { + Self::StandardIntegration(_) => None, + Self::PartnerIntegration(item) => Some(item.payer_id.clone()), + } + } + + pub fn generate_authorization_value(&self) -> String { + let auth_id = format!( + "{}:{}", + self.get_client_id().expose(), + self.get_client_secret().expose(), + ); + format!("Basic {}", consts::BASE64_ENGINE.encode(auth_id)) + } +} + +#[derive(Debug)] +pub struct StandardFlowCredentials { + pub(super) client_id: Secret, + pub(super) client_secret: Secret, +} + +#[derive(Debug)] +pub struct PartnerFlowCredentials { + pub(super) client_id: Secret, + pub(super) client_secret: Secret, + pub(super) payer_id: Secret, +} + +impl PaypalAuthType { + pub fn get_credentials( + &self, + ) -> CustomResult<&PaypalConnectorCredentials, errors::ConnectorError> { + match self { + Self::TemporaryAuth => Err(errors::ConnectorError::InvalidConnectorConfig { + config: "TemporaryAuth found in connector_account_details", + } + .into()), + Self::AuthWithDetails(credentials) => Ok(credentials), + } + } +} + +impl TryFrom<&ConnectorAuthType> for PaypalAuthType { type Error = error_stack::Report; - fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + fn try_from(auth_type: &ConnectorAuthType) -> Result { match auth_type { - types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { - api_key: api_key.to_owned(), - key1: key1.to_owned(), - }), + types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self::AuthWithDetails( + PaypalConnectorCredentials::StandardIntegration(StandardFlowCredentials { + client_id: key1.to_owned(), + client_secret: api_key.to_owned(), + }), + )), + types::ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Ok(Self::AuthWithDetails( + PaypalConnectorCredentials::PartnerIntegration(PartnerFlowCredentials { + client_id: key1.to_owned(), + client_secret: api_key.to_owned(), + payer_id: api_secret.to_owned(), + }), + )), + types::ConnectorAuthType::TemporaryAuth => Ok(Self::TemporaryAuth), _ => Err(errors::ConnectorError::FailedToObtainAuthType)?, } }