mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
feat(connector): add authorize, capture, void, psync, refund, rsync for PayPal connector (#747)
Co-authored-by: Arjun Karthik <m.arjunkarthik@gmail.com> Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
@ -67,6 +67,7 @@ cards = [
|
||||
"mollie",
|
||||
"multisafepay",
|
||||
"nuvei",
|
||||
"paypal",
|
||||
"payu",
|
||||
"shift4",
|
||||
"stripe",
|
||||
@ -106,6 +107,7 @@ klarna.base_url = "https://api-na.playground.klarna.com/"
|
||||
mollie.base_url = "https://api.mollie.com/v2/"
|
||||
multisafepay.base_url = "https://testapi.multisafepay.com/"
|
||||
nuvei.base_url = "https://ppp-test.nuvei.com/"
|
||||
paypal.base_url = "https://www.sandbox.paypal.com/"
|
||||
payu.base_url = "https://secure.snd.payu.com/"
|
||||
rapyd.base_url = "https://sandboxapi.rapyd.net"
|
||||
shift4.base_url = "https://api.shift4.com/"
|
||||
|
||||
@ -141,6 +141,7 @@ klarna.base_url = "https://api-na.playground.klarna.com/"
|
||||
mollie.base_url = "https://api.mollie.com/v2/"
|
||||
multisafepay.base_url = "https://testapi.multisafepay.com/"
|
||||
nuvei.base_url = "https://ppp-test.nuvei.com/"
|
||||
paypal.base_url = "https://www.sandbox.paypal.com/"
|
||||
payu.base_url = "https://secure.snd.payu.com/"
|
||||
rapyd.base_url = "https://sandboxapi.rapyd.net"
|
||||
shift4.base_url = "https://api.shift4.com/"
|
||||
@ -154,16 +155,16 @@ trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/"
|
||||
[connectors.supported]
|
||||
wallets = ["klarna", "braintree", "applepay"]
|
||||
cards = [
|
||||
"stripe",
|
||||
"adyen",
|
||||
"authorizedotnet",
|
||||
"checkout",
|
||||
"braintree",
|
||||
"checkout",
|
||||
"cybersource",
|
||||
"mollie",
|
||||
"paypal",
|
||||
"shift4",
|
||||
"stripe",
|
||||
"worldpay",
|
||||
"globalpay",
|
||||
]
|
||||
|
||||
# Scheduler settings provides a point to modify the behaviour of scheduler flow.
|
||||
|
||||
@ -86,6 +86,7 @@ klarna.base_url = "https://api-na.playground.klarna.com/"
|
||||
mollie.base_url = "https://api.mollie.com/v2/"
|
||||
multisafepay.base_url = "https://testapi.multisafepay.com/"
|
||||
nuvei.base_url = "https://ppp-test.nuvei.com/"
|
||||
paypal.base_url = "https://www.sandbox.paypal.com/"
|
||||
payu.base_url = "https://secure.snd.payu.com/"
|
||||
rapyd.base_url = "https://sandboxapi.rapyd.net"
|
||||
shift4.base_url = "https://api.shift4.com/"
|
||||
@ -95,6 +96,7 @@ worldpay.base_url = "https://try.access.worldpay.com/"
|
||||
trustpay.base_url = "https://test-tpgw.trustpay.eu/"
|
||||
trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/"
|
||||
|
||||
|
||||
[connectors.supported]
|
||||
wallets = ["klarna", "braintree", "applepay"]
|
||||
cards = [
|
||||
@ -113,6 +115,7 @@ cards = [
|
||||
"mollie",
|
||||
"multisafepay",
|
||||
"nuvei",
|
||||
"paypal",
|
||||
"payu",
|
||||
"shift4",
|
||||
"stripe",
|
||||
|
||||
@ -572,6 +572,7 @@ pub enum Connector {
|
||||
Mollie,
|
||||
Multisafepay,
|
||||
Nuvei,
|
||||
Paypal,
|
||||
Payu,
|
||||
Rapyd,
|
||||
Shift4,
|
||||
@ -587,6 +588,7 @@ impl Connector {
|
||||
(self, payment_method),
|
||||
(Self::Airwallex, _)
|
||||
| (Self::Globalpay, _)
|
||||
| (Self::Paypal, _)
|
||||
| (Self::Payu, _)
|
||||
| (Self::Trustpay, PaymentMethod::BankRedirect)
|
||||
)
|
||||
@ -624,6 +626,7 @@ pub enum RoutableConnectors {
|
||||
Mollie,
|
||||
Multisafepay,
|
||||
Nuvei,
|
||||
Paypal,
|
||||
Payu,
|
||||
Rapyd,
|
||||
Shift4,
|
||||
|
||||
@ -263,6 +263,7 @@ pub struct Connectors {
|
||||
pub mollie: ConnectorParams,
|
||||
pub multisafepay: ConnectorParams,
|
||||
pub nuvei: ConnectorParams,
|
||||
pub paypal: ConnectorParams,
|
||||
pub payu: ConnectorParams,
|
||||
pub rapyd: ConnectorParams,
|
||||
pub shift4: ConnectorParams,
|
||||
|
||||
@ -14,6 +14,7 @@ pub mod globalpay;
|
||||
pub mod klarna;
|
||||
pub mod multisafepay;
|
||||
pub mod nuvei;
|
||||
pub mod paypal;
|
||||
pub mod payu;
|
||||
pub mod rapyd;
|
||||
pub mod shift4;
|
||||
@ -30,6 +31,6 @@ pub use self::{
|
||||
authorizedotnet::Authorizedotnet, bambora::Bambora, bluesnap::Bluesnap, braintree::Braintree,
|
||||
checkout::Checkout, cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv,
|
||||
globalpay::Globalpay, klarna::Klarna, mollie::Mollie, multisafepay::Multisafepay, nuvei::Nuvei,
|
||||
payu::Payu, rapyd::Rapyd, shift4::Shift4, stripe::Stripe, trustpay::Trustpay,
|
||||
paypal::Paypal, payu::Payu, rapyd::Rapyd, shift4::Shift4, stripe::Stripe, trustpay::Trustpay,
|
||||
worldline::Worldline, worldpay::Worldpay,
|
||||
};
|
||||
|
||||
781
crates/router/src/connector/paypal.rs
Normal file
781
crates/router/src/connector/paypal.rs
Normal file
@ -0,0 +1,781 @@
|
||||
mod transformers;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use base64::Engine;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use transformers as paypal;
|
||||
|
||||
use self::transformers::PaypalMeta;
|
||||
use crate::{
|
||||
configs::settings,
|
||||
connector::utils::{to_connector_meta, RefundsRequestData},
|
||||
consts,
|
||||
core::{
|
||||
errors::{self, CustomResult},
|
||||
payments,
|
||||
},
|
||||
headers,
|
||||
services::{self, ConnectorIntegration, PaymentAction},
|
||||
types::{
|
||||
self,
|
||||
api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt},
|
||||
ErrorResponse, Response,
|
||||
},
|
||||
utils::{self, BytesExt},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Paypal;
|
||||
|
||||
impl api::Payment for Paypal {}
|
||||
impl api::PaymentSession for Paypal {}
|
||||
impl api::ConnectorAccessToken for Paypal {}
|
||||
impl api::PreVerify 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 Paypal {
|
||||
pub fn connector_transaction_id(
|
||||
&self,
|
||||
connector_meta: &Option<serde_json::Value>,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let meta: PaypalMeta = to_connector_meta(connector_meta.clone())?;
|
||||
Ok(meta.authorize_id)
|
||||
}
|
||||
|
||||
pub fn get_order_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> 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)?;
|
||||
|
||||
let message = match response.details {
|
||||
Some(mes) => {
|
||||
let mut des = "".to_owned();
|
||||
for item in mes.iter() {
|
||||
let mut description = format!("description - {}", item.to_owned().description);
|
||||
|
||||
if let Some(data) = &item.value {
|
||||
description.push_str(format!(", value - {}", data.to_owned()).as_str());
|
||||
}
|
||||
|
||||
if let Some(data) = &item.field {
|
||||
let field = data
|
||||
.clone()
|
||||
.split('/')
|
||||
.last()
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
|
||||
description.push_str(format!(", field - {};", field).as_str());
|
||||
}
|
||||
des.push_str(description.as_str())
|
||||
}
|
||||
des
|
||||
}
|
||||
None => consts::NO_ERROR_MESSAGE.to_string(),
|
||||
};
|
||||
Ok(ErrorResponse {
|
||||
status_code: res.status_code,
|
||||
code: response.name,
|
||||
message,
|
||||
reason: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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, String)>, errors::ConnectorError> {
|
||||
let access_token = req
|
||||
.access_token
|
||||
.clone()
|
||||
.ok_or(errors::ConnectorError::FailedToObtainAuthType)?;
|
||||
let key = &req.attempt_id;
|
||||
|
||||
Ok(vec![
|
||||
(
|
||||
headers::CONTENT_TYPE.to_string(),
|
||||
self.get_content_type().to_string(),
|
||||
),
|
||||
(
|
||||
headers::AUTHORIZATION.to_string(),
|
||||
format!("Bearer {}", access_token.token),
|
||||
),
|
||||
("Prefer".to_string(), "return=representation".to_string()),
|
||||
("PayPal-Request-Id".to_string(), key.to_string()),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorCommon for Paypal {
|
||||
fn id(&self) -> &'static str {
|
||||
"paypal"
|
||||
}
|
||||
|
||||
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: &types::ConnectorAuthType,
|
||||
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||
let auth: paypal::PaypalAuthType = auth_type
|
||||
.try_into()
|
||||
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
|
||||
Ok(vec![(headers::AUTHORIZATION.to_string(), auth.api_key)])
|
||||
}
|
||||
|
||||
fn build_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
let response: paypal::PaypalPaymentErrorResponse = res
|
||||
.response
|
||||
.parse_struct("Paypal ErrorResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
|
||||
let message = match response.details {
|
||||
Some(mes) => {
|
||||
let mut des = "".to_owned();
|
||||
for item in mes.iter() {
|
||||
let x = item.clone().description;
|
||||
let st = format!("description - {} ; ", x);
|
||||
des.push_str(&st);
|
||||
}
|
||||
des
|
||||
}
|
||||
None => consts::NO_ERROR_MESSAGE.to_string(),
|
||||
};
|
||||
|
||||
Ok(ErrorResponse {
|
||||
status_code: res.status_code,
|
||||
code: response.name,
|
||||
message,
|
||||
reason: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
|
||||
for Paypal
|
||||
{
|
||||
//TODO: implement sessions flow
|
||||
}
|
||||
|
||||
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, String)>, errors::ConnectorError> {
|
||||
let auth: paypal::PaypalAuthType = (&req.connector_auth_type)
|
||||
.try_into()
|
||||
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
|
||||
|
||||
let auth_id = format!("{}:{}", auth.key1, auth.api_key);
|
||||
let auth_val = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_id));
|
||||
|
||||
Ok(vec![
|
||||
(
|
||||
headers::CONTENT_TYPE.to_string(),
|
||||
types::RefreshTokenType::get_content_type(self).to_string(),
|
||||
),
|
||||
(headers::AUTHORIZATION.to_string(), auth_val),
|
||||
])
|
||||
}
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::RefreshTokenRouterData,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let paypal_req =
|
||||
utils::Encode::<paypal::PaypalAuthUpdateRequest>::convert_and_url_encode(req)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
|
||||
Ok(Some(paypal_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)?)
|
||||
.body(types::RefreshTokenType::get_request_body(self, req)?)
|
||||
.build(),
|
||||
);
|
||||
|
||||
Ok(req)
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::RefreshTokenRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::RefreshTokenRouterData, errors::ConnectorError> {
|
||||
let response: paypal::PaypalAuthUpdateResponse = res
|
||||
.response
|
||||
.parse_struct("Paypal PaypalAuthUpdateResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
let response: paypal::PaypalAccessTokenErrorResponse = res
|
||||
.response
|
||||
.parse_struct("Paypal AccessTokenErrorResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
|
||||
Ok(ErrorResponse {
|
||||
status_code: res.status_code,
|
||||
code: response.error,
|
||||
message: response.error_description,
|
||||
reason: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
|
||||
for Paypal
|
||||
{
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
|
||||
for Paypal
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, 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> {
|
||||
Ok(format!("{}v2/checkout/orders", self.base_url(connectors),))
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let req_obj = paypal::PaypalPaymentsRequest::try_from(req)?;
|
||||
let paypal_req =
|
||||
utils::Encode::<paypal::PaypalPaymentsRequest>::encode_to_string_of_json(&req_obj)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(paypal_req))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Ok(Some(
|
||||
services::RequestBuilder::new()
|
||||
.method(services::Method::Post)
|
||||
.url(&types::PaymentsAuthorizeType::get_url(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.headers(types::PaymentsAuthorizeType::get_headers(
|
||||
self, req, connectors,
|
||||
)?)
|
||||
.body(types::PaymentsAuthorizeType::get_request_body(self, req)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsAuthorizeRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
|
||||
let response: paypal::PaypalOrdersResponse = res
|
||||
.response
|
||||
.parse_struct("Paypal PaymentsAuthorizeResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.get_order_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl
|
||||
ConnectorIntegration<
|
||||
CompleteAuthorize,
|
||||
types::CompleteAuthorizeData,
|
||||
types::PaymentsResponseData,
|
||||
> for Paypal
|
||||
{
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
|
||||
for Paypal
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, 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 capture_id = req
|
||||
.request
|
||||
.connector_transaction_id
|
||||
.get_connector_transaction_id()
|
||||
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
|
||||
let paypal_meta: PaypalMeta = to_connector_meta(req.request.connector_meta.clone())?;
|
||||
let psync_url = match paypal_meta.psync_flow {
|
||||
transformers::PaypalPaymentIntent::Authorize => format!(
|
||||
"v2/payments/authorizations/{}",
|
||||
paypal_meta.authorize_id.unwrap_or_default()
|
||||
),
|
||||
transformers::PaypalPaymentIntent::Capture => {
|
||||
format!("v2/payments/captures/{}", capture_id)
|
||||
}
|
||||
};
|
||||
Ok(format!("{}{}", self.base_url(connectors), psync_url,))
|
||||
}
|
||||
|
||||
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,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
|
||||
let response: paypal::PaypalPaymentsSyncResponse = res
|
||||
.response
|
||||
.parse_struct("paypal PaymentsSyncResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
|
||||
for Paypal
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsCaptureRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, 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,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let connector_req = paypal::PaypalPaymentsCaptureRequest::try_from(req)?;
|
||||
let paypal_req =
|
||||
utils::Encode::<paypal::PaypalPaymentsCaptureRequest>::encode_to_string_of_json(
|
||||
&connector_req,
|
||||
)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(paypal_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,
|
||||
)?)
|
||||
.build(),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::PaymentsCaptureRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
|
||||
let response: paypal::PaymentCaptureResponse = res
|
||||
.response
|
||||
.parse_struct("Paypal PaymentsCaptureResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
|
||||
for Paypal
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsCancelRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, 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,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsCancelRouterData, errors::ConnectorError> {
|
||||
let response: paypal::PaypalPaymentsCancelResponse = res
|
||||
.response
|
||||
.parse_struct("PaymentCancelResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
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, 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 id = req.request.connector_transaction_id.clone();
|
||||
Ok(format!(
|
||||
"{}v2/payments/captures/{}/refund",
|
||||
self.base_url(connectors),
|
||||
id,
|
||||
))
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::RefundsRouterData<api::Execute>,
|
||||
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||
let req_obj = paypal::PaypalRefundRequest::try_from(req)?;
|
||||
let paypal_req =
|
||||
utils::Encode::<paypal::PaypalRefundRequest>::encode_to_string_of_json(&req_obj)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(paypal_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,
|
||||
)?)
|
||||
.body(types::RefundExecuteType::get_request_body(self, req)?)
|
||||
.build();
|
||||
|
||||
Ok(Some(request))
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::RefundsRouterData<api::Execute>,
|
||||
res: Response,
|
||||
) -> CustomResult<types::RefundsRouterData<api::Execute>, errors::ConnectorError> {
|
||||
let response: paypal::RefundResponse =
|
||||
res.response
|
||||
.parse_struct("paypal RefundResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData> for Paypal {
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::RefundSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, 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,
|
||||
res: Response,
|
||||
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
|
||||
let response: paypal::RefundSyncResponse = res
|
||||
.response
|
||||
.parse_struct("paypal RefundSyncResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
&self,
|
||||
res: Response,
|
||||
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[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> {
|
||||
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
||||
}
|
||||
|
||||
fn get_webhook_event_type(
|
||||
&self,
|
||||
_request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
||||
}
|
||||
|
||||
fn get_webhook_resource_object(
|
||||
&self,
|
||||
_request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
Ok(payments::CallConnectorAction::Trigger)
|
||||
}
|
||||
}
|
||||
608
crates/router/src/connector/paypal/transformers.rs
Normal file
608
crates/router/src/connector/paypal/transformers.rs
Normal file
@ -0,0 +1,608 @@
|
||||
use common_utils::errors::CustomResult;
|
||||
use masking::Secret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
connector::utils::{
|
||||
to_connector_meta, AccessTokenRequestInfo, AddressDetailsData, CardData,
|
||||
PaymentsAuthorizeRequestData,
|
||||
},
|
||||
core::errors,
|
||||
pii,
|
||||
types::{self, api, storage::enums as storage_enums, transformers::ForeignFrom},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum PaypalPaymentIntent {
|
||||
Capture,
|
||||
Authorize,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Eq, PartialEq, Deserialize)]
|
||||
pub struct OrderAmount {
|
||||
currency_code: storage_enums::Currency,
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
pub struct PurchaseUnitRequest {
|
||||
reference_id: String,
|
||||
amount: OrderAmount,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Address {
|
||||
address_line_1: Option<Secret<String>>,
|
||||
postal_code: Option<Secret<String>>,
|
||||
country_code: api_models::enums::CountryCode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CardRequest {
|
||||
billing_address: Option<Address>,
|
||||
expiry: Option<Secret<String>>,
|
||||
name: Secret<String>,
|
||||
number: Option<Secret<String, pii::CardNumber>>,
|
||||
security_code: Option<Secret<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PaymentSourceItem {
|
||||
Card(CardRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PaypalPaymentsRequest {
|
||||
intent: PaypalPaymentIntent,
|
||||
purchase_units: Vec<PurchaseUnitRequest>,
|
||||
payment_source: Option<PaymentSourceItem>,
|
||||
}
|
||||
|
||||
fn get_address_info(
|
||||
payment_address: Option<&api_models::payments::Address>,
|
||||
) -> Result<Option<Address>, error_stack::Report<errors::ConnectorError>> {
|
||||
let address = payment_address.and_then(|payment_address| payment_address.address.as_ref());
|
||||
let address = match address {
|
||||
Some(address) => Some(Address {
|
||||
country_code: address.get_country()?.to_owned(),
|
||||
address_line_1: address.line1.clone(),
|
||||
postal_code: address.zip.clone(),
|
||||
}),
|
||||
None => None,
|
||||
};
|
||||
Ok(address)
|
||||
}
|
||||
|
||||
impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaypalPaymentsRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||
match item.request.payment_method_data {
|
||||
api_models::payments::PaymentMethodData::Card(ref ccard) => {
|
||||
let intent = match item.request.is_auto_capture() {
|
||||
true => PaypalPaymentIntent::Capture,
|
||||
false => PaypalPaymentIntent::Authorize,
|
||||
};
|
||||
let amount = OrderAmount {
|
||||
currency_code: item.request.currency,
|
||||
value: item.request.amount.to_string(),
|
||||
};
|
||||
let reference_id = item.attempt_id.clone();
|
||||
|
||||
let purchase_units = vec![PurchaseUnitRequest {
|
||||
reference_id,
|
||||
amount,
|
||||
}];
|
||||
let card = item.request.get_card()?;
|
||||
let expiry = Some(card.get_expiry_date_as_yyyymm("-"));
|
||||
|
||||
let payment_source = Some(PaymentSourceItem::Card(CardRequest {
|
||||
billing_address: get_address_info(item.address.billing.as_ref())?,
|
||||
expiry,
|
||||
name: ccard.card_holder_name.clone(),
|
||||
number: Some(ccard.card_number.clone()),
|
||||
security_code: Some(ccard.card_cvc.clone()),
|
||||
}));
|
||||
|
||||
Ok(Self {
|
||||
intent,
|
||||
purchase_units,
|
||||
payment_source,
|
||||
})
|
||||
}
|
||||
_ => Err(errors::ConnectorError::NotImplemented("Payment Method".to_string()).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
pub struct PaypalAuthUpdateRequest {
|
||||
grant_type: String,
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
}
|
||||
impl TryFrom<&types::RefreshTokenRouterData> for PaypalAuthUpdateRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::RefreshTokenRouterData) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
grant_type: "client_credentials".to_string(),
|
||||
client_id: item.get_request_id()?,
|
||||
client_secret: item.request.app_id.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, PartialEq)]
|
||||
pub struct PaypalAuthUpdateResponse {
|
||||
pub access_token: String,
|
||||
pub token_type: String,
|
||||
pub expires_in: i64,
|
||||
}
|
||||
|
||||
impl<F, T> TryFrom<types::ResponseRouterData<F, PaypalAuthUpdateResponse, T, types::AccessToken>>
|
||||
for types::RouterData<F, T, types::AccessToken>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<F, PaypalAuthUpdateResponse, T, types::AccessToken>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
response: Ok(types::AccessToken {
|
||||
token: item.response.access_token,
|
||||
expires: item.response.expires_in,
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PaypalAuthType {
|
||||
pub(super) api_key: String,
|
||||
pub(super) key1: String,
|
||||
}
|
||||
|
||||
impl TryFrom<&types::ConnectorAuthType> for PaypalAuthType {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
|
||||
match auth_type {
|
||||
types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self {
|
||||
api_key: api_key.to_string(),
|
||||
key1: key1.to_string(),
|
||||
}),
|
||||
_ => Err(errors::ConnectorError::FailedToObtainAuthType)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PaypalOrderStatus {
|
||||
Completed,
|
||||
Voided,
|
||||
Created,
|
||||
Saved,
|
||||
PayerActionRequired,
|
||||
Approved,
|
||||
}
|
||||
|
||||
impl ForeignFrom<(PaypalOrderStatus, PaypalPaymentIntent)> for storage_enums::AttemptStatus {
|
||||
fn foreign_from(item: (PaypalOrderStatus, PaypalPaymentIntent)) -> Self {
|
||||
match item.0 {
|
||||
PaypalOrderStatus::Completed => {
|
||||
if item.1 == PaypalPaymentIntent::Authorize {
|
||||
Self::Authorized
|
||||
} else {
|
||||
Self::Charged
|
||||
}
|
||||
}
|
||||
PaypalOrderStatus::Voided => Self::Voided,
|
||||
PaypalOrderStatus::Created | PaypalOrderStatus::Saved | PaypalOrderStatus::Approved => {
|
||||
Self::Pending
|
||||
}
|
||||
PaypalOrderStatus::PayerActionRequired => Self::Authorizing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PaymentsCollectionItem {
|
||||
amount: OrderAmount,
|
||||
expiration_time: Option<String>,
|
||||
id: String,
|
||||
final_capture: Option<bool>,
|
||||
status: PaypalOrderStatus,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PaymentsCollection {
|
||||
authorizations: Option<Vec<PaymentsCollectionItem>>,
|
||||
captures: Option<Vec<PaymentsCollectionItem>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PurchaseUnitItem {
|
||||
reference_id: String,
|
||||
payments: PaymentsCollection,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PaypalOrdersResponse {
|
||||
id: String,
|
||||
intent: PaypalPaymentIntent,
|
||||
status: PaypalOrderStatus,
|
||||
purchase_units: Vec<PurchaseUnitItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PaypalPaymentsSyncResponse {
|
||||
id: String,
|
||||
status: PaypalPaymentStatus,
|
||||
amount: OrderAmount,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PaypalMeta {
|
||||
pub authorize_id: Option<String>,
|
||||
pub order_id: String,
|
||||
pub psync_flow: PaypalPaymentIntent,
|
||||
}
|
||||
|
||||
fn get_id_based_on_intent(
|
||||
intent: &PaypalPaymentIntent,
|
||||
purchase_unit: &PurchaseUnitItem,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
|| -> _ {
|
||||
match intent {
|
||||
PaypalPaymentIntent::Capture => Some(
|
||||
purchase_unit
|
||||
.payments
|
||||
.captures
|
||||
.clone()?
|
||||
.into_iter()
|
||||
.next()?
|
||||
.id,
|
||||
),
|
||||
PaypalPaymentIntent::Authorize => Some(
|
||||
purchase_unit
|
||||
.payments
|
||||
.authorizations
|
||||
.clone()?
|
||||
.into_iter()
|
||||
.next()?
|
||||
.id,
|
||||
),
|
||||
}
|
||||
}()
|
||||
.ok_or(errors::ConnectorError::MissingConnectorTransactionID.into())
|
||||
}
|
||||
|
||||
impl<F, T>
|
||||
TryFrom<types::ResponseRouterData<F, PaypalOrdersResponse, T, types::PaymentsResponseData>>
|
||||
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<F, PaypalOrdersResponse, T, types::PaymentsResponseData>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let purchase_units = item
|
||||
.response
|
||||
.purchase_units
|
||||
.first()
|
||||
.ok_or(errors::ConnectorError::MissingConnectorTransactionID)?;
|
||||
|
||||
let id = get_id_based_on_intent(&item.response.intent, purchase_units)?;
|
||||
let (connector_meta, capture_id) = match item.response.intent.clone() {
|
||||
PaypalPaymentIntent::Capture => (
|
||||
serde_json::json!(PaypalMeta {
|
||||
authorize_id: None,
|
||||
order_id: item.response.id,
|
||||
psync_flow: item.response.intent.clone()
|
||||
}),
|
||||
types::ResponseId::ConnectorTransactionId(id),
|
||||
),
|
||||
|
||||
PaypalPaymentIntent::Authorize => (
|
||||
serde_json::json!(PaypalMeta {
|
||||
authorize_id: Some(id),
|
||||
order_id: item.response.id,
|
||||
psync_flow: item.response.intent.clone()
|
||||
}),
|
||||
types::ResponseId::NoResponseId,
|
||||
),
|
||||
};
|
||||
Ok(Self {
|
||||
status: storage_enums::AttemptStatus::foreign_from((
|
||||
item.response.status,
|
||||
item.response.intent,
|
||||
)),
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: capture_id,
|
||||
redirection_data: None,
|
||||
mandate_reference: None,
|
||||
connector_metadata: Some(connector_meta),
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, T>
|
||||
TryFrom<
|
||||
types::ResponseRouterData<F, PaypalPaymentsSyncResponse, T, types::PaymentsResponseData>,
|
||||
> for types::RouterData<F, T, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<
|
||||
F,
|
||||
PaypalPaymentsSyncResponse,
|
||||
T,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
status: storage_enums::AttemptStatus::from(item.response.status),
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
|
||||
redirection_data: None,
|
||||
mandate_reference: None,
|
||||
connector_metadata: None,
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PaypalPaymentsCaptureRequest {
|
||||
amount: OrderAmount,
|
||||
final_capture: bool,
|
||||
}
|
||||
|
||||
impl TryFrom<&types::PaymentsCaptureRouterData> for PaypalPaymentsCaptureRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
|
||||
let amount = OrderAmount {
|
||||
currency_code: item.request.currency,
|
||||
value: item.request.amount_to_capture.to_string(),
|
||||
};
|
||||
Ok(Self {
|
||||
amount,
|
||||
final_capture: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PaypalPaymentStatus {
|
||||
Created,
|
||||
Captured,
|
||||
Completed,
|
||||
Declined,
|
||||
Failed,
|
||||
Pending,
|
||||
Denied,
|
||||
Expired,
|
||||
PartiallyCaptured,
|
||||
Refunded,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PaymentCaptureResponse {
|
||||
id: String,
|
||||
status: PaypalPaymentStatus,
|
||||
amount: Option<OrderAmount>,
|
||||
final_capture: bool,
|
||||
}
|
||||
|
||||
impl From<PaypalPaymentStatus> for storage_enums::AttemptStatus {
|
||||
fn from(item: PaypalPaymentStatus) -> Self {
|
||||
match item {
|
||||
PaypalPaymentStatus::Created => Self::Authorized,
|
||||
PaypalPaymentStatus::Completed
|
||||
| PaypalPaymentStatus::Captured
|
||||
| PaypalPaymentStatus::Refunded => Self::Charged,
|
||||
PaypalPaymentStatus::Declined => Self::Failure,
|
||||
PaypalPaymentStatus::Failed => Self::CaptureFailed,
|
||||
PaypalPaymentStatus::Pending => Self::Pending,
|
||||
PaypalPaymentStatus::Denied | PaypalPaymentStatus::Expired => Self::Failure,
|
||||
PaypalPaymentStatus::PartiallyCaptured => Self::PartialCharged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<types::PaymentsCaptureResponseRouterData<PaymentCaptureResponse>>
|
||||
for types::PaymentsCaptureRouterData
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::PaymentsCaptureResponseRouterData<PaymentCaptureResponse>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let amount_captured = item.data.request.amount_to_capture;
|
||||
let status = storage_enums::AttemptStatus::from(item.response.status);
|
||||
let connector_payment_id: PaypalMeta =
|
||||
to_connector_meta(item.data.request.connector_meta.clone())?;
|
||||
Ok(Self {
|
||||
status,
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
|
||||
redirection_data: None,
|
||||
mandate_reference: None,
|
||||
connector_metadata: Some(serde_json::json!(PaypalMeta {
|
||||
authorize_id: connector_payment_id.authorize_id,
|
||||
order_id: item.data.request.connector_transaction_id.clone(),
|
||||
psync_flow: PaypalPaymentIntent::Capture
|
||||
})),
|
||||
}),
|
||||
amount_captured: Some(amount_captured),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PaypalCancelStatus {
|
||||
Voided,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PaypalPaymentsCancelResponse {
|
||||
id: String,
|
||||
status: PaypalCancelStatus,
|
||||
amount: Option<OrderAmount>,
|
||||
}
|
||||
|
||||
impl<F, T>
|
||||
TryFrom<
|
||||
types::ResponseRouterData<F, PaypalPaymentsCancelResponse, T, types::PaymentsResponseData>,
|
||||
> for types::RouterData<F, T, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<
|
||||
F,
|
||||
PaypalPaymentsCancelResponse,
|
||||
T,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let status = match item.response.status {
|
||||
PaypalCancelStatus::Voided => storage_enums::AttemptStatus::Voided,
|
||||
};
|
||||
Ok(Self {
|
||||
status,
|
||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
|
||||
redirection_data: None,
|
||||
mandate_reference: None,
|
||||
connector_metadata: None,
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize)]
|
||||
pub struct PaypalRefundRequest {
|
||||
pub amount: OrderAmount,
|
||||
}
|
||||
|
||||
impl<F> TryFrom<&types::RefundsRouterData<F>> for PaypalRefundRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
amount: OrderAmount {
|
||||
currency_code: item.request.currency,
|
||||
value: item.request.refund_amount.to_string(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum RefundStatus {
|
||||
Completed,
|
||||
Failed,
|
||||
Cancelled,
|
||||
Pending,
|
||||
}
|
||||
|
||||
impl From<RefundStatus> for storage_enums::RefundStatus {
|
||||
fn from(item: RefundStatus) -> Self {
|
||||
match item {
|
||||
RefundStatus::Completed => Self::Success,
|
||||
RefundStatus::Failed | RefundStatus::Cancelled => Self::Failure,
|
||||
RefundStatus::Pending => Self::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RefundResponse {
|
||||
id: String,
|
||||
status: RefundStatus,
|
||||
amount: Option<OrderAmount>,
|
||||
}
|
||||
|
||||
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
|
||||
for types::RefundsRouterData<api::Execute>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
response: Ok(types::RefundsResponseData {
|
||||
connector_refund_id: item.response.id,
|
||||
refund_status: storage_enums::RefundStatus::from(item.response.status),
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct RefundSyncResponse {
|
||||
id: String,
|
||||
status: RefundStatus,
|
||||
}
|
||||
|
||||
impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundSyncResponse>>
|
||||
for types::RefundsRouterData<api::RSync>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::RefundsResponseRouterData<api::RSync, RefundSyncResponse>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
response: Ok(types::RefundsResponseData {
|
||||
connector_refund_id: item.response.id,
|
||||
refund_status: storage_enums::RefundStatus::from(item.response.status),
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct OrderErrorDetails {
|
||||
pub issue: String,
|
||||
pub description: String,
|
||||
pub value: Option<String>,
|
||||
pub field: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PaypalOrderErrorResponse {
|
||||
pub name: String,
|
||||
pub message: String,
|
||||
pub debug_id: Option<String>,
|
||||
pub details: Option<Vec<OrderErrorDetails>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ErrorDetails {
|
||||
pub issue: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PaypalPaymentErrorResponse {
|
||||
pub name: String,
|
||||
pub message: String,
|
||||
pub debug_id: Option<String>,
|
||||
pub details: Option<Vec<ErrorDetails>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct PaypalAccessTokenErrorResponse {
|
||||
pub error: String,
|
||||
pub error_description: String,
|
||||
}
|
||||
@ -114,7 +114,7 @@ pub struct PaymentRequestCards {
|
||||
pub pan: Secret<String, pii::CardNumber>,
|
||||
pub cvv: Secret<String>,
|
||||
#[serde(rename = "exp")]
|
||||
pub expiry_date: String,
|
||||
pub expiry_date: Secret<String>,
|
||||
pub cardholder: Secret<String>,
|
||||
pub reference: String,
|
||||
#[serde(rename = "redirectUrl")]
|
||||
|
||||
@ -247,7 +247,11 @@ pub enum CardIssuer {
|
||||
pub trait CardData {
|
||||
fn get_card_expiry_year_2_digit(&self) -> Secret<String>;
|
||||
fn get_card_issuer(&self) -> Result<CardIssuer, Error>;
|
||||
fn get_card_expiry_month_year_2_digit_with_delimiter(&self, delimiter: String) -> String;
|
||||
fn get_card_expiry_month_year_2_digit_with_delimiter(
|
||||
&self,
|
||||
delimiter: String,
|
||||
) -> Secret<String>;
|
||||
fn get_expiry_date_as_yyyymm(&self, delimiter: &str) -> Secret<String>;
|
||||
}
|
||||
|
||||
impl CardData for api::Card {
|
||||
@ -263,14 +267,29 @@ impl CardData for api::Card {
|
||||
.map(|card| card.split_whitespace().collect());
|
||||
get_card_issuer(card.peek().clone().as_str())
|
||||
}
|
||||
fn get_card_expiry_month_year_2_digit_with_delimiter(&self, delimiter: String) -> String {
|
||||
fn get_card_expiry_month_year_2_digit_with_delimiter(
|
||||
&self,
|
||||
delimiter: String,
|
||||
) -> Secret<String> {
|
||||
let year = self.get_card_expiry_year_2_digit();
|
||||
format!(
|
||||
Secret::new(format!(
|
||||
"{}{}{}",
|
||||
self.card_exp_month.peek().clone(),
|
||||
delimiter,
|
||||
year.peek()
|
||||
)
|
||||
))
|
||||
}
|
||||
fn get_expiry_date_as_yyyymm(&self, delimiter: &str) -> Secret<String> {
|
||||
let mut x = self.card_exp_year.peek().clone();
|
||||
if x.len() == 2 {
|
||||
x = format!("20{}", x);
|
||||
}
|
||||
Secret::new(format!(
|
||||
"{}{}{}",
|
||||
x,
|
||||
delimiter,
|
||||
self.card_exp_month.peek().clone()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -133,3 +133,38 @@ default_imp_for_connector_redirect_response!(
|
||||
connector::Worldline,
|
||||
connector::Worldpay
|
||||
);
|
||||
|
||||
macro_rules! default_imp_for_connector_request_id{
|
||||
($($path:ident::$connector:ident),*)=> {
|
||||
$(
|
||||
impl api::ConnectorTransactionId for $path::$connector {}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
default_imp_for_connector_request_id!(
|
||||
connector::Aci,
|
||||
connector::Adyen,
|
||||
connector::Airwallex,
|
||||
connector::Applepay,
|
||||
connector::Authorizedotnet,
|
||||
connector::Bambora,
|
||||
connector::Bluesnap,
|
||||
connector::Braintree,
|
||||
connector::Checkout,
|
||||
connector::Cybersource,
|
||||
connector::Dlocal,
|
||||
connector::Fiserv,
|
||||
connector::Globalpay,
|
||||
connector::Klarna,
|
||||
connector::Mollie,
|
||||
connector::Multisafepay,
|
||||
connector::Nuvei,
|
||||
connector::Payu,
|
||||
connector::Rapyd,
|
||||
connector::Shift4,
|
||||
connector::Stripe,
|
||||
connector::Trustpay,
|
||||
connector::Worldline,
|
||||
connector::Worldpay
|
||||
);
|
||||
|
||||
@ -6,6 +6,7 @@ use router_env::{instrument, tracing};
|
||||
use super::{flows::Feature, PaymentAddress, PaymentData};
|
||||
use crate::{
|
||||
configs::settings::Server,
|
||||
connector::Paypal,
|
||||
core::{
|
||||
errors::{self, RouterResponse, RouterResult},
|
||||
payments::{self, helpers},
|
||||
@ -28,11 +29,11 @@ pub async fn construct_payment_router_data<'a, F, T>(
|
||||
merchant_account: &storage::MerchantAccount,
|
||||
) -> RouterResult<types::RouterData<F, T, types::PaymentsResponseData>>
|
||||
where
|
||||
T: TryFrom<PaymentAdditionalData<F>>,
|
||||
T: TryFrom<PaymentAdditionalData<'a, F>>,
|
||||
types::RouterData<F, T, types::PaymentsResponseData>: Feature<F, T>,
|
||||
F: Clone,
|
||||
error_stack::Report<errors::ApiErrorResponse>:
|
||||
From<<T as TryFrom<PaymentAdditionalData<F>>>::Error>,
|
||||
From<<T as TryFrom<PaymentAdditionalData<'a, F>>>::Error>,
|
||||
{
|
||||
let (merchant_connector_account, payment_method, router_data);
|
||||
let db = &*state.store;
|
||||
@ -72,6 +73,7 @@ where
|
||||
router_base_url: state.conf.server.base_url.clone(),
|
||||
connector_name: connector_id.to_string(),
|
||||
payment_data: payment_data.clone(),
|
||||
state,
|
||||
};
|
||||
|
||||
router_data = types::RouterData {
|
||||
@ -423,18 +425,19 @@ impl ForeignTryFrom<(storage::PaymentIntent, storage::PaymentAttempt)> for api::
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PaymentAdditionalData<F>
|
||||
pub struct PaymentAdditionalData<'a, F>
|
||||
where
|
||||
F: Clone,
|
||||
{
|
||||
router_base_url: String,
|
||||
connector_name: String,
|
||||
payment_data: PaymentData<F>,
|
||||
state: &'a AppState,
|
||||
}
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<F>> for types::PaymentsAuthorizeData {
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsAuthorizeData {
|
||||
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
||||
|
||||
fn try_from(additional_data: PaymentAdditionalData<F>) -> Result<Self, Self::Error> {
|
||||
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
|
||||
let payment_data = additional_data.payment_data;
|
||||
let router_base_url = &additional_data.router_base_url;
|
||||
let connector_name = &additional_data.connector_name;
|
||||
@ -509,10 +512,10 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<F>> for types::PaymentsAuthorizeDat
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<F>> for types::PaymentsSyncData {
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSyncData {
|
||||
type Error = errors::ApiErrorResponse;
|
||||
|
||||
fn try_from(additional_data: PaymentAdditionalData<F>) -> Result<Self, Self::Error> {
|
||||
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
|
||||
let payment_data = additional_data.payment_data;
|
||||
Ok(Self {
|
||||
connector_transaction_id: match payment_data.payment_attempt.connector_transaction_id {
|
||||
@ -528,11 +531,34 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<F>> for types::PaymentsSyncData {
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<F>> for types::PaymentsCaptureData {
|
||||
impl api::ConnectorTransactionId for Paypal {
|
||||
fn connector_transaction_id(
|
||||
&self,
|
||||
payment_attempt: storage::PaymentAttempt,
|
||||
) -> Result<Option<String>, errors::ApiErrorResponse> {
|
||||
let metadata = Self::connector_transaction_id(self, &payment_attempt.connector_metadata);
|
||||
match metadata {
|
||||
Ok(data) => Ok(data),
|
||||
_ => Err(errors::ApiErrorResponse::ResourceIdNotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsCaptureData {
|
||||
type Error = errors::ApiErrorResponse;
|
||||
|
||||
fn try_from(additional_data: PaymentAdditionalData<F>) -> Result<Self, Self::Error> {
|
||||
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
|
||||
let payment_data = additional_data.payment_data;
|
||||
let connector = api::ConnectorData::get_connector_by_name(
|
||||
&additional_data.state.conf.connectors,
|
||||
&additional_data.connector_name,
|
||||
api::GetToken::Connector,
|
||||
);
|
||||
let connectors = match connector {
|
||||
Ok(conn) => *conn.connector,
|
||||
_ => Err(errors::ApiErrorResponse::ResourceIdNotFound)?,
|
||||
};
|
||||
|
||||
let amount_to_capture: i64 = payment_data
|
||||
.payment_attempt
|
||||
.amount_to_capture
|
||||
@ -540,40 +566,45 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<F>> for types::PaymentsCaptureData
|
||||
Ok(Self {
|
||||
amount_to_capture,
|
||||
currency: payment_data.currency,
|
||||
connector_transaction_id: payment_data
|
||||
.payment_attempt
|
||||
.connector_transaction_id
|
||||
.ok_or(errors::ApiErrorResponse::MerchantConnectorAccountNotFound)?,
|
||||
connector_transaction_id: connectors
|
||||
.connector_transaction_id(payment_data.payment_attempt.clone())?
|
||||
.ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?,
|
||||
payment_amount: payment_data.amount.into(),
|
||||
connector_meta: payment_data.payment_attempt.connector_metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<F>> for types::PaymentsCancelData {
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsCancelData {
|
||||
type Error = errors::ApiErrorResponse;
|
||||
|
||||
fn try_from(additional_data: PaymentAdditionalData<F>) -> Result<Self, Self::Error> {
|
||||
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
|
||||
let payment_data = additional_data.payment_data;
|
||||
let connector = api::ConnectorData::get_connector_by_name(
|
||||
&additional_data.state.conf.connectors,
|
||||
&additional_data.connector_name,
|
||||
api::GetToken::Connector,
|
||||
);
|
||||
let connectors = match connector {
|
||||
Ok(conn) => *conn.connector,
|
||||
_ => Err(errors::ApiErrorResponse::ResourceIdNotFound)?,
|
||||
};
|
||||
Ok(Self {
|
||||
amount: Some(payment_data.amount.into()),
|
||||
currency: Some(payment_data.currency),
|
||||
connector_transaction_id: payment_data
|
||||
.payment_attempt
|
||||
.connector_transaction_id
|
||||
.ok_or(errors::ApiErrorResponse::MissingRequiredField {
|
||||
field_name: "connector_transaction_id",
|
||||
})?,
|
||||
connector_transaction_id: connectors
|
||||
.connector_transaction_id(payment_data.payment_attempt.clone())?
|
||||
.ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?,
|
||||
cancellation_reason: payment_data.payment_attempt.cancellation_reason,
|
||||
connector_meta: payment_data.payment_attempt.connector_metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<F>> for types::PaymentsSessionData {
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSessionData {
|
||||
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
||||
|
||||
fn try_from(additional_data: PaymentAdditionalData<F>) -> Result<Self, Self::Error> {
|
||||
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
|
||||
let payment_data = additional_data.payment_data;
|
||||
let parsed_metadata: Option<api_models::payments::Metadata> = payment_data
|
||||
.payment_intent
|
||||
@ -602,10 +633,10 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<F>> for types::PaymentsSessionData
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<F>> for types::VerifyRequestData {
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::VerifyRequestData {
|
||||
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
||||
|
||||
fn try_from(additional_data: PaymentAdditionalData<F>) -> Result<Self, Self::Error> {
|
||||
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
|
||||
let payment_data = additional_data.payment_data;
|
||||
Ok(Self {
|
||||
currency: payment_data.currency,
|
||||
@ -622,10 +653,10 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<F>> for types::VerifyRequestData {
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<F>> for types::CompleteAuthorizeData {
|
||||
impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::CompleteAuthorizeData {
|
||||
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
||||
|
||||
fn try_from(additional_data: PaymentAdditionalData<F>) -> Result<Self, Self::Error> {
|
||||
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
|
||||
let payment_data = additional_data.payment_data;
|
||||
let browser_info: Option<types::BrowserInformation> = payment_data
|
||||
.payment_attempt
|
||||
|
||||
@ -35,6 +35,15 @@ pub trait ConnectorAccessToken:
|
||||
{
|
||||
}
|
||||
|
||||
pub trait ConnectorTransactionId: ConnectorCommon + Sync {
|
||||
fn connector_transaction_id(
|
||||
&self,
|
||||
payment_attempt: storage_models::payment_attempt::PaymentAttempt,
|
||||
) -> Result<Option<String>, errors::ApiErrorResponse> {
|
||||
Ok(payment_attempt.connector_transaction_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ConnectorCommon {
|
||||
/// Name of the connector (in lowercase).
|
||||
fn id(&self) -> &'static str;
|
||||
@ -90,7 +99,14 @@ pub trait ConnectorCommonExt<Flow, Req, Resp>:
|
||||
pub trait Router {}
|
||||
|
||||
pub trait Connector:
|
||||
Send + Refund + Payment + Debug + ConnectorRedirectResponse + IncomingWebhook + ConnectorAccessToken
|
||||
Send
|
||||
+ Refund
|
||||
+ Payment
|
||||
+ Debug
|
||||
+ ConnectorRedirectResponse
|
||||
+ IncomingWebhook
|
||||
+ ConnectorAccessToken
|
||||
+ ConnectorTransactionId
|
||||
{
|
||||
}
|
||||
|
||||
@ -105,7 +121,8 @@ impl<
|
||||
+ ConnectorRedirectResponse
|
||||
+ Send
|
||||
+ IncomingWebhook
|
||||
+ ConnectorAccessToken,
|
||||
+ ConnectorAccessToken
|
||||
+ ConnectorTransactionId,
|
||||
> Connector for T
|
||||
{
|
||||
}
|
||||
@ -189,6 +206,7 @@ impl ConnectorData {
|
||||
"worldline" => Ok(Box::new(&connector::Worldline)),
|
||||
"worldpay" => Ok(Box::new(&connector::Worldpay)),
|
||||
"multisafepay" => Ok(Box::new(&connector::Multisafepay)),
|
||||
"paypal" => Ok(Box::new(&connector::Paypal)),
|
||||
"trustpay" => Ok(Box::new(&connector::Trustpay)),
|
||||
_ => Err(report!(errors::ConnectorError::InvalidConnectorName)
|
||||
.attach_printable(format!("invalid connector name: {connector_name}")))
|
||||
|
||||
@ -19,6 +19,7 @@ pub(crate) struct ConnectorAuthentication {
|
||||
pub mollie: Option<HeaderKey>,
|
||||
pub multisafepay: Option<HeaderKey>,
|
||||
pub nuvei: Option<SignatureKey>,
|
||||
pub paypal: Option<BodyKey>,
|
||||
pub payu: Option<BodyKey>,
|
||||
pub rapyd: Option<BodyKey>,
|
||||
pub shift4: Option<HeaderKey>,
|
||||
|
||||
@ -15,6 +15,7 @@ mod globalpay;
|
||||
mod mollie;
|
||||
mod multisafepay;
|
||||
mod nuvei;
|
||||
mod paypal;
|
||||
mod payu;
|
||||
mod rapyd;
|
||||
mod shift4;
|
||||
|
||||
627
crates/router/tests/connectors/paypal.rs
Normal file
627
crates/router/tests/connectors/paypal.rs
Normal file
@ -0,0 +1,627 @@
|
||||
use masking::Secret;
|
||||
use router::types::{self, api, storage::enums, AccessToken, ConnectorAuthType};
|
||||
|
||||
use crate::{
|
||||
connector_auth,
|
||||
utils::{self, Connector, ConnectorActions},
|
||||
};
|
||||
|
||||
struct PaypalTest;
|
||||
impl ConnectorActions for PaypalTest {}
|
||||
impl Connector for PaypalTest {
|
||||
fn get_data(&self) -> types::api::ConnectorData {
|
||||
use router::connector::Paypal;
|
||||
types::api::ConnectorData {
|
||||
connector: Box::new(&Paypal),
|
||||
connector_name: types::Connector::Paypal,
|
||||
get_token: types::api::GetToken::Connector,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_auth_token(&self) -> ConnectorAuthType {
|
||||
types::ConnectorAuthType::from(
|
||||
connector_auth::ConnectorAuthentication::new()
|
||||
.paypal
|
||||
.expect("Missing connector authentication configuration"),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
"paypal".to_string()
|
||||
}
|
||||
}
|
||||
static CONNECTOR: PaypalTest = PaypalTest {};
|
||||
|
||||
fn get_access_token() -> Option<AccessToken> {
|
||||
let connector = PaypalTest {};
|
||||
|
||||
match connector.get_auth_token() {
|
||||
ConnectorAuthType::BodyKey { api_key, key1: _ } => Some(AccessToken {
|
||||
token: api_key,
|
||||
expires: 18600,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn get_default_payment_info() -> Option<utils::PaymentInfo> {
|
||||
Some(utils::PaymentInfo {
|
||||
access_token: get_access_token(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn get_payment_data() -> Option<types::PaymentsAuthorizeData> {
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_number: Secret::new(String::from("4000020000000000")),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
})
|
||||
}
|
||||
|
||||
// Cards Positive Tests
|
||||
// Creates a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_only_authorize_payment() {
|
||||
let response = CONNECTOR
|
||||
.authorize_payment(get_payment_data(), get_default_payment_info())
|
||||
.await
|
||||
.expect("Authorize payment response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Authorized);
|
||||
}
|
||||
|
||||
// Captures a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_capture_authorized_payment() {
|
||||
let authorize_response = CONNECTOR
|
||||
.authorize_payment(get_payment_data(), get_default_payment_info())
|
||||
.await
|
||||
.expect("Authorize payment response");
|
||||
let txn_id = "".to_string();
|
||||
let connector_meta = utils::get_connector_metadata(authorize_response.response);
|
||||
let response = CONNECTOR
|
||||
.capture_payment(
|
||||
txn_id,
|
||||
Some(types::PaymentsCaptureData {
|
||||
connector_meta,
|
||||
..utils::PaymentCaptureType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("Capture payment response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Charged);
|
||||
}
|
||||
|
||||
// Partially captures a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_partially_capture_authorized_payment() {
|
||||
let authorize_response = CONNECTOR
|
||||
.authorize_payment(get_payment_data(), get_default_payment_info())
|
||||
.await
|
||||
.expect("Authorize payment response");
|
||||
let txn_id = "".to_string();
|
||||
let connector_meta = utils::get_connector_metadata(authorize_response.response);
|
||||
let response = CONNECTOR
|
||||
.capture_payment(
|
||||
txn_id,
|
||||
Some(types::PaymentsCaptureData {
|
||||
connector_meta,
|
||||
amount_to_capture: 50,
|
||||
..utils::PaymentCaptureType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("Capture payment response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Charged);
|
||||
}
|
||||
|
||||
// Synchronizes a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_sync_authorized_payment() {
|
||||
let authorize_response = CONNECTOR
|
||||
.authorize_payment(get_payment_data(), get_default_payment_info())
|
||||
.await
|
||||
.expect("Authorize payment response");
|
||||
let txn_id = "".to_string();
|
||||
let connector_meta = utils::get_connector_metadata(authorize_response.response);
|
||||
let response = CONNECTOR
|
||||
.psync_retry_till_status_matches(
|
||||
enums::AttemptStatus::Authorized,
|
||||
Some(types::PaymentsSyncData {
|
||||
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(txn_id),
|
||||
encoded_data: None,
|
||||
capture_method: None,
|
||||
connector_meta,
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("PSync response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Authorized,);
|
||||
}
|
||||
|
||||
// Voids a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_void_authorized_payment() {
|
||||
let authorize_response = CONNECTOR
|
||||
.authorize_payment(get_payment_data(), get_default_payment_info())
|
||||
.await
|
||||
.expect("Authorize payment response");
|
||||
let txn_id = "".to_string();
|
||||
let connector_meta = utils::get_connector_metadata(authorize_response.response);
|
||||
let response = CONNECTOR
|
||||
.void_payment(
|
||||
txn_id,
|
||||
Some(types::PaymentsCancelData {
|
||||
connector_transaction_id: String::from(""),
|
||||
cancellation_reason: Some("requested_by_customer".to_string()),
|
||||
connector_meta,
|
||||
..Default::default()
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("Void payment response");
|
||||
assert_eq!(response.status, enums::AttemptStatus::Voided);
|
||||
}
|
||||
|
||||
// Refunds a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_refund_manually_captured_payment() {
|
||||
let authorize_response = CONNECTOR
|
||||
.authorize_payment(get_payment_data(), get_default_payment_info())
|
||||
.await
|
||||
.expect("Authorize payment response");
|
||||
let txn_id = "".to_string();
|
||||
let capture_connector_meta = utils::get_connector_metadata(authorize_response.response);
|
||||
let capture_response = CONNECTOR
|
||||
.capture_payment(
|
||||
txn_id,
|
||||
Some(types::PaymentsCaptureData {
|
||||
connector_meta: capture_connector_meta,
|
||||
..utils::PaymentCaptureType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("Capture payment response");
|
||||
let refund_txn_id =
|
||||
utils::get_connector_transaction_id(capture_response.response.clone()).unwrap();
|
||||
let response = CONNECTOR
|
||||
.refund_payment(
|
||||
refund_txn_id,
|
||||
Some(types::RefundsData {
|
||||
..utils::PaymentRefundType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Partially refunds a payment using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_partially_refund_manually_captured_payment() {
|
||||
let authorize_response = CONNECTOR
|
||||
.authorize_payment(get_payment_data(), get_default_payment_info())
|
||||
.await
|
||||
.expect("Authorize payment response");
|
||||
let txn_id = "".to_string();
|
||||
let capture_connector_meta = utils::get_connector_metadata(authorize_response.response);
|
||||
let capture_response = CONNECTOR
|
||||
.capture_payment(
|
||||
txn_id,
|
||||
Some(types::PaymentsCaptureData {
|
||||
connector_meta: capture_connector_meta,
|
||||
..utils::PaymentCaptureType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("Capture payment response");
|
||||
let refund_txn_id =
|
||||
utils::get_connector_transaction_id(capture_response.response.clone()).unwrap();
|
||||
let response = CONNECTOR
|
||||
.refund_payment(
|
||||
refund_txn_id,
|
||||
Some(types::RefundsData {
|
||||
refund_amount: 50,
|
||||
..utils::PaymentRefundType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Synchronizes a refund using the manual capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_sync_manually_captured_refund() {
|
||||
let authorize_response = CONNECTOR
|
||||
.authorize_payment(get_payment_data(), get_default_payment_info())
|
||||
.await
|
||||
.expect("Authorize payment response");
|
||||
let txn_id = "".to_string();
|
||||
let capture_connector_meta = utils::get_connector_metadata(authorize_response.response);
|
||||
let capture_response = CONNECTOR
|
||||
.capture_payment(
|
||||
txn_id,
|
||||
Some(types::PaymentsCaptureData {
|
||||
connector_meta: capture_connector_meta,
|
||||
..utils::PaymentCaptureType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("Capture payment response");
|
||||
let refund_txn_id =
|
||||
utils::get_connector_transaction_id(capture_response.response.clone()).unwrap();
|
||||
let refund_response = CONNECTOR
|
||||
.refund_payment(
|
||||
refund_txn_id,
|
||||
Some(types::RefundsData {
|
||||
..utils::PaymentRefundType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let response = CONNECTOR
|
||||
.rsync_retry_till_status_matches(
|
||||
enums::RefundStatus::Success,
|
||||
refund_response.response.unwrap().connector_refund_id,
|
||||
None,
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a payment using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_make_payment() {
|
||||
let authorize_response = CONNECTOR
|
||||
.make_payment(get_payment_data(), get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
|
||||
}
|
||||
|
||||
// Synchronizes a payment using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_sync_auto_captured_payment() {
|
||||
let authorize_response = CONNECTOR
|
||||
.make_payment(get_payment_data(), get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
authorize_response.status.clone(),
|
||||
enums::AttemptStatus::Charged
|
||||
);
|
||||
let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone());
|
||||
assert_ne!(txn_id, None, "Empty connector transaction id");
|
||||
let connector_meta = utils::get_connector_metadata(authorize_response.response);
|
||||
let response = CONNECTOR
|
||||
.psync_retry_till_status_matches(
|
||||
enums::AttemptStatus::Charged,
|
||||
Some(types::PaymentsSyncData {
|
||||
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
|
||||
txn_id.unwrap(),
|
||||
),
|
||||
encoded_data: None,
|
||||
capture_method: Some(enums::CaptureMethod::Automatic),
|
||||
connector_meta,
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status, enums::AttemptStatus::Charged,);
|
||||
}
|
||||
|
||||
// Refunds a payment using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_refund_auto_captured_payment() {
|
||||
let response = CONNECTOR
|
||||
.make_payment_and_refund(get_payment_data(), None, get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Partially refunds a payment using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_partially_refund_succeeded_payment() {
|
||||
let authorize_response = CONNECTOR
|
||||
.make_payment(get_payment_data(), get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap();
|
||||
let refund_response = CONNECTOR
|
||||
.refund_payment(
|
||||
txn_id,
|
||||
Some(types::RefundsData {
|
||||
refund_amount: 50,
|
||||
..utils::PaymentRefundType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
refund_response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_refund_succeeded_payment_multiple_times() {
|
||||
let authorize_response = CONNECTOR
|
||||
.make_payment(get_payment_data(), get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap();
|
||||
for _x in 0..2 {
|
||||
let refund_response = CONNECTOR
|
||||
.refund_payment(
|
||||
txn_id.clone(),
|
||||
Some(types::RefundsData {
|
||||
refund_amount: 50,
|
||||
..utils::PaymentRefundType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
refund_response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Synchronizes a refund using the automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_sync_refund() {
|
||||
let refund_response = CONNECTOR
|
||||
.make_payment_and_refund(get_payment_data(), None, get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
let response = CONNECTOR
|
||||
.rsync_retry_till_status_matches(
|
||||
enums::RefundStatus::Success,
|
||||
refund_response.response.unwrap().connector_refund_id,
|
||||
None,
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap().refund_status,
|
||||
enums::RefundStatus::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Cards Negative scenerios
|
||||
// Creates a payment with incorrect card number.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_incorrect_card_number() {
|
||||
let response = CONNECTOR
|
||||
.make_payment(
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_number: Secret::new("1234567891011".to_string()),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"description - UNPROCESSABLE_ENTITY",
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a payment with empty card number.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_empty_card_number() {
|
||||
let response = CONNECTOR
|
||||
.make_payment(
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_number: Secret::new(String::from("")),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let x = response.response.unwrap_err();
|
||||
assert_eq!(
|
||||
x.message,
|
||||
"description - The card number is required when attempting to process payment with card., field - number;",
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a payment with incorrect CVC.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_incorrect_cvc() {
|
||||
let response = CONNECTOR
|
||||
.make_payment(
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_cvc: Secret::new("12345".to_string()),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"description - The value of a field does not conform to the expected format., value - 12345, field - security_code;",
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a payment with incorrect expiry month.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_invalid_exp_month() {
|
||||
let response = CONNECTOR
|
||||
.make_payment(
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_exp_month: Secret::new("20".to_string()),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"description - The value of a field does not conform to the expected format., value - 2025-20, field - expiry;",
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a payment with incorrect expiry year.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_payment_for_incorrect_expiry_year() {
|
||||
let response = CONNECTOR
|
||||
.make_payment(
|
||||
Some(types::PaymentsAuthorizeData {
|
||||
payment_method_data: types::api::PaymentMethodData::Card(api::Card {
|
||||
card_exp_year: Secret::new("2000".to_string()),
|
||||
..utils::CCardType::default().0
|
||||
}),
|
||||
..utils::PaymentAuthorizeType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.response.unwrap_err().message,
|
||||
"description - The card is expired., field - expiry;",
|
||||
);
|
||||
}
|
||||
|
||||
// Voids a payment using automatic capture flow (Non 3DS).
|
||||
#[actix_web::test]
|
||||
async fn should_fail_void_payment_for_auto_capture() {
|
||||
let authorize_response = CONNECTOR
|
||||
.authorize_payment(get_payment_data(), get_default_payment_info())
|
||||
.await
|
||||
.expect("Authorize payment response");
|
||||
let txn_id = "".to_string();
|
||||
let capture_connector_meta = utils::get_connector_metadata(authorize_response.response);
|
||||
let capture_response = CONNECTOR
|
||||
.capture_payment(
|
||||
txn_id,
|
||||
Some(types::PaymentsCaptureData {
|
||||
connector_meta: capture_connector_meta,
|
||||
..utils::PaymentCaptureType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("Capture payment response");
|
||||
let txn_id = utils::get_connector_transaction_id(capture_response.clone().response).unwrap();
|
||||
let connector_meta = utils::get_connector_metadata(capture_response.response);
|
||||
let void_response = CONNECTOR
|
||||
.void_payment(
|
||||
txn_id,
|
||||
Some(types::PaymentsCancelData {
|
||||
cancellation_reason: Some("requested_by_customer".to_string()),
|
||||
connector_meta,
|
||||
..Default::default()
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.expect("Void payment response");
|
||||
assert_eq!(
|
||||
void_response.response.unwrap_err().message,
|
||||
"description - Authorization has been previously captured and hence cannot be voided. ; "
|
||||
);
|
||||
}
|
||||
|
||||
// Captures a payment using invalid connector payment id.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_capture_for_invalid_payment() {
|
||||
let connector_meta = Some(serde_json::json!({
|
||||
"authorize_id": "56YH8TZ",
|
||||
"order_id":"02569315XM5003146",
|
||||
"psync_flow":"AUTHORIZE",
|
||||
}));
|
||||
let capture_response = CONNECTOR
|
||||
.capture_payment(
|
||||
"".to_string(),
|
||||
Some(types::PaymentsCaptureData {
|
||||
connector_meta,
|
||||
..utils::PaymentCaptureType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
capture_response.response.unwrap_err().message,
|
||||
"description - Specified resource ID does not exist. Please check the resource ID and try again. ; ",
|
||||
);
|
||||
}
|
||||
|
||||
// Refunds a payment with refund amount higher than payment amount.
|
||||
#[actix_web::test]
|
||||
async fn should_fail_for_refund_amount_higher_than_payment_amount() {
|
||||
let authorize_response = CONNECTOR
|
||||
.make_payment(get_payment_data(), get_default_payment_info())
|
||||
.await
|
||||
.unwrap();
|
||||
let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap();
|
||||
let response = CONNECTOR
|
||||
.refund_payment(
|
||||
txn_id,
|
||||
Some(types::RefundsData {
|
||||
refund_amount: 150,
|
||||
..utils::PaymentRefundType::default().0
|
||||
}),
|
||||
get_default_payment_info(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(&response.response.unwrap_err().message, "description - The refund amount must be less than or equal to the capture amount that has not yet been refunded. ; ");
|
||||
}
|
||||
|
||||
// Connector dependent test cases goes here
|
||||
|
||||
// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests
|
||||
@ -67,5 +67,9 @@ api_key = "api_key"
|
||||
key1 = "key1"
|
||||
api_secret = "secret"
|
||||
|
||||
[paypal]
|
||||
api_key = "api_key"
|
||||
key1 = "key1"
|
||||
|
||||
[mollie]
|
||||
api_key = "API Key"
|
||||
@ -579,3 +579,17 @@ pub fn get_connector_transaction_id(
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_connector_metadata(
|
||||
response: Result<types::PaymentsResponseData, types::ErrorResponse>,
|
||||
) -> Option<serde_json::Value> {
|
||||
match response {
|
||||
Ok(types::PaymentsResponseData::TransactionResponse {
|
||||
resource_id: _,
|
||||
redirection_data: _,
|
||||
mandate_reference: _,
|
||||
connector_metadata,
|
||||
}) => connector_metadata,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,6 +72,7 @@ klarna.base_url = "https://api-na.playground.klarna.com/"
|
||||
mollie.base_url = "https://api.mollie.com/v2/"
|
||||
multisafepay.base_url = "https://testapi.multisafepay.com/"
|
||||
nuvei.base_url = "https://ppp-test.nuvei.com/"
|
||||
paypal.base_url = "https://www.sandbox.paypal.com/"
|
||||
payu.base_url = "https://secure.snd.payu.com/"
|
||||
rapyd.base_url = "https://sandboxapi.rapyd.net"
|
||||
shift4.base_url = "https://api.shift4.com/"
|
||||
@ -99,6 +100,7 @@ cards = [
|
||||
"mollie",
|
||||
"multisafepay",
|
||||
"nuvei",
|
||||
"paypal",
|
||||
"payu",
|
||||
"shift4",
|
||||
"stripe",
|
||||
|
||||
Reference in New Issue
Block a user