From 36049c1341dded623e3bcb2787c3ae64437f18e5 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Thu, 6 Apr 2023 19:06:43 +0530 Subject: [PATCH] feat(connector): add authorize, capture, void, psync, refund, rsync for PayPal connector (#747) Co-authored-by: Arjun Karthik Co-authored-by: Arun Raj M --- config/Development.toml | 2 + config/config.example.toml | 7 +- config/docker_compose.toml | 3 + crates/api_models/src/enums.rs | 3 + crates/router/src/configs/settings.rs | 1 + crates/router/src/connector.rs | 3 +- crates/router/src/connector/paypal.rs | 781 ++++++++++++++++++ .../src/connector/paypal/transformers.rs | 608 ++++++++++++++ .../src/connector/trustpay/transformers.rs | 2 +- crates/router/src/connector/utils.rs | 27 +- crates/router/src/core/payments/flows.rs | 35 + .../router/src/core/payments/transformers.rs | 85 +- crates/router/src/types/api.rs | 22 +- .../router/tests/connectors/connector_auth.rs | 1 + crates/router/tests/connectors/main.rs | 1 + crates/router/tests/connectors/paypal.rs | 627 ++++++++++++++ .../router/tests/connectors/sample_auth.toml | 4 + crates/router/tests/connectors/utils.rs | 14 + loadtest/config/Development.toml | 2 + 19 files changed, 2190 insertions(+), 38 deletions(-) create mode 100644 crates/router/src/connector/paypal.rs create mode 100644 crates/router/src/connector/paypal/transformers.rs create mode 100644 crates/router/tests/connectors/paypal.rs diff --git a/config/Development.toml b/config/Development.toml index 2989e035c6..25110882c8 100644 --- a/config/Development.toml +++ b/config/Development.toml @@ -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/" diff --git a/config/config.example.toml b/config/config.example.toml index 9f6652bc3c..597d1181da 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -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. diff --git a/config/docker_compose.toml b/config/docker_compose.toml index fdcb63d2a0..50de78bf66 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -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", diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index c20fe28f9e..6d7cf15df2 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -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, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index eb88a48c9d..70833e20cb 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -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, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 9f5211d053..4a3299daff 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -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, }; diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs new file mode 100644 index 0000000000..d1f15c12f9 --- /dev/null +++ b/crates/router/src/connector/paypal.rs @@ -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, + ) -> CustomResult, 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 { + //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 ConnectorCommonExt for Paypal +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, 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, 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 { + 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 + for Paypal +{ + //TODO: implement sessions flow +} + +impl ConnectorIntegration + for Paypal +{ + fn get_url( + &self, + _req: &types::RefreshTokenRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + 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, 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, errors::ConnectorError> { + let paypal_req = + utils::Encode::::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, 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 { + 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 { + 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 + for Paypal +{ +} + +impl ConnectorIntegration + for Paypal +{ + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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 { + Ok(format!("{}v2/checkout/orders", self.base_url(connectors),)) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = paypal::PaypalPaymentsRequest::try_from(req)?; + let paypal_req = + utils::Encode::::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, 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 { + 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 { + self.get_order_error_response(res) + } +} + +impl + ConnectorIntegration< + CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + > for Paypal +{ +} + +impl ConnectorIntegration + for Paypal +{ + fn get_headers( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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 { + 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, 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 { + 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 { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Paypal +{ + fn get_headers( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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 { + 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, errors::ConnectorError> { + let connector_req = paypal::PaypalPaymentsCaptureRequest::try_from(req)?; + let paypal_req = + utils::Encode::::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, 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 { + 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 { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Paypal +{ + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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 { + 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, 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 { + 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 { + self.build_error_response(res) + } +} + +impl ConnectorIntegration for Paypal { + fn get_headers( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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, + connectors: &settings::Connectors, + ) -> CustomResult { + 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, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = paypal::PaypalRefundRequest::try_from(req)?; + let paypal_req = + utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(paypal_req)) + } + + fn build_request( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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, + res: Response, + ) -> CustomResult, 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 { + self.build_error_response(res) + } +} + +impl ConnectorIntegration for Paypal { + fn get_headers( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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 { + 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, 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 { + 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 { + 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 { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} + +impl services::ConnectorRedirectResponse for Paypal { + fn get_flow_type( + &self, + _query_params: &str, + _json_payload: Option, + _action: PaymentAction, + ) -> CustomResult { + Ok(payments::CallConnectorAction::Trigger) + } +} diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs new file mode 100644 index 0000000000..8a4ce0d3a3 --- /dev/null +++ b/crates/router/src/connector/paypal/transformers.rs @@ -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>, + postal_code: Option>, + country_code: api_models::enums::CountryCode, +} + +#[derive(Debug, Serialize)] +pub struct CardRequest { + billing_address: Option
, + expiry: Option>, + name: Secret, + number: Option>, + security_code: Option>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum PaymentSourceItem { + Card(CardRequest), +} + +#[derive(Debug, Serialize)] +pub struct PaypalPaymentsRequest { + intent: PaypalPaymentIntent, + purchase_units: Vec, + payment_source: Option, +} + +fn get_address_info( + payment_address: Option<&api_models::payments::Address>, +) -> Result, error_stack::Report> { + 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; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + 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; + fn try_from(item: &types::RefreshTokenRouterData) -> Result { + 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 TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + 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; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + 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, + id: String, + final_capture: Option, + status: PaypalOrderStatus, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct PaymentsCollection { + authorizations: Option>, + captures: Option>, +} + +#[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, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PaypalPaymentsSyncResponse { + id: String, + status: PaypalPaymentStatus, + amount: OrderAmount, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PaypalMeta { + pub authorize_id: Option, + pub order_id: String, + pub psync_flow: PaypalPaymentIntent, +} + +fn get_id_based_on_intent( + intent: &PaypalPaymentIntent, + purchase_unit: &PurchaseUnitItem, +) -> CustomResult { + || -> _ { + 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 + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + 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 + TryFrom< + types::ResponseRouterData, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PaypalPaymentsSyncResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + 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; + fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + 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, + final_capture: bool, +} + +impl From 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> + for types::PaymentsCaptureRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::PaymentsCaptureResponseRouterData, + ) -> Result { + 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, +} + +impl + TryFrom< + types::ResponseRouterData, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + PaypalPaymentsCancelResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + 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 TryFrom<&types::RefundsRouterData> for PaypalRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundsRouterData) -> Result { + 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 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, +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + 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> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + 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, + pub field: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct PaypalOrderErrorResponse { + pub name: String, + pub message: String, + pub debug_id: Option, + pub details: Option>, +} + +#[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, + pub details: Option>, +} + +#[derive(Deserialize, Debug)] +pub struct PaypalAccessTokenErrorResponse { + pub error: String, + pub error_description: String, +} diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index 9f7030c5dc..a28c6f2bcc 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -114,7 +114,7 @@ pub struct PaymentRequestCards { pub pan: Secret, pub cvv: Secret, #[serde(rename = "exp")] - pub expiry_date: String, + pub expiry_date: Secret, pub cardholder: Secret, pub reference: String, #[serde(rename = "redirectUrl")] diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index cc57764eea..f7eda87d08 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -247,7 +247,11 @@ pub enum CardIssuer { pub trait CardData { fn get_card_expiry_year_2_digit(&self) -> Secret; fn get_card_issuer(&self) -> Result; - 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; + fn get_expiry_date_as_yyyymm(&self, delimiter: &str) -> Secret; } 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 { 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 { + 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() + )) } } diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 6a51588e07..1fba3783b0 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -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 +); diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 5fa7a235ce..96c22dd57e 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -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> where - T: TryFrom>, + T: TryFrom>, types::RouterData: Feature, F: Clone, error_stack::Report: - From<>>::Error>, + From<>>::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 +pub struct PaymentAdditionalData<'a, F> where F: Clone, { router_base_url: String, connector_name: String, payment_data: PaymentData, + state: &'a AppState, } -impl TryFrom> for types::PaymentsAuthorizeData { +impl TryFrom> for types::PaymentsAuthorizeData { type Error = error_stack::Report; - fn try_from(additional_data: PaymentAdditionalData) -> Result { + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { 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 TryFrom> for types::PaymentsAuthorizeDat } } -impl TryFrom> for types::PaymentsSyncData { +impl TryFrom> for types::PaymentsSyncData { type Error = errors::ApiErrorResponse; - fn try_from(additional_data: PaymentAdditionalData) -> Result { + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { 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 TryFrom> for types::PaymentsSyncData { } } -impl TryFrom> for types::PaymentsCaptureData { +impl api::ConnectorTransactionId for Paypal { + fn connector_transaction_id( + &self, + payment_attempt: storage::PaymentAttempt, + ) -> Result, errors::ApiErrorResponse> { + let metadata = Self::connector_transaction_id(self, &payment_attempt.connector_metadata); + match metadata { + Ok(data) => Ok(data), + _ => Err(errors::ApiErrorResponse::ResourceIdNotFound), + } + } +} + +impl TryFrom> for types::PaymentsCaptureData { type Error = errors::ApiErrorResponse; - fn try_from(additional_data: PaymentAdditionalData) -> Result { + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { 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 TryFrom> 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 TryFrom> for types::PaymentsCancelData { +impl TryFrom> for types::PaymentsCancelData { type Error = errors::ApiErrorResponse; - fn try_from(additional_data: PaymentAdditionalData) -> Result { + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { 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 TryFrom> for types::PaymentsSessionData { +impl TryFrom> for types::PaymentsSessionData { type Error = error_stack::Report; - fn try_from(additional_data: PaymentAdditionalData) -> Result { + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { let payment_data = additional_data.payment_data; let parsed_metadata: Option = payment_data .payment_intent @@ -602,10 +633,10 @@ impl TryFrom> for types::PaymentsSessionData } } -impl TryFrom> for types::VerifyRequestData { +impl TryFrom> for types::VerifyRequestData { type Error = error_stack::Report; - fn try_from(additional_data: PaymentAdditionalData) -> Result { + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { let payment_data = additional_data.payment_data; Ok(Self { currency: payment_data.currency, @@ -622,10 +653,10 @@ impl TryFrom> for types::VerifyRequestData { } } -impl TryFrom> for types::CompleteAuthorizeData { +impl TryFrom> for types::CompleteAuthorizeData { type Error = error_stack::Report; - fn try_from(additional_data: PaymentAdditionalData) -> Result { + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { let payment_data = additional_data.payment_data; let browser_info: Option = payment_data .payment_attempt diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 654800c3c8..0eea03f2a7 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -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, 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: 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}"))) diff --git a/crates/router/tests/connectors/connector_auth.rs b/crates/router/tests/connectors/connector_auth.rs index 15d75ecd53..daa1772298 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -19,6 +19,7 @@ pub(crate) struct ConnectorAuthentication { pub mollie: Option, pub multisafepay: Option, pub nuvei: Option, + pub paypal: Option, pub payu: Option, pub rapyd: Option, pub shift4: Option, diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index 62b29ffe98..e314408125 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -15,6 +15,7 @@ mod globalpay; mod mollie; mod multisafepay; mod nuvei; +mod paypal; mod payu; mod rapyd; mod shift4; diff --git a/crates/router/tests/connectors/paypal.rs b/crates/router/tests/connectors/paypal.rs new file mode 100644 index 0000000000..15b52c9d34 --- /dev/null +++ b/crates/router/tests/connectors/paypal.rs @@ -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 { + 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 { + Some(utils::PaymentInfo { + access_token: get_access_token(), + ..Default::default() + }) +} + +fn get_payment_data() -> Option { + 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 diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index f12c63851b..2a2ad2d8ed 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -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" \ No newline at end of file diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 78688bf1a7..db92f2ead2 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -579,3 +579,17 @@ pub fn get_connector_transaction_id( Err(_) => None, } } + +pub fn get_connector_metadata( + response: Result, +) -> Option { + match response { + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: _, + redirection_data: _, + mandate_reference: _, + connector_metadata, + }) => connector_metadata, + _ => None, + } +} diff --git a/loadtest/config/Development.toml b/loadtest/config/Development.toml index d5c88c13f5..7406f43639 100644 --- a/loadtest/config/Development.toml +++ b/loadtest/config/Development.toml @@ -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",