refactor(connector): [Paypal] Add support for both BodyKey and SignatureKey (#2633)

Co-authored-by: Mani Chandra Dulam <mani.dchandra@juspay.in>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com>
This commit is contained in:
Swangi Kumari
2023-11-21 19:44:40 +05:30
committed by GitHub
parent 938b63a1fc
commit d8fcd3c971
2 changed files with 325 additions and 56 deletions

View File

@ -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<String>,
client_id: &Secret<String>,
) -> 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<Vec<(String, request::Maskable<String>)>, 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<api::AccessTokenAuth, types::AccessTokenRequestData, t
req: &types::RefreshTokenRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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<Vec<(String, request::Maskable<String>)>, 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![
(

View File

@ -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<String>, //reference for an item in purchase_units
invoice_id: Option<String>, //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<String>, //Used to reconcile client transactions with PayPal transactions.
amount: OrderAmount,
amount: OrderRequestAmount,
#[serde(skip_serializing_if = "Option::is_none")]
payee: Option<Payee>,
shipping: Option<ShippingAddress>,
items: Vec<ItemDetails>,
}
#[derive(Debug, Serialize)]
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct Payee {
merchant_id: Secret<String>,
}
#[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<Secret<String>>,
postal_code: Option<Secret<String>>,
country_code: api_models::enums::CountryAlpha2,
admin_area_2: Option<String>,
}
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
pub struct ShippingAddress {
address: Option<Address>,
name: Option<ShippingName>,
}
impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for ShippingAddress {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>,
) -> Result<Self, Self::Error> {
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<Secret<String>>,
}
#[derive(Debug, Serialize)]
@ -124,6 +224,22 @@ pub struct RedirectRequest {
pub struct ContextStruct {
return_url: Option<String>,
cancel_url: Option<String>,
user_action: Option<UserAction>,
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<Payee> {
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<errors::ConnectorError>;
fn try_from(
item: &PaypalRouterData<&types::PaymentsAuthorizeRouterData>,
) -> Result<Self, Self::Error> {
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<F, T> TryFrom<types::ResponseRouterData<F, PaypalAuthUpdateResponse, T, typ
}
#[derive(Debug)]
pub struct PaypalAuthType {
pub(super) api_key: Secret<String>,
pub(super) key1: Secret<String>,
pub enum PaypalAuthType {
TemporaryAuth,
AuthWithDetails(PaypalConnectorCredentials),
}
impl TryFrom<&types::ConnectorAuthType> for PaypalAuthType {
#[derive(Debug)]
pub enum PaypalConnectorCredentials {
StandardIntegration(StandardFlowCredentials),
PartnerIntegration(PartnerFlowCredentials),
}
impl PaypalConnectorCredentials {
pub fn get_client_id(&self) -> Secret<String> {
match self {
Self::StandardIntegration(item) => item.client_id.clone(),
Self::PartnerIntegration(item) => item.client_id.clone(),
}
}
pub fn get_client_secret(&self) -> Secret<String> {
match self {
Self::StandardIntegration(item) => item.client_secret.clone(),
Self::PartnerIntegration(item) => item.client_secret.clone(),
}
}
pub fn get_payer_id(&self) -> Option<Secret<String>> {
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<String>,
pub(super) client_secret: Secret<String>,
}
#[derive(Debug)]
pub struct PartnerFlowCredentials {
pub(super) client_id: Secret<String>,
pub(super) client_secret: Secret<String>,
pub(super) payer_id: Secret<String>,
}
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<errors::ConnectorError>;
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
fn try_from(auth_type: &ConnectorAuthType) -> Result<Self, Self::Error> {
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)?,
}
}