Files
Kiran Kumar 83e8bc0775 feat(connector): [Paypal] implement vaulting for paypal cards via zero mandates (#5324)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: swangi-kumari <swangi.12015941@lpu.in>
2024-11-25 06:59:51 +00:00

2207 lines
84 KiB
Rust

pub mod transformers;
use std::fmt::Write;
use base64::Engine;
use common_utils::{
ext_traits::ByteSliceExt,
request::RequestContent,
types::{AmountConvertor, MinorUnit, StringMajorUnit, StringMajorUnitForConnector},
};
use diesel_models::enums;
use error_stack::ResultExt;
use masking::{ExposeInterface, PeekInterface, Secret};
use router_env::logger;
#[cfg(feature = "payouts")]
use router_env::{instrument, tracing};
use transformers as paypal;
use self::transformers::{auth_headers, PaypalAuthResponse, PaypalMeta, PaypalWebhookEventType};
use super::utils::{
ConnectorErrorType, PaymentMethodDataType, PaymentsAuthorizeRequestData,
PaymentsCompleteAuthorizeRequestData,
};
use crate::{
configs::settings,
connector::utils::{
self as connector_utils, to_connector_meta, ConnectorErrorTypeMapping, RefundsRequestData,
},
consts,
core::{
errors::{self, CustomResult},
payments,
},
db::domain,
events::connector_api_logs::ConnectorEvent,
headers,
services::{
self,
request::{self, Mask},
ConnectorIntegration, ConnectorValidation, PaymentAction,
},
types::{
self,
api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource},
storage::enums as storage_enums,
transformers::{ForeignFrom, ForeignTryFrom},
ConnectorAuthType, ErrorResponse, Response,
},
utils::BytesExt,
};
#[derive(Clone)]
pub struct Paypal {
amount_converter: &'static (dyn AmountConvertor<Output = StringMajorUnit> + Sync),
}
impl Paypal {
pub fn new() -> &'static Self {
&Self {
amount_converter: &StringMajorUnitForConnector,
}
}
}
impl api::Payment for Paypal {}
impl api::PaymentSession for Paypal {}
impl api::PaymentToken for Paypal {}
impl api::ConnectorAccessToken for Paypal {}
impl api::MandateSetup for Paypal {}
impl api::PaymentAuthorize for Paypal {}
impl api::PaymentsCompleteAuthorize for Paypal {}
impl api::PaymentSync for Paypal {}
impl api::PaymentCapture for Paypal {}
impl api::PaymentVoid for Paypal {}
impl api::Refund for Paypal {}
impl api::RefundExecute for Paypal {}
impl api::RefundSync for Paypal {}
impl api::ConnectorVerifyWebhookSource for Paypal {}
impl api::PaymentPostSessionTokens for Paypal {}
impl api::PaymentSessionUpdate for Paypal {}
impl api::Payouts for Paypal {}
#[cfg(feature = "payouts")]
impl api::PayoutCreate for Paypal {}
#[cfg(feature = "payouts")]
impl api::PayoutFulfill for Paypal {}
#[cfg(feature = "payouts")]
impl api::PayoutSync for Paypal {}
impl Paypal {
pub fn get_order_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
//Handled error response separately for Orders as the end point is different for Orders - (Authorize) and Payments - (Capture, void, refund, rsync).
//Error response have different fields for Orders and Payments.
let response: paypal::PaypalOrderErrorResponse = res
.response
.parse_struct("Paypal ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_error_response_body(&response));
router_env::logger::info!(connector_response=?response);
let error_reason = response.details.clone().map(|order_errors| {
order_errors
.iter()
.map(|error| {
let mut reason = format!("description - {}", error.description);
if let Some(value) = &error.value {
reason.push_str(&format!(", value - {value}"));
}
if let Some(field) = error
.field
.as_ref()
.and_then(|field| field.split('/').last())
{
reason.push_str(&format!(", field - {field}"));
}
reason.push(';');
reason
})
.collect::<String>()
});
let errors_list = response.details.unwrap_or_default();
let option_error_code_message =
connector_utils::get_error_code_error_message_based_on_priority(
self.clone(),
errors_list
.into_iter()
.map(|errors| errors.into())
.collect(),
);
Ok(ErrorResponse {
status_code: res.status_code,
code: option_error_code_message
.clone()
.map(|error_code_message| error_code_message.error_code)
.unwrap_or(consts::NO_ERROR_CODE.to_string()),
message: option_error_code_message
.map(|error_code_message| error_code_message.error_message)
.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()),
reason: error_reason.or(Some(response.message)),
attempt_status: None,
connector_transaction_id: response.debug_id,
})
}
}
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Paypal
where
Self: ConnectorIntegration<Flow, Request, Response>,
{
fn build_headers(
&self,
req: &types::RouterData<Flow, Request, Response>,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
let access_token = req
.access_token
.clone()
.ok_or(errors::ConnectorError::FailedToObtainAuthType)?;
let key = &req.connector_request_reference_id;
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(),
),
(
headers::AUTHORIZATION.to_string(),
format!("Bearer {}", access_token.token.peek()).into_masked(),
),
(
auth_headers::PREFER.to_string(),
"return=representation".to_string().into(),
),
(
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"
}
fn get_currency_unit(&self) -> api::CurrencyUnit {
api::CurrencyUnit::Base
}
fn common_get_content_type(&self) -> &'static str {
"application/json"
}
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
connectors.paypal.base_url.as_ref()
}
fn get_auth_header(
&self,
auth_type: &ConnectorAuthType,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
let auth = paypal::PaypalAuthType::try_from(auth_type)?;
let credentials = auth.get_credentials()?;
Ok(vec![(
headers::AUTHORIZATION.to_string(),
credentials.get_client_secret().into_masked(),
)])
}
fn build_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
let response: paypal::PaypalPaymentErrorResponse = res
.response
.parse_struct("Paypal ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_error_response_body(&response));
router_env::logger::info!(connector_response=?response);
let error_reason = response
.details
.clone()
.map(|error_details| {
error_details
.iter()
.try_fold(String::new(), |mut acc, error| {
if let Some(description) = &error.description {
write!(acc, "description - {} ;", description)
.change_context(
errors::ConnectorError::ResponseDeserializationFailed,
)
.attach_printable("Failed to concatenate error details")
.map(|_| acc)
} else {
Ok(acc)
}
})
})
.transpose()?;
let reason = match error_reason {
Some(err_reason) => err_reason
.is_empty()
.then(|| response.message.to_owned())
.or(Some(err_reason)),
None => Some(response.message.to_owned()),
};
let errors_list = response.details.unwrap_or_default();
let option_error_code_message =
connector_utils::get_error_code_error_message_based_on_priority(
self.clone(),
errors_list
.into_iter()
.map(|errors| errors.into())
.collect(),
);
Ok(ErrorResponse {
status_code: res.status_code,
code: option_error_code_message
.clone()
.map(|error_code_message| error_code_message.error_code)
.unwrap_or(consts::NO_ERROR_CODE.to_string()),
message: option_error_code_message
.map(|error_code_message| error_code_message.error_message)
.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()),
reason,
attempt_status: None,
connector_transaction_id: response.debug_id,
})
}
}
impl ConnectorValidation for Paypal {
fn validate_capture_method(
&self,
capture_method: Option<enums::CaptureMethod>,
_pmt: Option<enums::PaymentMethodType>,
) -> CustomResult<(), errors::ConnectorError> {
let capture_method = capture_method.unwrap_or_default();
match capture_method {
enums::CaptureMethod::Automatic | enums::CaptureMethod::Manual => Ok(()),
enums::CaptureMethod::ManualMultiple | enums::CaptureMethod::Scheduled => Err(
connector_utils::construct_not_implemented_error_report(capture_method, self.id()),
),
}
}
fn validate_mandate_payment(
&self,
pm_type: Option<types::storage::enums::PaymentMethodType>,
pm_data: domain::PaymentMethodData,
) -> CustomResult<(), errors::ConnectorError> {
let mandate_supported_pmd = std::collections::HashSet::from([
PaymentMethodDataType::Card,
PaymentMethodDataType::PaypalRedirect,
PaymentMethodDataType::PaypalSdk,
]);
connector_utils::is_mandate_supported(pm_data, pm_type, mandate_supported_pmd, self.id())
}
}
impl
ConnectorIntegration<
api::PaymentMethodToken,
types::PaymentMethodTokenizationData,
types::PaymentsResponseData,
> for Paypal
{
}
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
for Paypal
{
}
impl ConnectorIntegration<api::AccessTokenAuth, types::AccessTokenRequestData, types::AccessToken>
for Paypal
{
fn get_url(
&self,
_req: &types::RefreshTokenRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!("{}v1/oauth2/token", self.base_url(connectors)))
}
fn get_content_type(&self) -> &'static str {
"application/x-www-form-urlencoded"
}
fn get_headers(
&self,
req: &types::RefreshTokenRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
let auth = paypal::PaypalAuthType::try_from(&req.connector_auth_type)?;
let credentials = auth.get_credentials()?;
let auth_val = credentials.generate_authorization_value();
Ok(vec![
(
headers::CONTENT_TYPE.to_string(),
types::RefreshTokenType::get_content_type(self)
.to_string()
.into(),
),
(headers::AUTHORIZATION.to_string(), auth_val.into_masked()),
])
}
fn get_request_body(
&self,
req: &types::RefreshTokenRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = paypal::PaypalAuthUpdateRequest::try_from(req)?;
Ok(RequestContent::FormUrlEncoded(Box::new(connector_req)))
}
fn build_request(
&self,
req: &types::RefreshTokenRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let req = Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.headers(types::RefreshTokenType::get_headers(self, req, connectors)?)
.url(&types::RefreshTokenType::get_url(self, req, connectors)?)
.set_body(types::RefreshTokenType::get_request_body(
self, req, connectors,
)?)
.build(),
);
Ok(req)
}
fn handle_response(
&self,
data: &types::RefreshTokenRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::RefreshTokenRouterData, errors::ConnectorError> {
let response: paypal::PaypalAuthUpdateResponse = res
.response
.parse_struct("Paypal PaypalAuthUpdateResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
let response: paypal::PaypalAccessTokenErrorResponse = res
.response
.parse_struct("Paypal AccessTokenErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_error_response_body(&response));
router_env::logger::info!(connector_response=?response);
Ok(ErrorResponse {
status_code: res.status_code,
code: response.error.clone(),
message: response.error.clone(),
reason: Some(response.error_description),
attempt_status: None,
connector_transaction_id: None,
})
}
}
#[cfg(feature = "payouts")]
impl ConnectorIntegration<api::PoFulfill, types::PayoutsData, types::PayoutsResponseData>
for Paypal
{
fn get_url(
&self,
_req: &types::PayoutsRouterData<api::PoFulfill>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!("{}v1/payments/payouts", self.base_url(connectors)))
}
fn get_headers(
&self,
req: &types::PayoutsRouterData<api::PoFulfill>,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn get_request_body(
&self,
req: &types::PayoutsRouterData<api::PoFulfill>,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let amount = connector_utils::convert_amount(
self.amount_converter,
req.request.minor_amount,
req.request.destination_currency,
)?;
let connector_router_data =
paypal::PaypalRouterData::try_from((amount, None, None, None, req))?;
let connector_req = paypal::PaypalFulfillRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
fn build_request(
&self,
req: &types::PayoutsRouterData<api::PoFulfill>,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PayoutFulfillType::get_url(self, req, connectors)?)
.attach_default_headers()
.headers(types::PayoutFulfillType::get_headers(
self, req, connectors,
)?)
.set_body(types::PayoutFulfillType::get_request_body(
self, req, connectors,
)?)
.build();
Ok(Some(request))
}
#[instrument(skip_all)]
fn handle_response(
&self,
data: &types::PayoutsRouterData<api::PoFulfill>,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::PayoutsRouterData<api::PoFulfill>, errors::ConnectorError> {
let response: paypal::PaypalFulfillResponse = res
.response
.parse_struct("PaypalFulfillResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
#[cfg(feature = "payouts")]
impl ConnectorIntegration<api::PoSync, types::PayoutsData, types::PayoutsResponseData> for Paypal {
fn get_url(
&self,
req: &types::PayoutsRouterData<api::PoSync>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let batch_id = req.request.connector_payout_id.clone().ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "connector_payout_id",
},
)?;
Ok(format!(
"{}v1/payments/payouts/{}",
self.base_url(connectors),
batch_id
))
}
fn get_headers(
&self,
req: &types::PayoutsRouterData<api::PoSync>,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}
fn build_request(
&self,
req: &types::PayoutsRouterData<api::PoSync>,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Get)
.url(&types::PayoutSyncType::get_url(self, req, connectors)?)
.attach_default_headers()
.headers(types::PayoutSyncType::get_headers(self, req, connectors)?)
.build();
Ok(Some(request))
}
#[instrument(skip_all)]
fn handle_response(
&self,
data: &types::PayoutsRouterData<api::PoSync>,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::PayoutsRouterData<api::PoSync>, errors::ConnectorError> {
let response: paypal::PaypalFulfillResponse = res
.response
.parse_struct("PaypalFulfillResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
#[async_trait::async_trait]
#[cfg(feature = "payouts")]
impl ConnectorIntegration<api::PoCreate, types::PayoutsData, types::PayoutsResponseData>
for Paypal
{
fn build_request(
&self,
_req: &types::PayoutsRouterData<api::PoCreate>,
_connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
// Eligibility check for wallet is not implemented
Err(
errors::ConnectorError::NotImplemented("Payout Eligibility for Paypal".to_string())
.into(),
)
}
}
impl
ConnectorIntegration<
api::SetupMandate,
types::SetupMandateRequestData,
types::PaymentsResponseData,
> for Paypal
{
fn get_headers(
&self,
req: &types::SetupMandateRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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::SetupMandateRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}v3/vault/payment-tokens/",
self.base_url(connectors)
))
}
fn get_request_body(
&self,
req: &types::SetupMandateRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = paypal::PaypalZeroMandateRequest::try_from(req)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
fn build_request(
&self,
req: &types::SetupMandateRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<common_utils::request::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::SetupMandateType::get_url(self, req, connectors)?)
.attach_default_headers()
.headers(types::SetupMandateType::get_headers(self, req, connectors)?)
.set_body(types::SetupMandateType::get_request_body(
self, req, connectors,
)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::SetupMandateRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::SetupMandateRouterData, errors::ConnectorError> {
let response: paypal::PaypalSetupMandatesResponse = res
.response
.parse_struct("PaypalSetupMandatesResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl
ConnectorIntegration<
api::PostSessionTokens,
types::PaymentsPostSessionTokensData,
types::PaymentsResponseData,
> for Paypal
{
fn get_headers(
&self,
req: &types::PaymentsPostSessionTokensRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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::PaymentsPostSessionTokensRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!("{}v2/checkout/orders", self.base_url(connectors)))
}
fn build_request(
&self,
req: &types::PaymentsPostSessionTokensRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsPostSessionTokensType::get_url(
self, req, connectors,
)?)
.attach_default_headers()
.headers(types::PaymentsPostSessionTokensType::get_headers(
self, req, connectors,
)?)
.set_body(types::PaymentsPostSessionTokensType::get_request_body(
self, req, connectors,
)?)
.build(),
))
}
fn get_request_body(
&self,
req: &types::PaymentsPostSessionTokensRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let amount = connector_utils::convert_amount(
self.amount_converter,
req.request.amount,
req.request.currency,
)?;
let shipping_cost = connector_utils::convert_amount(
self.amount_converter,
req.request.shipping_cost.unwrap_or(MinorUnit::zero()),
req.request.currency,
)?;
let order_amount = connector_utils::convert_amount(
self.amount_converter,
req.request.order_amount,
req.request.currency,
)?;
let connector_router_data = paypal::PaypalRouterData::try_from((
amount,
Some(shipping_cost),
None,
Some(order_amount),
req,
))?;
let connector_req = paypal::PaypalPaymentsRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
fn handle_response(
&self,
data: &types::PaymentsPostSessionTokensRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::PaymentsPostSessionTokensRouterData, errors::ConnectorError> {
let response: paypal::PaypalRedirectResponse = res
.response
.parse_struct("PaypalRedirectResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.get_order_error_response(res, event_builder)
}
}
impl
ConnectorIntegration<
api::SdkSessionUpdate,
types::SdkPaymentsSessionUpdateData,
types::PaymentsResponseData,
> for Paypal
{
fn get_headers(
&self,
req: &types::SdkSessionUpdateRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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::SdkSessionUpdateRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let session_id =
req.request
.session_id
.clone()
.ok_or(errors::ConnectorError::MissingRequiredField {
field_name: "session_id",
})?;
Ok(format!(
"{}v2/checkout/orders/{}",
self.base_url(connectors),
session_id
))
}
fn build_request(
&self,
req: &types::SdkSessionUpdateRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Patch)
.url(&types::SdkSessionUpdateType::get_url(
self, req, connectors,
)?)
.attach_default_headers()
.headers(types::SdkSessionUpdateType::get_headers(
self, req, connectors,
)?)
.set_body(types::SdkSessionUpdateType::get_request_body(
self, req, connectors,
)?)
.build(),
))
}
fn get_request_body(
&self,
req: &types::SdkSessionUpdateRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let order_amount = connector_utils::convert_amount(
self.amount_converter,
req.request.order_amount,
req.request.currency,
)?;
let amount = connector_utils::convert_amount(
self.amount_converter,
req.request.amount,
req.request.currency,
)?;
let order_tax_amount = connector_utils::convert_amount(
self.amount_converter,
req.request.order_tax_amount,
req.request.currency,
)?;
let shipping_cost = connector_utils::convert_amount(
self.amount_converter,
req.request.shipping_cost.unwrap_or(MinorUnit::zero()),
req.request.currency,
)?;
let connector_router_data = paypal::PaypalRouterData::try_from((
amount,
Some(shipping_cost),
Some(order_tax_amount),
Some(order_amount),
req,
))?;
let connector_req = paypal::PaypalUpdateOrderRequest::try_from(&connector_router_data)?;
// encode only for for urlencoded things.
Ok(RequestContent::Json(Box::new(
connector_req.get_inner_value(),
)))
}
fn handle_response(
&self,
data: &types::SdkSessionUpdateRouterData,
_event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::SdkSessionUpdateRouterData, errors::ConnectorError> {
logger::debug!("Expected zero bytes response, skipped parsing of the response");
// https://developer.paypal.com/docs/api/orders/v2/#orders_patch
// If 204 status code, then the session was updated successfully.
let status = if res.status_code == 204 {
enums::SessionUpdateStatus::Success
} else {
enums::SessionUpdateStatus::Failure
};
Ok(types::SdkSessionUpdateRouterData {
response: Ok(types::PaymentsResponseData::SessionUpdateResponse { status }),
..data.clone()
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
for Paypal
{
fn get_headers(
&self,
req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
match &req.request.payment_method_data {
domain::PaymentMethodData::Wallet(domain::WalletData::PaypalSdk(
paypal_wallet_data,
)) => {
let authorize_url = if req.request.is_auto_capture()? {
"capture".to_string()
} else {
"authorize".to_string()
};
Ok(format!(
"{}v2/checkout/orders/{}/{authorize_url}",
self.base_url(connectors),
paypal_wallet_data.token
))
}
_ => Ok(format!("{}v2/checkout/orders", self.base_url(connectors))),
}
}
fn get_request_body(
&self,
req: &types::PaymentsAuthorizeRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let amount = connector_utils::convert_amount(
self.amount_converter,
req.request.minor_amount,
req.request.currency,
)?;
let shipping_cost = connector_utils::convert_amount(
self.amount_converter,
req.request.shipping_cost.unwrap_or(MinorUnit::zero()),
req.request.currency,
)?;
let connector_router_data =
paypal::PaypalRouterData::try_from((amount, Some(shipping_cost), None, None, req))?;
let connector_req = paypal::PaypalPaymentsRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
fn build_request(
&self,
req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let payment_method_data = req.request.payment_method_data.clone();
let req = match payment_method_data {
domain::PaymentMethodData::Wallet(domain::WalletData::PaypalSdk(_)) => {
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsAuthorizeType::get_url(
self, req, connectors,
)?)
.headers(types::PaymentsAuthorizeType::get_headers(
self, req, connectors,
)?)
.build()
}
_ => services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsAuthorizeType::get_url(
self, req, connectors,
)?)
.attach_default_headers()
.headers(types::PaymentsAuthorizeType::get_headers(
self, req, connectors,
)?)
.set_body(types::PaymentsAuthorizeType::get_request_body(
self, req, connectors,
)?)
.build(),
};
Ok(Some(req))
}
fn handle_response(
&self,
data: &types::PaymentsAuthorizeRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
let response: PaypalAuthResponse =
res.response
.parse_struct("paypal PaypalAuthResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
match response {
PaypalAuthResponse::PaypalOrdersResponse(response) => {
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
PaypalAuthResponse::PaypalRedirectResponse(response) => {
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
PaypalAuthResponse::PaypalThreeDsResponse(response) => {
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
}
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.get_order_error_response(res, event_builder)
}
}
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,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::PaymentsPreProcessingRouterData, errors::ConnectorError> {
let response: paypal::PaypalPreProcessingResponse = res
.response
.parse_struct("paypal PaypalPreProcessingResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
match response {
// if card supports 3DS check for liability
paypal::PaypalPreProcessingResponse::PaypalLiabilityResponse(liability_response) => {
// permutation for status to continue payment
match (
liability_response
.payment_source
.card
.authentication_result
.three_d_secure
.enrollment_status
.as_ref(),
liability_response
.payment_source
.card
.authentication_result
.three_d_secure
.authentication_status
.as_ref(),
liability_response
.payment_source
.card
.authentication_result
.liability_shift
.clone(),
) {
(
Some(paypal::EnrollmentStatus::Ready),
Some(paypal::AuthenticationStatus::Success),
paypal::LiabilityShift::Possible,
)
| (
Some(paypal::EnrollmentStatus::Ready),
Some(paypal::AuthenticationStatus::Attempted),
paypal::LiabilityShift::Possible,
)
| (Some(paypal::EnrollmentStatus::NotReady), None, paypal::LiabilityShift::No)
| (Some(paypal::EnrollmentStatus::Unavailable), None, paypal::LiabilityShift::No)
| (Some(paypal::EnrollmentStatus::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: Box::new(None),
mandate_reference: Box::new(None),
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
incremental_authorization_allowed: None,
charge_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,
liability_response
.payment_source
.card
.authentication_result
.liability_shift,
liability_response
.payment_source
.card
.authentication_result
.three_d_secure
.enrollment_status
.unwrap_or(paypal::EnrollmentStatus::Null),
liability_response
.payment_source
.card
.authentication_result
.three_d_secure
.authentication_status
.unwrap_or(paypal::AuthenticationStatus::Null),
)),
status_code: res.status_code,
}),
..data.clone()
}),
}
}
// if card does not supports 3DS check for liability
paypal::PaypalPreProcessingResponse::PaypalNonLiabilityResponse(_) => {
Ok(types::PaymentsPreProcessingRouterData {
status: storage_enums::AttemptStatus::AuthenticationSuccessful,
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::NoResponseId,
redirection_data: Box::new(None),
mandate_reference: Box::new(None),
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: None,
incremental_authorization_allowed: None,
charge_id: None,
}),
..data.clone()
})
}
}
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl
ConnectorIntegration<
CompleteAuthorize,
types::CompleteAuthorizeData,
types::PaymentsResponseData,
> for Paypal
{
fn get_headers(
&self,
req: &types::PaymentsCompleteAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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<String, errors::ConnectorError> {
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}",
self.base_url(connectors),
req.request
.connector_transaction_id
.clone()
.ok_or(errors::ConnectorError::MissingConnectorTransactionID)?
))
}
fn build_request(
&self,
req: &types::PaymentsCompleteAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, 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,
)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCompleteAuthorizeRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::PaymentsCompleteAuthorizeRouterData, errors::ConnectorError> {
let response: paypal::PaypalOrdersResponse = res
.response
.parse_struct("paypal PaypalOrdersResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for Paypal
{
fn get_headers(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let paypal_meta: PaypalMeta = to_connector_meta(req.request.connector_meta.clone())?;
match req.payment_method {
enums::PaymentMethod::Wallet | enums::PaymentMethod::BankRedirect => Ok(format!(
"{}v2/checkout/orders/{}",
self.base_url(connectors),
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 => {
let authorize_id = paypal_meta.authorize_id.ok_or(
errors::ConnectorError::RequestEncodingFailedWithReason(
"Missing Authorize id".to_string(),
),
)?;
format!("v2/payments/authorizations/{authorize_id}",)
}
transformers::PaypalPaymentIntent::Capture => {
let capture_id = paypal_meta.capture_id.ok_or(
errors::ConnectorError::RequestEncodingFailedWithReason(
"Missing Capture id".to_string(),
),
)?;
format!("v2/payments/captures/{capture_id}")
}
// only set when payment is done through card 3DS
//because no authorize or capture id is generated during payment authorize call for card 3DS
transformers::PaypalPaymentIntent::Authenticate => {
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)))
}
}
}
fn build_request(
&self,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Get)
.url(&types::PaymentsSyncType::get_url(self, req, connectors)?)
.headers(types::PaymentsSyncType::get_headers(self, req, connectors)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsSyncRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
let response: paypal::PaypalSyncResponse = res
.response
.parse_struct("paypal SyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::foreign_try_from((
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
},
data.request.payment_experience,
))
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
for Paypal
{
fn get_headers(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let paypal_meta: PaypalMeta = to_connector_meta(req.request.connector_meta.clone())?;
let authorize_id = paypal_meta.authorize_id.ok_or(
errors::ConnectorError::RequestEncodingFailedWithReason(
"Missing Authorize id".to_string(),
),
)?;
Ok(format!(
"{}v2/payments/authorizations/{}/capture",
self.base_url(connectors),
authorize_id
))
}
fn get_request_body(
&self,
req: &types::PaymentsCaptureRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let amount_to_capture = connector_utils::convert_amount(
self.amount_converter,
req.request.minor_amount_to_capture,
req.request.currency,
)?;
let connector_router_data =
paypal::PaypalRouterData::try_from((amount_to_capture, None, None, None, req))?;
let connector_req = paypal::PaypalPaymentsCaptureRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
fn build_request(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsCaptureType::get_url(self, req, connectors)?)
.headers(types::PaymentsCaptureType::get_headers(
self, req, connectors,
)?)
.set_body(types::PaymentsCaptureType::get_request_body(
self, req, connectors,
)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCaptureRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
let response: paypal::PaypalCaptureResponse = res
.response
.parse_struct("Paypal PaymentsCaptureResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
for Paypal
{
fn get_headers(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let paypal_meta: PaypalMeta = to_connector_meta(req.request.connector_meta.clone())?;
let authorize_id = paypal_meta.authorize_id.ok_or(
errors::ConnectorError::RequestEncodingFailedWithReason(
"Missing Authorize id".to_string(),
),
)?;
Ok(format!(
"{}v2/payments/authorizations/{}/void",
self.base_url(connectors),
authorize_id,
))
}
fn build_request(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsVoidType::get_url(self, req, connectors)?)
.headers(types::PaymentsVoidType::get_headers(self, req, connectors)?)
.build();
Ok(Some(request))
}
fn handle_response(
&self,
data: &types::PaymentsCancelRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::PaymentsCancelRouterData, errors::ConnectorError> {
let response: paypal::PaypalPaymentsCancelResponse = res
.response
.parse_struct("PaymentCancelResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData> for Paypal {
fn get_headers(
&self,
req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let paypal_meta: PaypalMeta = to_connector_meta(req.request.connector_metadata.clone())?;
let capture_id = paypal_meta.capture_id.ok_or(
errors::ConnectorError::RequestEncodingFailedWithReason(
"Missing Capture id".to_string(),
),
)?;
Ok(format!(
"{}v2/payments/captures/{}/refund",
self.base_url(connectors),
capture_id,
))
}
fn get_request_body(
&self,
req: &types::RefundsRouterData<api::Execute>,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let amount = connector_utils::convert_amount(
self.amount_converter,
req.request.minor_refund_amount,
req.request.currency,
)?;
let connector_router_data =
paypal::PaypalRouterData::try_from((amount, None, None, None, req))?;
let connector_req = paypal::PaypalRefundRequest::try_from(&connector_router_data)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
fn build_request(
&self,
req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::RefundExecuteType::get_url(self, req, connectors)?)
.headers(types::RefundExecuteType::get_headers(
self, req, connectors,
)?)
.set_body(types::RefundExecuteType::get_request_body(
self, req, connectors,
)?)
.build();
Ok(Some(request))
}
fn handle_response(
&self,
data: &types::RefundsRouterData<api::Execute>,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::RefundsRouterData<api::Execute>, errors::ConnectorError> {
let response: paypal::RefundResponse =
res.response
.parse_struct("paypal RefundResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData> for Paypal {
fn get_headers(
&self,
req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}v2/payments/refunds/{}",
self.base_url(connectors),
req.request.get_connector_refund_id()?
))
}
fn build_request(
&self,
req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Get)
.url(&types::RefundSyncType::get_url(self, req, connectors)?)
.headers(types::RefundSyncType::get_headers(self, req, connectors)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::RefundSyncRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
let response: paypal::RefundSyncResponse = res
.response
.parse_struct("paypal RefundSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl
ConnectorIntegration<
VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
> for Paypal
{
fn get_headers(
&self,
req: &types::RouterData<
VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
>,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
let auth = paypal::PaypalAuthType::try_from(&req.connector_auth_type)?;
let credentials = auth.get_credentials()?;
let auth_val = credentials.generate_authorization_value();
Ok(vec![
(
headers::CONTENT_TYPE.to_string(),
types::VerifyWebhookSourceType::get_content_type(self)
.to_string()
.into(),
),
(headers::AUTHORIZATION.to_string(), auth_val.into_masked()),
])
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
_req: &types::RouterData<
VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}v1/notifications/verify-webhook-signature",
self.base_url(connectors)
))
}
fn build_request(
&self,
req: &types::VerifyWebhookSourceRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::VerifyWebhookSourceType::get_url(
self, req, connectors,
)?)
.headers(types::VerifyWebhookSourceType::get_headers(
self, req, connectors,
)?)
.set_body(types::VerifyWebhookSourceType::get_request_body(
self, req, connectors,
)?)
.build();
Ok(Some(request))
}
fn get_request_body(
&self,
req: &types::RouterData<
VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
>,
_connectors: &settings::Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = paypal::PaypalSourceVerificationRequest::try_from(&req.request)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}
fn handle_response(
&self,
data: &types::VerifyWebhookSourceRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<types::VerifyWebhookSourceRouterData, errors::ConnectorError> {
let response: paypal::PaypalSourceVerificationResponse = res
.response
.parse_struct("paypal PaypalSourceVerificationResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
#[async_trait::async_trait]
impl api::IncomingWebhook for Paypal {
fn get_webhook_object_reference_id(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
let payload: paypal::PaypalWebhooksBody =
request
.body
.parse_struct("PaypalWebhooksBody")
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
match payload.resource {
paypal::PaypalResource::PaypalCardWebhooks(resource) => {
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
api_models::payments::PaymentIdType::ConnectorTransactionId(
resource.supplementary_data.related_ids.order_id,
),
))
}
paypal::PaypalResource::PaypalRedirectsWebhooks(resource) => {
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
api_models::payments::PaymentIdType::PaymentAttemptId(
resource
.purchase_units
.first()
.and_then(|unit| unit.invoice_id.clone().or(unit.reference_id.clone()))
.ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?,
),
))
}
paypal::PaypalResource::PaypalRefundWebhooks(resource) => {
Ok(api_models::webhooks::ObjectReferenceId::RefundId(
api_models::webhooks::RefundIdType::ConnectorRefundId(resource.id),
))
}
paypal::PaypalResource::PaypalDisputeWebhooks(resource) => {
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
api_models::payments::PaymentIdType::ConnectorTransactionId(
resource
.disputed_transactions
.first()
.map(|transaction| transaction.seller_transaction_id.clone())
.ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?,
),
))
}
}
}
fn get_webhook_event_type(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
let payload: paypal::PaypalWebooksEventType = request
.body
.parse_struct("PaypalWebooksEventType")
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
let outcome = match payload.event_type {
PaypalWebhookEventType::CustomerDisputeResolved => Some(
request
.body
.parse_struct::<paypal::DisputeOutcome>("PaypalWebooksEventType")
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?
.outcome_code,
),
PaypalWebhookEventType::CustomerDisputeCreated
| PaypalWebhookEventType::RiskDisputeCreated
| PaypalWebhookEventType::CustomerDisputedUpdated
| PaypalWebhookEventType::PaymentAuthorizationCreated
| PaypalWebhookEventType::PaymentAuthorizationVoided
| PaypalWebhookEventType::PaymentCaptureDeclined
| PaypalWebhookEventType::PaymentCaptureCompleted
| PaypalWebhookEventType::PaymentCapturePending
| PaypalWebhookEventType::PaymentCaptureRefunded
| PaypalWebhookEventType::CheckoutOrderApproved
| PaypalWebhookEventType::CheckoutOrderCompleted
| PaypalWebhookEventType::CheckoutOrderProcessed
| PaypalWebhookEventType::Unknown => None,
};
Ok(api::IncomingWebhookEvent::foreign_from((
payload.event_type,
outcome,
)))
}
fn get_webhook_resource_object(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Box<dyn masking::ErasedMaskSerialize>, errors::ConnectorError> {
let details: paypal::PaypalWebhooksBody =
request
.body
.parse_struct("PaypalWebhooksBody")
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
Ok(match details.resource {
paypal::PaypalResource::PaypalCardWebhooks(resource) => Box::new(
paypal::PaypalPaymentsSyncResponse::try_from((*resource, details.event_type))?,
),
paypal::PaypalResource::PaypalRedirectsWebhooks(resource) => Box::new(
paypal::PaypalOrdersResponse::try_from((*resource, details.event_type))?,
),
paypal::PaypalResource::PaypalRefundWebhooks(resource) => Box::new(
paypal::RefundSyncResponse::try_from((*resource, details.event_type))?,
),
paypal::PaypalResource::PaypalDisputeWebhooks(_) => Box::new(details),
})
}
fn get_dispute_details(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api::disputes::DisputePayload, errors::ConnectorError> {
let webhook_payload: paypal::PaypalWebhooksBody = request
.body
.parse_struct("PaypalWebhooksBody")
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
match webhook_payload.resource {
transformers::PaypalResource::PaypalCardWebhooks(_)
| transformers::PaypalResource::PaypalRedirectsWebhooks(_)
| transformers::PaypalResource::PaypalRefundWebhooks(_) => {
Err(errors::ConnectorError::ResponseDeserializationFailed)
.attach_printable("Expected Dispute webhooks,but found other webhooks")?
}
transformers::PaypalResource::PaypalDisputeWebhooks(payload) => {
Ok(api::disputes::DisputePayload {
amount: connector_utils::to_currency_lower_unit(
payload.dispute_amount.value.get_amount_as_string(),
payload.dispute_amount.currency_code,
)?,
currency: payload.dispute_amount.currency_code,
dispute_stage: api_models::enums::DisputeStage::from(
payload.dispute_life_cycle_stage.clone(),
),
connector_status: payload.status.to_string(),
connector_dispute_id: payload.dispute_id,
connector_reason: payload.reason.clone(),
connector_reason_code: payload.reason,
challenge_required_by: None,
created_at: payload.create_time,
updated_at: payload.update_time,
})
}
}
}
}
impl services::ConnectorRedirectResponse for Paypal {
fn get_flow_type(
&self,
_query_params: &str,
_json_payload: Option<serde_json::Value>,
action: PaymentAction,
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
match action {
PaymentAction::PSync
| PaymentAction::CompleteAuthorize
| PaymentAction::PaymentAuthenticateCompleteAuthorize => {
Ok(payments::CallConnectorAction::Trigger)
}
}
}
}
impl ConnectorErrorTypeMapping for Paypal {
fn get_connector_error_type(
&self,
error_code: String,
_error_message: String,
) -> ConnectorErrorType {
match error_code.as_str() {
"CANNOT_BE_NEGATIVE" => ConnectorErrorType::UserError,
"CANNOT_BE_ZERO_OR_NEGATIVE" => ConnectorErrorType::UserError,
"CARD_EXPIRED" => ConnectorErrorType::UserError,
"DECIMAL_PRECISION" => ConnectorErrorType::UserError,
"DUPLICATE_INVOICE_ID" => ConnectorErrorType::UserError,
"INSTRUMENT_DECLINED" => ConnectorErrorType::BusinessError,
"INTERNAL_SERVER_ERROR" => ConnectorErrorType::TechnicalError,
"INVALID_ACCOUNT_STATUS" => ConnectorErrorType::BusinessError,
"INVALID_CURRENCY_CODE" => ConnectorErrorType::UserError,
"INVALID_PARAMETER_SYNTAX" => ConnectorErrorType::UserError,
"INVALID_PARAMETER_VALUE" => ConnectorErrorType::UserError,
"INVALID_RESOURCE_ID" => ConnectorErrorType::UserError,
"INVALID_STRING_LENGTH" => ConnectorErrorType::UserError,
"MISSING_REQUIRED_PARAMETER" => ConnectorErrorType::UserError,
"PAYER_ACCOUNT_LOCKED_OR_CLOSED" => ConnectorErrorType::BusinessError,
"PAYER_ACCOUNT_RESTRICTED" => ConnectorErrorType::BusinessError,
"PAYER_CANNOT_PAY" => ConnectorErrorType::BusinessError,
"PERMISSION_DENIED" => ConnectorErrorType::BusinessError,
"INVALID_ARRAY_MAX_ITEMS" => ConnectorErrorType::UserError,
"INVALID_ARRAY_MIN_ITEMS" => ConnectorErrorType::UserError,
"INVALID_COUNTRY_CODE" => ConnectorErrorType::UserError,
"NOT_SUPPORTED" => ConnectorErrorType::BusinessError,
"PAYPAL_REQUEST_ID_REQUIRED" => ConnectorErrorType::UserError,
"MALFORMED_REQUEST_JSON" => ConnectorErrorType::UserError,
"PERMISSION_DENIED_FOR_DONATION_ITEMS" => ConnectorErrorType::BusinessError,
"MALFORMED_REQUEST" => ConnectorErrorType::TechnicalError,
"AMOUNT_MISMATCH" => ConnectorErrorType::UserError,
"BILLING_ADDRESS_INVALID" => ConnectorErrorType::UserError,
"CITY_REQUIRED" => ConnectorErrorType::UserError,
"DONATION_ITEMS_NOT_SUPPORTED" => ConnectorErrorType::BusinessError,
"DUPLICATE_REFERENCE_ID" => ConnectorErrorType::UserError,
"INVALID_PAYER_ID" => ConnectorErrorType::UserError,
"ITEM_TOTAL_REQUIRED" => ConnectorErrorType::UserError,
"MAX_VALUE_EXCEEDED" => ConnectorErrorType::UserError,
"MISSING_PICKUP_ADDRESS" => ConnectorErrorType::UserError,
"MULTI_CURRENCY_ORDER" => ConnectorErrorType::BusinessError,
"MULTIPLE_ITEM_CATEGORIES" => ConnectorErrorType::UserError,
"MULTIPLE_SHIPPING_ADDRESS_NOT_SUPPORTED" => ConnectorErrorType::UserError,
"MULTIPLE_SHIPPING_TYPE_NOT_SUPPORTED" => ConnectorErrorType::BusinessError,
"PAYEE_ACCOUNT_INVALID" => ConnectorErrorType::UserError,
"PAYEE_ACCOUNT_LOCKED_OR_CLOSED" => ConnectorErrorType::UserError,
"REFERENCE_ID_REQUIRED" => ConnectorErrorType::UserError,
"PAYMENT_SOURCE_CANNOT_BE_USED" => ConnectorErrorType::BusinessError,
"PAYMENT_SOURCE_DECLINED_BY_PROCESSOR" => ConnectorErrorType::BusinessError,
"PAYMENT_SOURCE_INFO_CANNOT_BE_VERIFIED" => ConnectorErrorType::BusinessError,
"POSTAL_CODE_REQUIRED" => ConnectorErrorType::UserError,
"SHIPPING_ADDRESS_INVALID" => ConnectorErrorType::UserError,
"TAX_TOTAL_MISMATCH" => ConnectorErrorType::UserError,
"TAX_TOTAL_REQUIRED" => ConnectorErrorType::UserError,
"UNSUPPORTED_INTENT" => ConnectorErrorType::BusinessError,
"UNSUPPORTED_PAYMENT_INSTRUCTION" => ConnectorErrorType::UserError,
"SHIPPING_TYPE_NOT_SUPPORTED_FOR_CLIENT" => ConnectorErrorType::BusinessError,
"UNSUPPORTED_SHIPPING_TYPE" => ConnectorErrorType::BusinessError,
"PREFERRED_SHIPPING_OPTION_AMOUNT_MISMATCH" => ConnectorErrorType::UserError,
"CARD_CLOSED" => ConnectorErrorType::BusinessError,
"ORDER_CANNOT_BE_SAVED" => ConnectorErrorType::BusinessError,
"SAVE_ORDER_NOT_SUPPORTED" => ConnectorErrorType::BusinessError,
"FIELD_NOT_PATCHABLE" => ConnectorErrorType::UserError,
"AMOUNT_NOT_PATCHABLE" => ConnectorErrorType::UserError,
"INVALID_PATCH_OPERATION" => ConnectorErrorType::UserError,
"PAYEE_ACCOUNT_NOT_SUPPORTED" => ConnectorErrorType::UserError,
"PAYEE_ACCOUNT_NOT_VERIFIED" => ConnectorErrorType::UserError,
"PAYEE_NOT_CONSENTED" => ConnectorErrorType::UserError,
"INVALID_JSON_POINTER_FORMAT" => ConnectorErrorType::BusinessError,
"INVALID_PARAMETER" => ConnectorErrorType::UserError,
"NOT_PATCHABLE" => ConnectorErrorType::BusinessError,
"PATCH_VALUE_REQUIRED" => ConnectorErrorType::UserError,
"PATCH_PATH_REQUIRED" => ConnectorErrorType::UserError,
"REFERENCE_ID_NOT_FOUND" => ConnectorErrorType::UserError,
"SHIPPING_OPTION_NOT_SELECTED" => ConnectorErrorType::UserError,
"SHIPPING_OPTIONS_NOT_SUPPORTED" => ConnectorErrorType::BusinessError,
"MULTIPLE_SHIPPING_OPTION_SELECTED" => ConnectorErrorType::UserError,
"ORDER_ALREADY_COMPLETED" => ConnectorErrorType::BusinessError,
"ACTION_DOES_NOT_MATCH_INTENT" => ConnectorErrorType::BusinessError,
"AGREEMENT_ALREADY_CANCELLED" => ConnectorErrorType::BusinessError,
"BILLING_AGREEMENT_NOT_FOUND" => ConnectorErrorType::BusinessError,
"DOMESTIC_TRANSACTION_REQUIRED" => ConnectorErrorType::BusinessError,
"ORDER_NOT_APPROVED" => ConnectorErrorType::UserError,
"MAX_NUMBER_OF_PAYMENT_ATTEMPTS_EXCEEDED" => ConnectorErrorType::TechnicalError,
"PAYEE_BLOCKED_TRANSACTION" => ConnectorErrorType::BusinessError,
"TRANSACTION_LIMIT_EXCEEDED" => ConnectorErrorType::UserError,
"TRANSACTION_RECEIVING_LIMIT_EXCEEDED" => ConnectorErrorType::BusinessError,
"TRANSACTION_REFUSED" => ConnectorErrorType::TechnicalError,
"ORDER_ALREADY_AUTHORIZED" => ConnectorErrorType::BusinessError,
"AUTH_CAPTURE_NOT_ENABLED" => ConnectorErrorType::BusinessError,
"AMOUNT_CANNOT_BE_SPECIFIED" => ConnectorErrorType::BusinessError,
"AUTHORIZATION_AMOUNT_EXCEEDED" => ConnectorErrorType::UserError,
"AUTHORIZATION_CURRENCY_MISMATCH" => ConnectorErrorType::UserError,
"MAX_AUTHORIZATION_COUNT_EXCEEDED" => ConnectorErrorType::BusinessError,
"ORDER_COMPLETED_OR_VOIDED" => ConnectorErrorType::BusinessError,
"ORDER_EXPIRED" => ConnectorErrorType::BusinessError,
"INVALID_PICKUP_ADDRESS" => ConnectorErrorType::UserError,
"CONSENT_NEEDED" => ConnectorErrorType::UserError,
"COMPLIANCE_VIOLATION" => ConnectorErrorType::BusinessError,
"REDIRECT_PAYER_FOR_ALTERNATE_FUNDING" => ConnectorErrorType::TechnicalError,
"ORDER_ALREADY_CAPTURED" => ConnectorErrorType::UserError,
"TRANSACTION_BLOCKED_BY_PAYEE" => ConnectorErrorType::BusinessError,
"NOT_ENABLED_FOR_CARD_PROCESSING" => ConnectorErrorType::BusinessError,
"PAYEE_NOT_ENABLED_FOR_CARD_PROCESSING" => ConnectorErrorType::BusinessError,
_ => ConnectorErrorType::UnknownError,
}
}
}