From e5cc0d9d45d41c391720ceb3f6c18151ac5a00f2 Mon Sep 17 00:00:00 2001 From: ThisIsMani <84711804+ThisIsMani@users.noreply.github.com> Date: Thu, 4 May 2023 19:29:59 +0530 Subject: [PATCH] feat(connector): add dummy connector template code (#970) --- config/config.example.toml | 1 + config/development.toml | 2 + config/docker_compose.toml | 2 + crates/api_models/Cargo.toml | 2 + crates/api_models/src/enums.rs | 8 + crates/router/Cargo.toml | 1 + crates/router/src/configs/settings.rs | 2 + crates/router/src/connector.rs | 4 + crates/router/src/connector/dummyconnector.rs | 507 ++++++++++++++++++ .../connector/dummyconnector/transformers.rs | 215 ++++++++ crates/router/src/core/payments/flows.rs | 24 + crates/router/src/types/api.rs | 2 + .../router/tests/connectors/connector_auth.rs | 2 + .../router/tests/connectors/dummyconnector.rs | 465 ++++++++++++++++ crates/router/tests/connectors/main.rs | 2 + .../router/tests/connectors/sample_auth.toml | 5 +- loadtest/config/development.toml | 2 + scripts/add_connector.sh | 2 +- 18 files changed, 1246 insertions(+), 2 deletions(-) create mode 100644 crates/router/src/connector/dummyconnector.rs create mode 100644 crates/router/src/connector/dummyconnector/transformers.rs create mode 100644 crates/router/tests/connectors/dummyconnector.rs diff --git a/config/config.example.toml b/config/config.example.toml index ad263904e5..e00730d62c 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -156,6 +156,7 @@ checkout.base_url = "https://api.sandbox.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" cybersource.base_url = "https://apitest.cybersource.com/" dlocal.base_url = "https://sandbox.dlocal.com/" +dummyconnector.base_url = "http://localhost:8080/dummy-connector" fiserv.base_url = "https://cert.api.fiservapps.com/" forte.base_url = "https://sandbox.forte.net/api/v3" globalpay.base_url = "https://apis.sandbox.globalpay.com/ucp/" diff --git a/config/development.toml b/config/development.toml index e5b4ca0090..ced0a7b4fd 100644 --- a/config/development.toml +++ b/config/development.toml @@ -62,6 +62,7 @@ cards = [ "coinbase", "cybersource", "dlocal", + "dummyconnector", "fiserv", "forte", "globalpay", @@ -107,6 +108,7 @@ checkout.base_url = "https://api.sandbox.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" cybersource.base_url = "https://apitest.cybersource.com/" dlocal.base_url = "https://sandbox.dlocal.com/" +dummyconnector.base_url = "http://localhost:8080/dummy-connector" fiserv.base_url = "https://cert.api.fiservapps.com/" forte.base_url = "https://sandbox.forte.net/api/v3" globalpay.base_url = "https://apis.sandbox.globalpay.com/ucp/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index a777cb6f40..6a6d8220a5 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -80,6 +80,7 @@ checkout.base_url = "https://api.sandbox.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" cybersource.base_url = "https://apitest.cybersource.com/" dlocal.base_url = "https://sandbox.dlocal.com/" +dummyconnector.base_url = "http://localhost:8080/dummy-connector" fiserv.base_url = "https://cert.api.fiservapps.com/" forte.base_url = "https://sandbox.forte.net/api/v3" globalpay.base_url = "https://apis.sandbox.globalpay.com/ucp/" @@ -117,6 +118,7 @@ cards = [ "coinbase", "cybersource", "dlocal", + "dummyconnector", "fiserv", "forte", "globalpay", diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index 76d53dafc8..f1824e41be 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -5,11 +5,13 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] +default = [] errors = [ "dep:actix-web", "dep:reqwest", ] multiple_mca = [] +dummy_connector = [] [dependencies] actix-web = { version = "4.3.1", optional = true } diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index fbee4ea058..41800ad197 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -592,6 +592,10 @@ pub enum Connector { Cybersource, #[default] Dummy, + #[cfg(feature = "dummy_connector")] + #[serde(rename = "dummyconnector")] + #[strum(serialize = "dummyconnector")] + DummyConnector, Opennode, Bambora, Dlocal, @@ -649,6 +653,10 @@ impl Connector { #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum RoutableConnectors { + #[cfg(feature = "dummy_connector")] + #[serde(rename = "dummyconnector")] + #[strum(serialize = "dummyconnector")] + DummyConnector, Aci, Adyen, Airwallex, diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index f21d82bd1c..b1c60d3035 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -24,6 +24,7 @@ accounts_cache = [] openapi = ["olap", "oltp"] vergen = ["router_env/vergen"] multiple_mca = ["api_models/multiple_mca"] +dummy_connector = [] [dependencies] diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 2a95d3b3fd..f246f9eb42 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -322,6 +322,8 @@ pub struct Connectors { pub coinbase: ConnectorParams, pub cybersource: ConnectorParams, pub dlocal: ConnectorParams, + #[cfg(feature = "dummy_connector")] + pub dummyconnector: ConnectorParams, pub fiserv: ConnectorParams, pub forte: ConnectorParams, pub globalpay: ConnectorParams, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 751d4b68a8..0683a8be33 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -9,6 +9,8 @@ pub mod checkout; pub mod coinbase; pub mod cybersource; pub mod dlocal; +#[cfg(feature = "dummy_connector")] +pub mod dummyconnector; pub mod fiserv; pub mod forte; pub mod globalpay; @@ -31,6 +33,8 @@ pub mod zen; pub mod mollie; +#[cfg(feature = "dummy_connector")] +pub use self::dummyconnector::DummyConnector; pub use self::{ aci::Aci, adyen::Adyen, airwallex::Airwallex, authorizedotnet::Authorizedotnet, bambora::Bambora, bluesnap::Bluesnap, braintree::Braintree, checkout::Checkout, diff --git a/crates/router/src/connector/dummyconnector.rs b/crates/router/src/connector/dummyconnector.rs new file mode 100644 index 0000000000..36d34b4d87 --- /dev/null +++ b/crates/router/src/connector/dummyconnector.rs @@ -0,0 +1,507 @@ +mod transformers; + +use std::fmt::Debug; + +use error_stack::{IntoReport, ResultExt}; +use transformers as dummyconnector; + +use crate::{ + configs::settings, + core::errors::{self, CustomResult}, + headers, + services::{self, ConnectorIntegration}, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct DummyConnector; + +impl api::Payment for DummyConnector {} +impl api::PaymentSession for DummyConnector {} +impl api::ConnectorAccessToken for DummyConnector {} +impl api::PreVerify for DummyConnector {} +impl api::PaymentAuthorize for DummyConnector {} +impl api::PaymentSync for DummyConnector {} +impl api::PaymentCapture for DummyConnector {} +impl api::PaymentVoid for DummyConnector {} +impl api::Refund for DummyConnector {} +impl api::RefundExecute for DummyConnector {} +impl api::RefundSync for DummyConnector {} +impl api::PaymentToken for DummyConnector {} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for DummyConnector +{ + // Not Implemented (R) +} + +impl ConnectorCommonExt for DummyConnector +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + types::PaymentsAuthorizeType::get_content_type(self).to_string(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + +impl ConnectorCommon for DummyConnector { + fn id(&self) -> &'static str { + "dummyconnector" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.dummyconnector.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult, errors::ConnectorError> { + let auth = dummyconnector::DummyConnectorAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![(headers::AUTHORIZATION.to_string(), auth.api_key)]) + } + + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: dummyconnector::DummyConnectorErrorResponse = res + .response + .parse_struct("DummyConnectorErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.code, + message: response.message, + reason: response.reason, + }) + } +} + +impl ConnectorIntegration + for DummyConnector +{ + //TODO: implement sessions flow +} + +impl ConnectorIntegration + for DummyConnector +{ +} + +impl ConnectorIntegration + for DummyConnector +{ +} + +impl ConnectorIntegration + for DummyConnector +{ + 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 { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = dummyconnector::DummyConnectorPaymentsRequest::try_from(req)?; + let dummyconnector_req = + utils::Encode::::encode_to_string_of_json( + &req_obj, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(dummyconnector_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, + )?) + .attach_default_headers() + .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: dummyconnector::DummyConnectorPaymentsResponse = res + .response + .parse_struct("DummyConnector PaymentsAuthorizeResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for DummyConnector +{ + 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 { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + 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)?) + .attach_default_headers() + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsSyncRouterData, + res: Response, + ) -> CustomResult { + let response: dummyconnector::DummyConnectorPaymentsResponse = res + .response + .parse_struct("dummyconnector PaymentsSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for DummyConnector +{ + 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 { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + _req: &types::PaymentsCaptureRouterData, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + } + + 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)?) + .attach_default_headers() + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCaptureRouterData, + res: Response, + ) -> CustomResult { + let response: dummyconnector::DummyConnectorPaymentsResponse = res + .response + .parse_struct("DummyConnector PaymentsCaptureResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for DummyConnector +{ +} + +impl ConnectorIntegration + for DummyConnector +{ + 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 { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = dummyconnector::DummyConnectorRefundRequest::try_from(req)?; + let dummyconnector_req = + utils::Encode::::encode_to_string_of_json( + &req_obj, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(dummyconnector_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)?) + .attach_default_headers() + .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: dummyconnector::RefundResponse = res + .response + .parse_struct("dummyconnector RefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for DummyConnector +{ + 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 { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + 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)?) + .attach_default_headers() + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .body(types::RefundSyncType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::RefundSyncRouterData, + res: Response, + ) -> CustomResult { + let response: dummyconnector::RefundResponse = res + .response + .parse_struct("dummyconnector RefundSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for DummyConnector { + 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() + } +} diff --git a/crates/router/src/connector/dummyconnector/transformers.rs b/crates/router/src/connector/dummyconnector/transformers.rs new file mode 100644 index 0000000000..e55664a6aa --- /dev/null +++ b/crates/router/src/connector/dummyconnector/transformers.rs @@ -0,0 +1,215 @@ +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{ + connector::utils::PaymentsAuthorizeRequestData, + core::errors, + types::{self, api, storage::enums}, +}; + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct DummyConnectorPaymentsRequest { + amount: i64, + card: DummyConnectorCard, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct DummyConnectorCard { + name: Secret, + number: Secret, + expiry_month: Secret, + expiry_year: Secret, + cvc: Secret, + complete: bool, +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for DummyConnectorPaymentsRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + match item.request.payment_method_data.clone() { + api::PaymentMethodData::Card(req_card) => { + let card = DummyConnectorCard { + name: req_card.card_holder_name, + number: req_card.card_number, + expiry_month: req_card.card_exp_month, + expiry_year: req_card.card_exp_year, + cvc: req_card.card_cvc, + complete: item.request.is_auto_capture()?, + }; + Ok(Self { + amount: item.request.amount, + card, + }) + } + _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + } + } +} + +//TODO: Fill the struct with respective fields +// Auth Struct +pub struct DummyConnectorAuthType { + pub(super) api_key: String, +} + +impl TryFrom<&types::ConnectorAuthType> for DummyConnectorAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { + api_key: api_key.to_string(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} +// PaymentsResponse +//TODO: Append the remaining status flags +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum DummyConnectorPaymentStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for enums::AttemptStatus { + fn from(item: DummyConnectorPaymentStatus) -> Self { + match item { + DummyConnectorPaymentStatus::Succeeded => Self::Charged, + DummyConnectorPaymentStatus::Failed => Self::Failure, + DummyConnectorPaymentStatus::Processing => Self::Authorizing, + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct DummyConnectorPaymentsResponse { + status: DummyConnectorPaymentStatus, + id: String, +} + +impl + TryFrom< + types::ResponseRouterData< + F, + DummyConnectorPaymentsResponse, + T, + types::PaymentsResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + DummyConnectorPaymentsResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + Ok(Self { + status: 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, + network_txn_id: None, + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +// REFUND : +// Type definition for RefundRequest +#[derive(Default, Debug, Serialize)] +pub struct DummyConnectorRefundRequest { + pub amount: i64, +} + +impl TryFrom<&types::RefundsRouterData> for DummyConnectorRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundsRouterData) -> Result { + Ok(Self { + amount: item.request.amount, + }) + } +} + +// Type definition for Refund Response + +#[allow(dead_code)] +#[derive(Debug, Serialize, Default, Deserialize, Clone)] +pub enum RefundStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for enums::RefundStatus { + fn from(item: RefundStatus) -> Self { + match item { + RefundStatus::Succeeded => Self::Success, + RefundStatus::Failed => Self::Failure, + RefundStatus::Processing => Self::Pending, + //TODO: Review mapping + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct RefundResponse { + 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.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +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.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct DummyConnectorErrorResponse { + pub status_code: u16, + pub code: String, + pub message: String, + pub reason: Option, +} diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index ea5f7ad3dc..27b69dcffe 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -100,6 +100,9 @@ macro_rules! default_imp_for_complete_authorize{ }; } +#[cfg(feature = "dummy_connector")] +default_imp_for_complete_authorize!(connector::DummyConnector); + default_imp_for_complete_authorize!( connector::Aci, connector::Adyen, @@ -141,6 +144,9 @@ macro_rules! default_imp_for_create_customer{ }; } +#[cfg(feature = "dummy_connector")] +default_imp_for_create_customer!(connector::DummyConnector); + default_imp_for_create_customer!( connector::Aci, connector::Adyen, @@ -190,6 +196,9 @@ macro_rules! default_imp_for_connector_redirect_response{ }; } +#[cfg(feature = "dummy_connector")] +default_imp_for_connector_redirect_response!(connector::DummyConnector); + default_imp_for_connector_redirect_response!( connector::Aci, connector::Adyen, @@ -221,6 +230,9 @@ macro_rules! default_imp_for_connector_request_id{ }; } +#[cfg(feature = "dummy_connector")] +default_imp_for_connector_request_id!(connector::DummyConnector); + default_imp_for_connector_request_id!( connector::Aci, connector::Adyen, @@ -268,6 +280,9 @@ macro_rules! default_imp_for_accept_dispute{ }; } +#[cfg(feature = "dummy_connector")] +default_imp_for_accept_dispute!(connector::DummyConnector); + default_imp_for_accept_dispute!( connector::Aci, connector::Adyen, @@ -316,6 +331,9 @@ macro_rules! default_imp_for_file_upload{ }; } +#[cfg(feature = "dummy_connector")] +default_imp_for_file_upload!(connector::DummyConnector); + default_imp_for_file_upload!( connector::Aci, connector::Adyen, @@ -362,6 +380,9 @@ macro_rules! default_imp_for_submit_evidence{ }; } +#[cfg(feature = "dummy_connector")] +default_imp_for_submit_evidence!(connector::DummyConnector); + default_imp_for_submit_evidence!( connector::Aci, connector::Adyen, @@ -408,6 +429,9 @@ macro_rules! default_imp_for_defend_dispute{ }; } +#[cfg(feature = "dummy_connector")] +default_imp_for_defend_dispute!(connector::DummyConnector); + default_imp_for_defend_dispute!( connector::Aci, connector::Adyen, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index f84436112d..cdb87d673c 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -203,6 +203,8 @@ impl ConnectorData { "coinbase" => Ok(Box::new(&connector::Coinbase)), "cybersource" => Ok(Box::new(&connector::Cybersource)), "dlocal" => Ok(Box::new(&connector::Dlocal)), + #[cfg(feature = "dummy_connector")] + "dummyconnector" => Ok(Box::new(&connector::DummyConnector)), "fiserv" => Ok(Box::new(&connector::Fiserv)), "forte" => Ok(Box::new(&connector::Forte)), "globalpay" => Ok(Box::new(&connector::Globalpay)), diff --git a/crates/router/tests/connectors/connector_auth.rs b/crates/router/tests/connectors/connector_auth.rs index 3e83c00b9c..848dc9c22a 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -15,6 +15,8 @@ pub(crate) struct ConnectorAuthentication { pub coinbase: Option, pub cybersource: Option, pub dlocal: Option, + #[cfg(feature = "dummy_connector")] + pub dummyconnector: Option, pub fiserv: Option, pub forte: Option, pub globalpay: Option, diff --git a/crates/router/tests/connectors/dummyconnector.rs b/crates/router/tests/connectors/dummyconnector.rs new file mode 100644 index 0000000000..ba422137a3 --- /dev/null +++ b/crates/router/tests/connectors/dummyconnector.rs @@ -0,0 +1,465 @@ +use masking::Secret; +use router::types::{self, api, storage::enums}; + +use crate::{ + connector_auth, + utils::{self, ConnectorActions}, +}; + +#[derive(Clone, Copy)] +struct DummyConnectorTest; +impl ConnectorActions for DummyConnectorTest {} +impl utils::Connector for DummyConnectorTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::DummyConnector; + types::api::ConnectorData { + connector: Box::new(&DummyConnector), + connector_name: types::Connector::DummyConnector, + get_token: types::api::GetToken::Connector, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + types::ConnectorAuthType::from( + connector_auth::ConnectorAuthentication::new() + .dummyconnector + .expect("Missing connector authentication configuration"), + ) + } + + fn get_name(&self) -> String { + "dummyconnector".to_string() + } +} + +static CONNECTOR: DummyConnectorTest = DummyConnectorTest {}; + +fn get_default_payment_info() -> Option { + None +} + +fn payment_method_details() -> Option { + None +} + +// 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(payment_method_details(), 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 response = CONNECTOR + .authorize_and_capture_payment(payment_method_details(), None, 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 response = CONNECTOR + .authorize_and_capture_payment( + payment_method_details(), + Some(types::PaymentsCaptureData { + 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(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(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.unwrap(), + ), + ..Default::default() + }), + 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 response = CONNECTOR + .authorize_and_void_payment( + payment_method_details(), + Some(types::PaymentsCancelData { + connector_transaction_id: String::from(""), + cancellation_reason: Some("requested_by_customer".to_string()), + ..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 response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + 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 response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + 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 refund_response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + 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, + ); +} + +// Creates a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), 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(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + capture_method: Some(enums::CaptureMethod::Automatic), + ..Default::default() + }), + 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(payment_method_details(), 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 refund_response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + 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() { + CONNECTOR + .make_payment_and_multiple_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await; +} + +// 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(payment_method_details(), 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, + "Your card number is incorrect.".to_string(), + ); +} + +// 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, + "You passed an empty string for 'payment_method_data[card][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, + "Your card's security code is invalid.".to_string(), + ); +} + +// 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, + "Your card's expiration month is invalid.".to_string(), + ); +} + +// 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, + "Your card's expiration year is invalid.".to_string(), + ); +} + +// 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 + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let void_response = CONNECTOR + .void_payment(txn_id.unwrap(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + void_response.response.unwrap_err().message, + "You cannot cancel this PaymentIntent because it has a status of succeeded." + ); +} + +// Captures a payment using invalid connector payment id. +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + capture_response.response.unwrap_err().message, + String::from("No such payment_intent: '123456789'") + ); +} + +// 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 response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 150, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Refund amount (₹1.50) is greater than charge amount (₹1.00)", + ); +} + +// 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/main.rs b/crates/router/tests/connectors/main.rs index 2ab6351d2b..607ca09748 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -11,6 +11,8 @@ mod coinbase; mod connector_auth; mod cybersource; mod dlocal; +#[cfg(feature = "dummy_connector")] +mod dummyconnector; mod fiserv; mod forte; mod globalpay; diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 73311e860c..36187d09cd 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -95,4 +95,7 @@ key1 = "key1" [payeezy] api_key = "api_key" key1 = "key1" -api_secret = "secret" \ No newline at end of file +api_secret = "secret" + +[dummyconnector] +api_key="API Key" diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 9547a10630..b4055c37b0 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -69,6 +69,7 @@ checkout.base_url = "https://api.sandbox.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" cybersource.base_url = "https://apitest.cybersource.com/" dlocal.base_url = "https://sandbox.dlocal.com/" +dummyconnector.base_url = "http://localhost:8080/dummy-connector" fiserv.base_url = "https://cert.api.fiservapps.com/" forte.base_url = "https://sandbox.forte.net/api/v3" globalpay.base_url = "https://apis.sandbox.globalpay.com/ucp/" @@ -105,6 +106,7 @@ cards = [ "coinbase", "cybersource", "dlocal", + "dummyconnector", "fiserv", "forte", "globalpay", diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 77dd82cc39..5f3c23f30f 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -4,7 +4,7 @@ function find_prev_connector() { git checkout $self cp $self $self.tmp # add new connector to existing list and sort it - connectors=(aci adyen airwallex applepay authorizedotnet bambora bluesnap braintree checkout coinbase cybersource dlocal fiserv forte globalpay klarna mollie multisafepay nexinets nuvei opennode paypal payeezy payu rapyd shift4 stripe trustpay worldline worldpay "$1") + connectors=(aci adyen airwallex applepay authorizedotnet bambora bluesnap braintree checkout coinbase cybersource dlocal dummyconnector fiserv forte globalpay klarna mollie multisafepay nexinets nuvei opennode payeezy paypal payu rapyd shift4 stripe trustpay worldline worldpay "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res=`echo ${sorted[@]}` sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp