From d4dbaadb06f74835235c0deb53835a8f97fa26b6 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Tue, 16 Apr 2024 15:54:06 +0530 Subject: [PATCH] feat(connector): integrate netcetera connector with pre authentication flow (#4293) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 1 + config/deployments/integration_test.toml | 1 + config/deployments/sandbox.toml | 1 + config/development.toml | 2 + config/docker_compose.toml | 2 + crates/api_models/src/admin.rs | 4 + crates/api_models/src/enums.rs | 4 + crates/common_utils/src/types.rs | 6 +- crates/connector_configs/src/connector.rs | 7 + crates/diesel_models/src/authentication.rs | 4 +- crates/router/src/configs/settings.rs | 1 + crates/router/src/connector.rs | 14 +- crates/router/src/connector/netcetera.rs | 325 ++++++++++++++++ .../src/connector/netcetera/transformers.rs | 285 ++++++++++++++ .../src/connector/square/transformers.rs | 4 +- crates/router/src/connector/threedsecureio.rs | 2 +- .../connector/threedsecureio/transformers.rs | 2 +- crates/router/src/core/admin.rs | 4 + crates/router/src/core/payments/flows.rs | 27 ++ crates/router/src/types.rs | 18 + crates/router/src/types/api.rs | 1 + crates/router/src/types/api/authentication.rs | 1 + crates/router/src/types/authentication.rs | 2 +- crates/router/src/types/transformers.rs | 5 + crates/router/tests/connectors/main.rs | 1 + crates/router/tests/connectors/netcetera.rs | 352 ++++++++++++++++++ .../router/tests/connectors/sample_auth.toml | 5 + crates/test_utils/src/connector_auth.rs | 1 + loadtest/config/development.toml | 2 + openapi/openapi_spec.json | 4 +- scripts/add_connector.sh | 2 +- 31 files changed, 1074 insertions(+), 16 deletions(-) create mode 100644 crates/router/src/connector/netcetera.rs create mode 100644 crates/router/src/connector/netcetera/transformers.rs create mode 100644 crates/router/tests/connectors/netcetera.rs diff --git a/config/config.example.toml b/config/config.example.toml index 602db5d838..e8f4025569 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -197,6 +197,7 @@ klarna.base_url = "https://api-na.playground.klarna.com/" mollie.base_url = "https://api.mollie.com/v2/" mollie.secondary_base_url = "https://api.cc.mollie.com/v1/" multisafepay.base_url = "https://testapi.multisafepay.com/" +netcetera.base_url = "https://{{merchant_endpoint_prefix}}.3ds-server.prev.netcetera-cloud-payment.ch" nexinets.base_url = "https://apitest.payengine.de/v1" nmi.base_url = "https://secure.nmi.com/" noon.base_url = "https://api-test.noonpayments.com/" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 7237fd7446..9ee70479d7 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -81,6 +81,7 @@ zen.base_url = "https://api.zen-test.com/" zen.secondary_base_url = "https://secure.zen-test.com/" zsl.base_url = "https://api.sitoffalb.net/" threedsecureio.base_url = "https://service.sandbox.3dsecure.io" +netcetera.base_url = "https://{{merchant_endpoint_prefix}}.3ds-server.prev.netcetera-cloud-payment.ch" [dummy_connector] enabled = true diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index d713380c4c..acec5bbadf 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -85,6 +85,7 @@ zen.base_url = "https://api.zen-test.com/" zen.secondary_base_url = "https://secure.zen-test.com/" zsl.base_url = "https://api.sitoffalb.net/" threedsecureio.base_url = "https://service.sandbox.3dsecure.io" +netcetera.base_url = "https://{{merchant_endpoint_prefix}}.3ds-server.prev.netcetera-cloud-payment.ch" [delayed_session_response] connectors_with_delayed_session_response = "trustpay,payme" diff --git a/config/development.toml b/config/development.toml index 5e9abe1974..4062ae6d9e 100644 --- a/config/development.toml +++ b/config/development.toml @@ -118,6 +118,7 @@ cards = [ "iatapay", "mollie", "multisafepay", + "netcetera", "nexinets", "nmi", "noon", @@ -195,6 +196,7 @@ klarna.base_url = "https://api-na.playground.klarna.com/" mollie.base_url = "https://api.mollie.com/v2/" mollie.secondary_base_url = "https://api.cc.mollie.com/v1/" multisafepay.base_url = "https://testapi.multisafepay.com/" +netcetera.base_url = "https://{{merchant_endpoint_prefix}}.3ds-server.prev.netcetera-cloud-payment.ch" nexinets.base_url = "https://apitest.payengine.de/v1" nmi.base_url = "https://secure.nmi.com/" noon.base_url = "https://api-test.noonpayments.com/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 079b5c0b16..27e5878de6 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -131,6 +131,7 @@ klarna.base_url = "https://api-na.playground.klarna.com/" mollie.base_url = "https://api.mollie.com/v2/" mollie.secondary_base_url = "https://api.cc.mollie.com/v1/" multisafepay.base_url = "https://testapi.multisafepay.com/" +netcetera.base_url = "https://{{merchant_endpoint_prefix}}.3ds-server.prev.netcetera-cloud-payment.ch" nexinets.base_url = "https://apitest.payengine.de/v1" nmi.base_url = "https://secure.nmi.com/" noon.base_url = "https://api-test.noonpayments.com/" @@ -201,6 +202,7 @@ cards = [ "iatapay", "mollie", "multisafepay", + "netcetera", "nexinets", "nmi", "noon", diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 623abca814..f7013005c9 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -505,6 +505,10 @@ pub enum ConnectorAuthType { CurrencyAuthKey { auth_key_map: HashMap, }, + CertificateAuth { + certificate: Secret, + private_key: Secret, + }, #[default] NoKey, } diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index a24dfa1731..12e1e876d1 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -100,6 +100,7 @@ pub enum Connector { Klarna, Mollie, Multisafepay, + Netcetera, Nexinets, Nmi, Noon, @@ -213,6 +214,7 @@ impl Connector { | Self::Plaid | Self::Riskified | Self::Threedsecureio + | Self::Netcetera | Self::Cybersource | Self::Noon | Self::Stripe => false, @@ -273,6 +275,7 @@ impl Connector { | Self::Threedsecureio | Self::Cybersource | Self::Noon + | Self::Netcetera | Self::Stripe => false, Self::Checkout => true, } @@ -296,6 +299,7 @@ impl Connector { #[strum(serialize_all = "snake_case")] pub enum AuthenticationConnectors { Threedsecureio, + Netcetera, } #[cfg(feature = "payouts")] diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index 418ce591fd..b15a519caa 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -157,7 +157,7 @@ pub enum Surcharge { } /// This struct lets us represent a semantic version type -#[derive(Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression)] +#[derive(Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, Ord, PartialOrd)] #[diesel(sql_type = Jsonb)] #[derive(serde::Serialize, serde::Deserialize)] pub struct SemanticVersion(#[serde(with = "Version")] Version); @@ -167,6 +167,10 @@ impl SemanticVersion { pub fn get_major(&self) -> u64 { self.0.major } + /// Constructs new SemanticVersion instance + pub fn new(major: u64, minor: u64, patch: u64) -> Self { + Self(Version::new(major, minor, patch)) + } } impl Display for SemanticVersion { diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index 63219edab7..d89516c0cc 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -55,6 +55,10 @@ pub enum ConnectorAuthType { CurrencyAuthKey { auth_key_map: HashMap, }, + CertificateAuth { + certificate: String, + private_key: String, + }, #[default] NoKey, } @@ -157,6 +161,7 @@ pub struct ConnectorConfig { pub signifyd: Option, pub trustpay: Option, pub threedsecureio: Option, + pub netcetera: Option, pub tsys: Option, pub volt: Option, #[cfg(feature = "payouts")] @@ -216,6 +221,7 @@ impl ConnectorConfig { let connector_data = Self::new()?; match connector { AuthenticationConnectors::Threedsecureio => Ok(connector_data.threedsecureio), + AuthenticationConnectors::Netcetera => Ok(connector_data.netcetera), } } @@ -293,6 +299,7 @@ impl ConnectorConfig { Connector::DummyConnector6 => Ok(connector_data.dummy_connector), #[cfg(feature = "dummy_connector")] Connector::DummyConnector7 => Ok(connector_data.paypal_test), + Connector::Netcetera => Ok(connector_data.netcetera), } } } diff --git a/crates/diesel_models/src/authentication.rs b/crates/diesel_models/src/authentication.rs index cc675a06ac..8e7aa5f2a9 100644 --- a/crates/diesel_models/src/authentication.rs +++ b/crates/diesel_models/src/authentication.rs @@ -90,7 +90,7 @@ pub enum AuthenticationUpdate { threeds_server_transaction_id: String, maximum_supported_3ds_version: common_utils::types::SemanticVersion, connector_authentication_id: String, - three_ds_method_data: String, + three_ds_method_data: Option, three_ds_method_url: Option, message_version: common_utils::types::SemanticVersion, connector_metadata: Option, @@ -310,7 +310,7 @@ impl From for AuthenticationUpdateInternal { threeds_server_transaction_id: Some(threeds_server_transaction_id), maximum_supported_version: Some(maximum_supported_3ds_version), connector_authentication_id: Some(connector_authentication_id), - three_ds_method_data: Some(three_ds_method_data), + three_ds_method_data, three_ds_method_url, message_version: Some(message_version), connector_metadata, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 74598f0a5c..810ab5b98e 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -511,6 +511,7 @@ pub struct Connectors { pub klarna: ConnectorParams, pub mollie: ConnectorParams, pub multisafepay: ConnectorParams, + pub netcetera: ConnectorParams, pub nexinets: ConnectorParams, pub nmi: ConnectorParams, pub noon: ConnectorParamsWithModeType, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index c7a6256d85..01c22651cd 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -28,6 +28,7 @@ pub mod iatapay; pub mod klarna; pub mod mollie; pub mod multisafepay; +pub mod netcetera; pub mod nexinets; pub mod nmi; pub mod noon; @@ -68,10 +69,11 @@ pub use self::{ checkout::Checkout, coinbase::Coinbase, cryptopay::Cryptopay, cybersource::Cybersource, dlocal::Dlocal, ebanx::Ebanx, fiserv::Fiserv, forte::Forte, globalpay::Globalpay, globepay::Globepay, gocardless::Gocardless, helcim::Helcim, iatapay::Iatapay, klarna::Klarna, - mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, nmi::Nmi, noon::Noon, - nuvei::Nuvei, opayo::Opayo, opennode::Opennode, payeezy::Payeezy, payme::Payme, paypal::Paypal, - payu::Payu, placetopay::Placetopay, powertranz::Powertranz, prophetpay::Prophetpay, - rapyd::Rapyd, riskified::Riskified, shift4::Shift4, signifyd::Signifyd, square::Square, - stax::Stax, stripe::Stripe, threedsecureio::Threedsecureio, trustpay::Trustpay, tsys::Tsys, - volt::Volt, wise::Wise, worldline::Worldline, worldpay::Worldpay, zen::Zen, zsl::Zsl, + mollie::Mollie, multisafepay::Multisafepay, netcetera::Netcetera, nexinets::Nexinets, nmi::Nmi, + noon::Noon, nuvei::Nuvei, opayo::Opayo, opennode::Opennode, payeezy::Payeezy, payme::Payme, + paypal::Paypal, payu::Payu, placetopay::Placetopay, powertranz::Powertranz, + prophetpay::Prophetpay, rapyd::Rapyd, riskified::Riskified, shift4::Shift4, signifyd::Signifyd, + square::Square, stax::Stax, stripe::Stripe, threedsecureio::Threedsecureio, trustpay::Trustpay, + tsys::Tsys, volt::Volt, wise::Wise, worldline::Worldline, worldpay::Worldpay, zen::Zen, + zsl::Zsl, }; diff --git a/crates/router/src/connector/netcetera.rs b/crates/router/src/connector/netcetera.rs new file mode 100644 index 0000000000..0dead96757 --- /dev/null +++ b/crates/router/src/connector/netcetera.rs @@ -0,0 +1,325 @@ +pub mod transformers; + +use std::fmt::Debug; + +use common_utils::request::RequestContent; +use error_stack::{report, ResultExt}; +use masking::ExposeInterface; +use transformers as netcetera; + +use crate::{ + configs::settings, + consts, + core::errors::{self, CustomResult}, + events::connector_api_logs::ConnectorEvent, + headers, + services::{self, request, ConnectorIntegration, ConnectorValidation}, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, + }, + utils::BytesExt, +}; + +#[derive(Debug, Clone)] +pub struct Netcetera; + +impl api::Payment for Netcetera {} +impl api::PaymentSession for Netcetera {} +impl api::ConnectorAccessToken for Netcetera {} +impl api::MandateSetup for Netcetera {} +impl api::PaymentAuthorize for Netcetera {} +impl api::PaymentSync for Netcetera {} +impl api::PaymentCapture for Netcetera {} +impl api::PaymentVoid for Netcetera {} +impl api::Refund for Netcetera {} +impl api::RefundExecute for Netcetera {} +impl api::RefundSync for Netcetera {} +impl api::PaymentToken for Netcetera {} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Netcetera +{ + // Not Implemented (R) +} + +impl ConnectorCommonExt for Netcetera +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(), + self.get_content_type().to_string().into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + +impl ConnectorCommon for Netcetera { + fn id(&self) -> &'static str { + "netcetera" + } + + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Minor + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.netcetera.base_url.as_ref() + } + + fn get_auth_header( + &self, + _auth_type: &types::ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + Ok(vec![]) + } + + fn build_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + let response: netcetera::NetceteraErrorResponse = res + .response + .parse_struct("NetceteraErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.error_details.error_code, + message: response + .error_details + .error_description + .unwrap_or(consts::NO_ERROR_MESSAGE.into()), + reason: response.error_details.error_detail, + attempt_status: None, + connector_transaction_id: None, + }) + } +} + +impl ConnectorValidation for Netcetera {} + +impl ConnectorIntegration + for Netcetera +{ + //TODO: implement sessions flow +} + +impl ConnectorIntegration + for Netcetera +{ +} + +impl + ConnectorIntegration< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for Netcetera +{ +} + +impl ConnectorIntegration + for Netcetera +{ +} + +impl ConnectorIntegration + for Netcetera +{ +} + +impl ConnectorIntegration + for Netcetera +{ +} + +impl ConnectorIntegration + for Netcetera +{ +} + +impl ConnectorIntegration + for Netcetera +{ +} + +impl ConnectorIntegration + for Netcetera +{ +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Netcetera { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } +} + +fn build_endpoint( + base_url: &str, + connector_metadata: &Option, +) -> CustomResult { + let metadata = netcetera::NetceteraMetaData::try_from(connector_metadata)?; + let endpoint_prefix = metadata.endpoint_prefix; + Ok(base_url.replace("{{merchant_endpoint_prefix}}", &endpoint_prefix)) +} + +impl api::ConnectorPreAuthentication for Netcetera {} +impl api::ExternalAuthentication for Netcetera {} +impl api::ConnectorAuthentication for Netcetera {} +impl api::ConnectorPostAuthentication for Netcetera {} + +impl + ConnectorIntegration< + api::PreAuthentication, + types::authentication::PreAuthNRequestData, + types::authentication::AuthenticationResponseData, + > for Netcetera +{ + fn get_headers( + &self, + req: &types::authentication::PreAuthNRouterData, + 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::authentication::PreAuthNRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let base_url = build_endpoint(self.base_url(connectors), &req.connector_meta_data)?; + Ok(format!("{}/3ds/versioning", base_url,)) + } + + fn get_request_body( + &self, + req: &types::authentication::PreAuthNRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = netcetera::NetceteraRouterData::try_from((0, req))?; + let req_obj = + netcetera::NetceteraPreAuthenticationRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &types::authentication::PreAuthNRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let netcetera_auth_type = netcetera::NetceteraAuthType::try_from(&req.connector_auth_type)?; + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url( + &types::authentication::ConnectorPreAuthenticationType::get_url( + self, req, connectors, + )?, + ) + .attach_default_headers() + .headers( + types::authentication::ConnectorPreAuthenticationType::get_headers( + self, req, connectors, + )?, + ) + .set_body( + types::authentication::ConnectorPreAuthenticationType::get_request_body( + self, req, connectors, + )?, + ) + .add_certificate(Some(netcetera_auth_type.certificate.expose())) + .add_certificate_key(Some(netcetera_auth_type.private_key.expose())) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::authentication::PreAuthNRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: netcetera::NetceteraPreAuthenticationResponse = res + .response + .parse_struct("netcetera NetceteraPreAuthenticationResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl + ConnectorIntegration< + api::Authentication, + types::authentication::ConnectorAuthenticationRequestData, + types::authentication::AuthenticationResponseData, + > for Netcetera +{ +} + +impl + ConnectorIntegration< + api::PostAuthentication, + types::authentication::ConnectorPostAuthenticationRequestData, + types::authentication::AuthenticationResponseData, + > for Netcetera +{ +} diff --git a/crates/router/src/connector/netcetera/transformers.rs b/crates/router/src/connector/netcetera/transformers.rs new file mode 100644 index 0000000000..61af655466 --- /dev/null +++ b/crates/router/src/connector/netcetera/transformers.rs @@ -0,0 +1,285 @@ +use error_stack::ResultExt; +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{ + connector::utils, + consts::NO_ERROR_MESSAGE, + core::errors, + types::{self, api}, +}; + +//TODO: Fill the struct with respective fields +pub struct NetceteraRouterData { + pub amount: i64, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for NetceteraRouterData +{ + type Error = error_stack::Report; + fn try_from( + (_currency_unit, _currency, amount, item): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + //Todo : use utils to convert the amount to the type of amount that a connector accepts + Ok(Self { + amount, + router_data: item, + }) + } +} + +impl TryFrom<(i64, T)> for NetceteraRouterData { + type Error = error_stack::Report; + fn try_from((amount, router_data): (i64, T)) -> Result { + Ok(Self { + amount, + router_data, + }) + } +} + +impl + TryFrom< + types::ResponseRouterData< + api::PreAuthentication, + NetceteraPreAuthenticationResponse, + types::authentication::PreAuthNRequestData, + types::authentication::AuthenticationResponseData, + >, + > for types::authentication::PreAuthNRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + api::PreAuthentication, + NetceteraPreAuthenticationResponse, + types::authentication::PreAuthNRequestData, + types::authentication::AuthenticationResponseData, + >, + ) -> Result { + let response = match item.response { + NetceteraPreAuthenticationResponse::Success(pre_authn_response) => { + // if card is not enrolled for 3ds, card_range will be None + let card_range = pre_authn_response.get_card_range_if_available(); + let maximum_supported_3ds_version = card_range + .as_ref() + .map(|card_range| card_range.highest_common_supported_version.clone()) + .unwrap_or_else(|| { + // Version "0.0.0" will be less that "2.0.0", hence we will treat this card as not eligible for 3ds authentication + common_utils::types::SemanticVersion::new(0, 0, 0) + }); + let three_ds_method_data = card_range.as_ref().and_then(|card_range| { + card_range + .three_ds_method_data_form + .as_ref() + .map(|data| data.three_ds_method_data.clone()) + }); + let three_ds_method_url = card_range + .as_ref() + .and_then(|card_range| card_range.get_three_ds_method_url()); + Ok( + types::authentication::AuthenticationResponseData::PreAuthNResponse { + threeds_server_transaction_id: pre_authn_response + .three_ds_server_trans_id + .clone(), + maximum_supported_3ds_version: maximum_supported_3ds_version.clone(), + connector_authentication_id: pre_authn_response.three_ds_server_trans_id, + three_ds_method_data, + three_ds_method_url, + message_version: maximum_supported_3ds_version, + connector_metadata: None, + }, + ) + } + NetceteraPreAuthenticationResponse::Failure(error_response) => { + Err(types::ErrorResponse { + code: error_response.error_details.error_code, + message: error_response + .error_details + .error_description + .clone() + .unwrap_or(NO_ERROR_MESSAGE.to_owned()), + reason: error_response.error_details.error_description, + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: None, + }) + } + }; + Ok(Self { + response, + ..item.data.clone() + }) + } +} + +pub struct NetceteraAuthType { + pub(super) certificate: Secret, + pub(super) private_key: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for NetceteraAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type.to_owned() { + types::ConnectorAuthType::CertificateAuth { + certificate, + private_key, + } => Ok(Self { + certificate, + private_key, + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NetceteraErrorResponse { + pub three_ds_server_trans_id: Option, + pub error_details: NetceteraErrorDetails, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NetceteraErrorDetails { + pub error_code: String, + pub error_component: Option, + pub error_description: Option, + pub error_detail: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NetceteraMetaData { + pub mcc: String, + pub merchant_country_code: String, + pub merchant_name: String, + pub endpoint_prefix: String, +} + +impl TryFrom<&Option> for NetceteraMetaData { + type Error = error_stack::Report; + fn try_from( + meta_data: &Option, + ) -> Result { + let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + Ok(metadata) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NetceteraPreAuthenticationRequest { + cardholder_account_number: cards::CardNumber, + scheme_id: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum SchemeId { + Visa, + Mastercard, + #[serde(rename = "JCB")] + Jcb, + #[serde(rename = "American Express")] + AmericanExpress, + Diners, + // For Cartes Bancaires and UnionPay, it is recommended to send the scheme ID + #[serde(rename = "CB")] + CartesBancaires, + UnionPay, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum NetceteraPreAuthenticationResponse { + Success(Box), + Failure(Box), +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NetceteraPreAuthenticationResponseData { + #[serde(rename = "threeDSServerTransID")] + pub three_ds_server_trans_id: String, + pub card_ranges: Vec, +} + +impl NetceteraPreAuthenticationResponseData { + pub fn get_card_range_if_available(&self) -> Option { + let card_range = self + .card_ranges + .iter() + .max_by_key(|card_range| &card_range.highest_common_supported_version); + card_range.cloned() + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CardRange { + pub scheme_id: SchemeId, + pub directory_server_id: Option, + pub acs_protocol_versions: Vec, + #[serde(rename = "threeDSMethodDataForm")] + pub three_ds_method_data_form: Option, + pub highest_common_supported_version: common_utils::types::SemanticVersion, +} + +impl CardRange { + pub fn get_three_ds_method_url(&self) -> Option { + self.acs_protocol_versions + .iter() + .find(|acs_protocol_version| { + acs_protocol_version.version == self.highest_common_supported_version + }) + .and_then(|acs_version| acs_version.three_ds_method_url.clone()) + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ThreeDSMethodDataForm { + // base64 encoded value for 3ds method data collection + #[serde(rename = "threeDSMethodData")] + pub three_ds_method_data: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AcsProtocolVersion { + pub version: common_utils::types::SemanticVersion, + #[serde(rename = "threeDSMethodURL")] + pub three_ds_method_url: Option, +} + +impl TryFrom<&NetceteraRouterData<&types::authentication::PreAuthNRouterData>> + for NetceteraPreAuthenticationRequest +{ + type Error = error_stack::Report; + + fn try_from( + value: &NetceteraRouterData<&types::authentication::PreAuthNRouterData>, + ) -> Result { + let router_data = value.router_data; + Ok(Self { + cardholder_account_number: router_data.request.card_holder_account_number.clone(), + scheme_id: None, + }) + } +} diff --git a/crates/router/src/connector/square/transformers.rs b/crates/router/src/connector/square/transformers.rs index 3cac6e80ce..318e33978c 100644 --- a/crates/router/src/connector/square/transformers.rs +++ b/crates/router/src/connector/square/transformers.rs @@ -313,13 +313,13 @@ impl TryFrom<&types::ConnectorAuthType> for SquareAuthType { api_key: api_key.to_owned(), key1: key1.to_owned(), }), - types::ConnectorAuthType::HeaderKey { .. } | types::ConnectorAuthType::SignatureKey { .. } | types::ConnectorAuthType::MultiAuthKey { .. } | types::ConnectorAuthType::CurrencyAuthKey { .. } | types::ConnectorAuthType::TemporaryAuth { .. } - | types::ConnectorAuthType::NoKey { .. } => { + | types::ConnectorAuthType::NoKey { .. } + | types::ConnectorAuthType::CertificateAuth { .. } => { Err(errors::ConnectorError::FailedToObtainAuthType.into()) } } diff --git a/crates/router/src/connector/threedsecureio.rs b/crates/router/src/connector/threedsecureio.rs index ecb8043359..77fda3e864 100644 --- a/crates/router/src/connector/threedsecureio.rs +++ b/crates/router/src/connector/threedsecureio.rs @@ -303,7 +303,7 @@ impl types::authentication::ConnectorAuthenticationRouterData, errors::ConnectorError, > { - let response = res + let response: threedsecureio::ThreedsecureioAuthenticationResponse = res .response .parse_struct("ThreedsecureioAuthenticationResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; diff --git a/crates/router/src/connector/threedsecureio/transformers.rs b/crates/router/src/connector/threedsecureio/transformers.rs index 04a2399b3b..99de7d7738 100644 --- a/crates/router/src/connector/threedsecureio/transformers.rs +++ b/crates/router/src/connector/threedsecureio/transformers.rs @@ -107,7 +107,7 @@ impl ) .change_context(errors::ConnectorError::ParsingFailed)?, connector_authentication_id: pre_authn_response.threeds_server_trans_id, - three_ds_method_data: three_ds_method_data_base64, + three_ds_method_data: Some(three_ds_method_data_base64), three_ds_method_url: pre_authn_response.threeds_method_url, message_version: common_utils::types::SemanticVersion::from_str( &pre_authn_response.acs_end_protocol_version, diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index c7e1e4118b..1ac9b93fe5 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1824,6 +1824,10 @@ pub(crate) fn validate_auth_and_metadata_type( multisafepay::transformers::MultisafepayAuthType::try_from(val)?; Ok(()) } + api_enums::Connector::Netcetera => { + netcetera::transformers::NetceteraAuthType::try_from(val)?; + Ok(()) + } api_enums::Connector::Nexinets => { nexinets::transformers::NexinetsAuthType::try_from(val)?; Ok(()) diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 3f8d56a372..03cba29d02 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -165,6 +165,7 @@ default_imp_for_complete_authorize!( connector::Iatapay, connector::Klarna, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Noon, connector::Opayo, @@ -243,6 +244,7 @@ default_imp_for_webhook_source_verification!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -328,6 +330,7 @@ default_imp_for_create_customer!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -407,6 +410,7 @@ default_imp_for_connector_redirect_response!( connector::Iatapay, connector::Klarna, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Opayo, connector::Opennode, @@ -470,6 +474,7 @@ default_imp_for_connector_request_id!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nmi, connector::Noon, connector::Nuvei, @@ -557,6 +562,7 @@ default_imp_for_accept_dispute!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -665,6 +671,7 @@ default_imp_for_file_upload!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -750,6 +757,7 @@ default_imp_for_submit_evidence!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -835,6 +843,7 @@ default_imp_for_defend_dispute!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -918,6 +927,7 @@ default_imp_for_pre_processing_steps!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Noon, connector::Nuvei, @@ -983,6 +993,7 @@ default_imp_for_payouts!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -1069,6 +1080,7 @@ default_imp_for_payouts_create!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -1158,6 +1170,7 @@ default_imp_for_payouts_eligibility!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -1244,6 +1257,7 @@ default_imp_for_payouts_fulfill!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -1330,6 +1344,7 @@ default_imp_for_payouts_cancel!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -1417,6 +1432,7 @@ default_imp_for_payouts_quote!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -1504,6 +1520,7 @@ default_imp_for_payouts_recipient!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -1590,6 +1607,7 @@ default_imp_for_approve!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -1677,6 +1695,7 @@ default_imp_for_reject!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -1748,6 +1767,7 @@ default_imp_for_fraud_check!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -1835,6 +1855,7 @@ default_imp_for_frm_sale!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -1922,6 +1943,7 @@ default_imp_for_frm_checkout!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -2009,6 +2031,7 @@ default_imp_for_frm_transaction!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -2096,6 +2119,7 @@ default_imp_for_frm_fulfillment!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -2183,6 +2207,7 @@ default_imp_for_frm_record_return!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -2267,6 +2292,7 @@ default_imp_for_incremental_authorization!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Noon, @@ -2351,6 +2377,7 @@ default_imp_for_revoking_mandates!( connector::Klarna, connector::Mollie, connector::Multisafepay, + connector::Netcetera, connector::Nexinets, connector::Nmi, connector::Nuvei, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 1bc8319235..dc43b673ec 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -1200,6 +1200,10 @@ pub enum ConnectorAuthType { CurrencyAuthKey { auth_key_map: HashMap, }, + CertificateAuth { + certificate: Secret, + private_key: Secret, + }, #[default] NoKey, } @@ -1238,6 +1242,13 @@ impl From for ConnectorAuthType { Self::CurrencyAuthKey { auth_key_map } } api_models::admin::ConnectorAuthType::NoKey => Self::NoKey, + api_models::admin::ConnectorAuthType::CertificateAuth { + certificate, + private_key, + } => Self::CertificateAuth { + certificate, + private_key, + }, } } } @@ -1272,6 +1283,13 @@ impl ForeignFrom for api_models::admin::ConnectorAuthType { Self::CurrencyAuthKey { auth_key_map } } ConnectorAuthType::NoKey => Self::NoKey, + ConnectorAuthType::CertificateAuth { + certificate, + private_key, + } => Self::CertificateAuth { + certificate, + private_key, + }, } } } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index dd7a082195..d38b46c6a7 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -379,6 +379,7 @@ impl ConnectorData { enums::Connector::Worldline => Ok(Box::new(&connector::Worldline)), enums::Connector::Worldpay => Ok(Box::new(&connector::Worldpay)), enums::Connector::Multisafepay => Ok(Box::new(&connector::Multisafepay)), + enums::Connector::Netcetera => Ok(Box::new(&connector::Netcetera)), enums::Connector::Nexinets => Ok(Box::new(&connector::Nexinets)), enums::Connector::Paypal => Ok(Box::new(&connector::Paypal)), enums::Connector::Trustpay => Ok(Box::new(&connector::Trustpay)), diff --git a/crates/router/src/types/api/authentication.rs b/crates/router/src/types/api/authentication.rs index b69533db5a..dbca3c1e68 100644 --- a/crates/router/src/types/api/authentication.rs +++ b/crates/router/src/types/api/authentication.rs @@ -107,6 +107,7 @@ impl AuthenticationConnectorData { enums::AuthenticationConnectors::Threedsecureio => { Ok(Box::new(&connector::Threedsecureio)) } + enums::AuthenticationConnectors::Netcetera => Ok(Box::new(&connector::Netcetera)), } } } diff --git a/crates/router/src/types/authentication.rs b/crates/router/src/types/authentication.rs index 1de7d110a6..367b96235a 100644 --- a/crates/router/src/types/authentication.rs +++ b/crates/router/src/types/authentication.rs @@ -14,7 +14,7 @@ pub enum AuthenticationResponseData { threeds_server_transaction_id: String, maximum_supported_3ds_version: common_utils::types::SemanticVersion, connector_authentication_id: String, - three_ds_method_data: String, + three_ds_method_data: Option, three_ds_method_url: Option, message_version: common_utils::types::SemanticVersion, connector_metadata: Option, diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 039f1cc303..d0cd2b7a11 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -209,6 +209,11 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Klarna => Self::Klarna, api_enums::Connector::Mollie => Self::Mollie, api_enums::Connector::Multisafepay => Self::Multisafepay, + api_enums::Connector::Netcetera => { + Err(common_utils::errors::ValidationError::InvalidValue { + message: "netcetera is not a routable connector".to_string(), + })? + } api_enums::Connector::Nexinets => Self::Nexinets, api_enums::Connector::Nmi => Self::Nmi, api_enums::Connector::Noon => Self::Noon, diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index 92921acfff..eb1c00a227 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -36,6 +36,7 @@ mod helcim; mod iatapay; mod mollie; mod multisafepay; +mod netcetera; mod nexinets; mod nmi; mod noon; diff --git a/crates/router/tests/connectors/netcetera.rs b/crates/router/tests/connectors/netcetera.rs new file mode 100644 index 0000000000..f06e2a0f5d --- /dev/null +++ b/crates/router/tests/connectors/netcetera.rs @@ -0,0 +1,352 @@ +use router::types::{self, storage::enums}; +use test_utils::connector_auth; + +use crate::utils::{self, ConnectorActions}; + +#[derive(Clone, Copy)] +struct NetceteraTest; +impl ConnectorActions for NetceteraTest {} +impl utils::Connector for NetceteraTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Netcetera; + types::api::ConnectorData { + connector: Box::new(&Netcetera), + connector_name: types::Connector::Netcetera, + get_token: types::api::GetToken::Connector, + merchant_connector_id: None, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + utils::to_connector_auth_type( + connector_auth::ConnectorAuthentication::new() + .netcetera + .expect("Missing connector authentication configuration") + .into(), + ) + } + + fn get_name(&self) -> String { + "netcetera".to_string() + } +} + +static CONNECTOR: NetceteraTest = NetceteraTest {}; + +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, + ); +} + +// 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/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index a1b1940fc8..a72326b903 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -204,6 +204,11 @@ api_key="API Key" api_key="API Key" +[netcetera] +certificate="Certificate" +private_key="Private Key" + + [ebanx] api_key="API Key" diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index 85114699e9..b5c18cfa5d 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -40,6 +40,7 @@ pub struct ConnectorAuthentication { pub iatapay: Option, pub mollie: Option, pub multisafepay: Option, + pub netcetera: Option, pub nexinets: Option, pub noon: Option, pub nmi: Option, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 66c1c855d3..799582c733 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -98,6 +98,7 @@ klarna.base_url = "https://api-na.playground.klarna.com/" mollie.base_url = "https://api.mollie.com/v2/" mollie.secondary_base_url = "https://api.cc.mollie.com/v1/" multisafepay.base_url = "https://testapi.multisafepay.com/" +netcetera.base_url = "https://{{merchant_endpoint_prefix}}.3ds-server.prev.netcetera-cloud-payment.ch" nexinets.base_url = "https://apitest.payengine.de/v1" nmi.base_url = "https://secure.nmi.com/" noon.base_url = "https://api-test.noonpayments.com/" @@ -167,6 +168,7 @@ cards = [ "iatapay", "mollie", "multisafepay", + "netcetera", "nexinets", "nmi", "noon", diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 57d14aa923..3dc53c0825 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -5125,7 +5125,8 @@ "AuthenticationConnectors": { "type": "string", "enum": [ - "threedsecureio" + "threedsecureio", + "netcetera" ] }, "AuthenticationStatus": { @@ -7156,6 +7157,7 @@ "klarna", "mollie", "multisafepay", + "netcetera", "nexinets", "nmi", "noon", diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 440a37a726..d8e789408b 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -6,7 +6,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 bankofamerica billwerk bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector ebanx fiserv forte globalpay globepay gocardless helcim iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu placetopay powertranz prophetpay rapyd shift4 square stax stripe threedsecureio trustpay tsys volt wise worldline worldpay zsl "$1") + connectors=(aci adyen airwallex applepay authorizedotnet bambora bankofamerica billwerk bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector ebanx fiserv forte globalpay globepay gocardless helcim iatapay klarna mollie multisafepay netcetera nexinets noon nuvei opayo opennode payeezy payme paypal payu placetopay powertranz prophetpay rapyd shift4 square stax stripe threedsecureio trustpay tsys volt wise worldline worldpay zsl "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res=`echo ${sorted[@]}` sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp