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:
Prasunna Soppa
2023-04-06 19:06:43 +05:30
committed by GitHub
parent f26a632cdb
commit 36049c1341
19 changed files with 2190 additions and 38 deletions

View File

@ -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/"

View File

@ -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.

View File

@ -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",

View File

@ -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,

View File

@ -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,

View File

@ -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,
};

View 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)
}
}

View 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,
}

View File

@ -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")]

View File

@ -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()
))
}
}

View File

@ -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
);

View File

@ -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

View File

@ -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}")))

View File

@ -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>,

View File

@ -15,6 +15,7 @@ mod globalpay;
mod mollie;
mod multisafepay;
mod nuvei;
mod paypal;
mod payu;
mod rapyd;
mod shift4;

View 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

View File

@ -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"

View File

@ -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,
}
}

View File

@ -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",