From 68876811a8817cdec09be407fbbbbf7f19992565 Mon Sep 17 00:00:00 2001 From: awasthi21 <107559116+awasthi21@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:42:51 +0530 Subject: [PATCH] feat(connector): [Elavon] Implement cards Flow (#6485) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .typos.toml | 1 + Cargo.lock | 1 + api-reference-v2/openapi_spec.json | 2 + api-reference/openapi_spec.json | 2 + config/config.example.toml | 2 +- config/deployments/integration_test.toml | 2 +- config/deployments/production.toml | 2 +- config/deployments/sandbox.toml | 2 +- config/development.toml | 2 +- config/docker_compose.toml | 2 +- crates/api_models/src/connector_enums.rs | 2 + crates/common_enums/src/connector_enums.rs | 1 + crates/connector_configs/src/connector.rs | 2 + .../connector_configs/toml/development.toml | 44 +- crates/connector_configs/toml/production.toml | 44 +- crates/connector_configs/toml/sandbox.toml | 44 +- crates/hyperswitch_connectors/Cargo.toml | 1 + .../src/connectors/elavon.rs | 226 ++++--- .../src/connectors/elavon/transformers.rs | 631 ++++++++++++++---- crates/hyperswitch_connectors/src/utils.rs | 17 + .../src/router_request_types.rs | 1 + .../payment_connector_required_fields.rs | 114 ++++ crates/router/src/core/admin.rs | 4 + crates/router/src/core/utils.rs | 1 + crates/router/src/types/api.rs | 4 +- crates/router/src/types/transformers.rs | 2 +- crates/router/tests/connectors/utils.rs | 2 + .../cypress/e2e/PaymentUtils/Elavon.js | 571 ++++++++++++++++ .../cypress/e2e/PaymentUtils/Utils.js | 2 + loadtest/config/development.toml | 2 +- 30 files changed, 1498 insertions(+), 235 deletions(-) create mode 100644 cypress-tests/cypress/e2e/PaymentUtils/Elavon.js diff --git a/.typos.toml b/.typos.toml index 109e411884a8..983ead3f75d6 100644 --- a/.typos.toml +++ b/.typos.toml @@ -19,6 +19,7 @@ HypoNoeLbFurNiederosterreichUWien = "HypoNoeLbFurNiederosterreichUWien" hypo_noe_lb_fur_niederosterreich_u_wien = "hypo_noe_lb_fur_niederosterreich_u_wien" IOT = "IOT" # British Indian Ocean Territory country code klick = "klick" # Swedish word for clicks +FPR = "FPR" # Fraud Prevention Rules LSO = "LSO" # Lesotho country code NAM = "NAM" # Namibia country code ND = "ND" # North Dakota state code diff --git a/Cargo.lock b/Cargo.lock index a2ede8c08c10..4118ea29c2da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4048,6 +4048,7 @@ dependencies = [ "mime", "once_cell", "qrcode", + "quick-xml", "rand", "regex", "reqwest 0.11.27", diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 2a5b1cfa3107..1494908c5fcd 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -5911,6 +5911,7 @@ "digitalvirgo", "dlocal", "ebanx", + "elavon", "fiserv", "fiservemea", "fiuu", @@ -18123,6 +18124,7 @@ "digitalvirgo", "dlocal", "ebanx", + "elavon", "fiserv", "fiservemea", "fiuu", diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index e029bef94534..f624b4f68dda 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -8743,6 +8743,7 @@ "digitalvirgo", "dlocal", "ebanx", + "elavon", "fiserv", "fiservemea", "fiuu", @@ -22803,6 +22804,7 @@ "digitalvirgo", "dlocal", "ebanx", + "elavon", "fiserv", "fiservemea", "fiuu", diff --git a/config/config.example.toml b/config/config.example.toml index 08a32035c4b7..e55665ffc708 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -207,7 +207,7 @@ digitalvirgo.base_url = "https://dcb-integration-service-sandbox-external.stagin dlocal.base_url = "https://sandbox.dlocal.com/" dummyconnector.base_url = "http://localhost:8080/dummy-connector" ebanx.base_url = "https://sandbox.ebanxpay.com/" -elavon.base_url = "https://api.demo.convergepay.com" +elavon.base_url = "https://api.demo.convergepay.com/VirtualMerchantDemo/" fiserv.base_url = "https://cert.api.fiservapps.com/" fiservemea.base_url = "https://prod.emea.api.fiservapps.com/sandbox" fiuu.base_url = "https://sandbox.merchant.razer.com/" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index e1ea99bc8d11..5f4de111ac8c 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -49,7 +49,7 @@ digitalvirgo.base_url = "https://dcb-integration-service-sandbox-external.stagin dlocal.base_url = "https://sandbox.dlocal.com/" dummyconnector.base_url = "http://localhost:8080/dummy-connector" ebanx.base_url = "https://sandbox.ebanxpay.com/" -elavon.base_url = "https://api.demo.convergepay.com" +elavon.base_url = "https://api.demo.convergepay.com/VirtualMerchantDemo/" fiserv.base_url = "https://cert.api.fiservapps.com/" fiservemea.base_url = "https://prod.emea.api.fiservapps.com/sandbox" fiuu.base_url = "https://sandbox.merchant.razer.com/" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index e5f906460584..61738f04cdb2 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -53,7 +53,7 @@ digitalvirgo.base_url = "https://dcb-integration-service-sandbox-external.stagin dlocal.base_url = "https://sandbox.dlocal.com/" dummyconnector.base_url = "http://localhost:8080/dummy-connector" ebanx.base_url = "https://sandbox.ebanxpay.com/" -elavon.base_url = "https://api.convergepay.com" +elavon.base_url = "https://api.convergepay.com/VirtualMerchant/" fiserv.base_url = "https://cert.api.fiservapps.com/" fiservemea.base_url = "https://prod.emea.api.fiservapps.com" fiuu.base_url = "https://pay.merchant.razer.com/" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 4421d1b1e967..24c7642200c9 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -53,7 +53,7 @@ digitalvirgo.base_url = "https://dcb-integration-service-sandbox-external.stagin dlocal.base_url = "https://sandbox.dlocal.com/" dummyconnector.base_url = "http://localhost:8080/dummy-connector" ebanx.base_url = "https://sandbox.ebanxpay.com/" -elavon.base_url = "https://api.demo.convergepay.com" +elavon.base_url = "https://api.demo.convergepay.com/VirtualMerchantDemo/" fiserv.base_url = "https://cert.api.fiservapps.com/" fiservemea.base_url = "https://prod.emea.api.fiservapps.com/sandbox" fiuu.base_url = "https://sandbox.merchant.razer.com/" diff --git a/config/development.toml b/config/development.toml index a58497f4a51e..630cc4a2303c 100644 --- a/config/development.toml +++ b/config/development.toml @@ -222,7 +222,7 @@ digitalvirgo.base_url = "https://dcb-integration-service-sandbox-external.stagin dlocal.base_url = "https://sandbox.dlocal.com/" dummyconnector.base_url = "http://localhost:8080/dummy-connector" ebanx.base_url = "https://sandbox.ebanxpay.com/" -elavon.base_url = "https://api.demo.convergepay.com" +elavon.base_url = "https://api.demo.convergepay.com/VirtualMerchantDemo/" fiserv.base_url = "https://cert.api.fiservapps.com/" fiservemea.base_url = "https://prod.emea.api.fiservapps.com/sandbox" fiuu.base_url = "https://sandbox.merchant.razer.com/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index c293a7ac821e..354af98dbadf 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -137,7 +137,7 @@ digitalvirgo.base_url = "https://dcb-integration-service-sandbox-external.stagin dlocal.base_url = "https://sandbox.dlocal.com/" dummyconnector.base_url = "http://localhost:8080/dummy-connector" ebanx.base_url = "https://sandbox.ebanxpay.com/" -elavon.base_url = "https://api.demo.convergepay.com" +elavon.base_url = "https://api.demo.convergepay.com/VirtualMerchantDemo/" fiserv.base_url = "https://cert.api.fiservapps.com/" fiservemea.base_url = "https://prod.emea.api.fiservapps.com/sandbox" fiuu.base_url = "https://sandbox.merchant.razer.com/" diff --git a/crates/api_models/src/connector_enums.rs b/crates/api_models/src/connector_enums.rs index 11b384009203..fe2c4f037bf5 100644 --- a/crates/api_models/src/connector_enums.rs +++ b/crates/api_models/src/connector_enums.rs @@ -74,6 +74,7 @@ pub enum Connector { Digitalvirgo, Dlocal, Ebanx, + Elavon, Fiserv, Fiservemea, Fiuu, @@ -216,6 +217,7 @@ impl Connector { | Self::Digitalvirgo | Self::Dlocal | Self::Ebanx + | Self::Elavon | Self::Fiserv | Self::Fiservemea | Self::Fiuu diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index e75a4038c077..9d17cdb61d4c 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -71,6 +71,7 @@ pub enum RoutableConnectors { Digitalvirgo, Dlocal, Ebanx, + Elavon, Fiserv, Fiservemea, Fiuu, diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index b1cf0bd9f9c7..65b286f8423c 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -182,6 +182,7 @@ pub struct ConnectorConfig { pub digitalvirgo: Option, pub dlocal: Option, pub ebanx_payout: Option, + pub elavon: Option, pub fiserv: Option, pub fiservemea: Option, pub fiuu: Option, @@ -346,6 +347,7 @@ impl ConnectorConfig { Connector::Digitalvirgo => Ok(connector_data.digitalvirgo), Connector::Dlocal => Ok(connector_data.dlocal), Connector::Ebanx => Ok(connector_data.ebanx_payout), + Connector::Elavon => Ok(connector_data.elavon), Connector::Fiserv => Ok(connector_data.fiserv), Connector::Fiservemea => Ok(connector_data.fiservemea), Connector::Fiuu => Ok(connector_data.fiuu), diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 5d41efc1a50f..8ed44df888f0 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -4312,4 +4312,46 @@ type="Radio" options=["Hyperswitch"] [fiuu.connector_webhook_details] -merchant_secret="Source verification key" \ No newline at end of file +merchant_secret="Source verification key" + +[elavon] +[[elavon.credit]] + payment_method_type = "Mastercard" +[[elavon.credit]] + payment_method_type = "Visa" +[[elavon.credit]] + payment_method_type = "Interac" +[[elavon.credit]] + payment_method_type = "AmericanExpress" +[[elavon.credit]] + payment_method_type = "JCB" +[[elavon.credit]] + payment_method_type = "DinersClub" +[[elavon.credit]] + payment_method_type = "Discover" +[[elavon.credit]] + payment_method_type = "CartesBancaires" +[[elavon.credit]] + payment_method_type = "UnionPay" +[[elavon.debit]] + payment_method_type = "Mastercard" +[[elavon.debit]] + payment_method_type = "Visa" +[[elavon.debit]] + payment_method_type = "Interac" +[[elavon.debit]] + payment_method_type = "AmericanExpress" +[[elavon.debit]] + payment_method_type = "JCB" +[[elavon.debit]] + payment_method_type = "DinersClub" +[[elavon.debit]] + payment_method_type = "Discover" +[[elavon.debit]] + payment_method_type = "CartesBancaires" +[[elavon.debit]] + payment_method_type = "UnionPay" +[elavon.connector_auth.SignatureKey] +api_key="Account Id" +key1="User ID" +api_secret="Pin" \ No newline at end of file diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index cc501a4c101e..887a1398b48f 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -3270,4 +3270,46 @@ type="Radio" options=["Hyperswitch"] [fiuu.connector_webhook_details] -merchant_secret="Source verification key" \ No newline at end of file +merchant_secret="Source verification key" + +[elavon] +[[elavon.credit]] + payment_method_type = "Mastercard" +[[elavon.credit]] + payment_method_type = "Visa" +[[elavon.credit]] + payment_method_type = "Interac" +[[elavon.credit]] + payment_method_type = "AmericanExpress" +[[elavon.credit]] + payment_method_type = "JCB" +[[elavon.credit]] + payment_method_type = "DinersClub" +[[elavon.credit]] + payment_method_type = "Discover" +[[elavon.credit]] + payment_method_type = "CartesBancaires" +[[elavon.credit]] + payment_method_type = "UnionPay" +[[elavon.debit]] + payment_method_type = "Mastercard" +[[elavon.debit]] + payment_method_type = "Visa" +[[elavon.debit]] + payment_method_type = "Interac" +[[elavon.debit]] + payment_method_type = "AmericanExpress" +[[elavon.debit]] + payment_method_type = "JCB" +[[elavon.debit]] + payment_method_type = "DinersClub" +[[elavon.debit]] + payment_method_type = "Discover" +[[elavon.debit]] + payment_method_type = "CartesBancaires" +[[elavon.debit]] + payment_method_type = "UnionPay" +[elavon.connector_auth.SignatureKey] +api_key="Account Id" +key1="User ID" +api_secret="Pin" \ No newline at end of file diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 5462c121c371..57cb91050422 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -4276,4 +4276,46 @@ type="Radio" options=["Hyperswitch"] [fiuu.connector_webhook_details] -merchant_secret="Source verification key" \ No newline at end of file +merchant_secret="Source verification key" + +[elavon] +[[elavon.credit]] + payment_method_type = "Mastercard" +[[elavon.credit]] + payment_method_type = "Visa" +[[elavon.credit]] + payment_method_type = "Interac" +[[elavon.credit]] + payment_method_type = "AmericanExpress" +[[elavon.credit]] + payment_method_type = "JCB" +[[elavon.credit]] + payment_method_type = "DinersClub" +[[elavon.credit]] + payment_method_type = "Discover" +[[elavon.credit]] + payment_method_type = "CartesBancaires" +[[elavon.credit]] + payment_method_type = "UnionPay" +[[elavon.debit]] + payment_method_type = "Mastercard" +[[elavon.debit]] + payment_method_type = "Visa" +[[elavon.debit]] + payment_method_type = "Interac" +[[elavon.debit]] + payment_method_type = "AmericanExpress" +[[elavon.debit]] + payment_method_type = "JCB" +[[elavon.debit]] + payment_method_type = "DinersClub" +[[elavon.debit]] + payment_method_type = "Discover" +[[elavon.debit]] + payment_method_type = "CartesBancaires" +[[elavon.debit]] + payment_method_type = "UnionPay" +[elavon.connector_auth.SignatureKey] +api_key="Account Id" +key1="User ID" +api_secret="Pin" \ No newline at end of file diff --git a/crates/hyperswitch_connectors/Cargo.toml b/crates/hyperswitch_connectors/Cargo.toml index ca9d338920f8..5f4def7587aa 100644 --- a/crates/hyperswitch_connectors/Cargo.toml +++ b/crates/hyperswitch_connectors/Cargo.toml @@ -23,6 +23,7 @@ image = { version = "0.25.1", default-features = false, features = ["png"] } mime = "0.3.17" once_cell = "1.19.0" qrcode = "0.14.0" +quick-xml = { version = "0.31.0", features = ["serialize"] } rand = "0.8.5" regex = "1.10.4" reqwest = { version = "0.11.27" } diff --git a/crates/hyperswitch_connectors/src/connectors/elavon.rs b/crates/hyperswitch_connectors/src/connectors/elavon.rs index 7e4408d301fa..b236e3a604c0 100644 --- a/crates/hyperswitch_connectors/src/connectors/elavon.rs +++ b/crates/hyperswitch_connectors/src/connectors/elavon.rs @@ -1,14 +1,16 @@ pub mod transformers; +use std::{collections::HashMap, str}; +use common_enums::{CaptureMethod, PaymentMethodType}; use common_utils::{ errors::CustomResult, - ext_traits::BytesExt, request::{Method, Request, RequestBuilder, RequestContent}, - types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, + types::{AmountConvertor, StringMajorUnit, StringMajorUnitForConnector}, }; -use error_stack::{report, ResultExt}; +use error_stack::report; use hyperswitch_domain_models::{ - router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, + payment_method_data::PaymentMethodData, + router_data::{AccessToken, ErrorResponse, RouterData}, router_flow_types::{ access_token_auth::AccessTokenAuth, payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void}, @@ -21,8 +23,8 @@ use hyperswitch_domain_models::{ }, router_response_types::{PaymentsResponseData, RefundsResponseData}, types::{ - PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, - RefundSyncRouterData, RefundsRouterData, + PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, + PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData, }, }; use hyperswitch_interfaces::{ @@ -33,20 +35,40 @@ use hyperswitch_interfaces::{ types::{self, Response}, webhooks, }; -use masking::{ExposeInterface, Mask}; +use masking::{Secret, WithoutType}; +use serde::Serialize; use transformers as elavon; -use crate::{constants::headers, types::ResponseRouterData, utils}; +use crate::{ + constants::headers, + types::ResponseRouterData, + utils::{self, PaymentMethodDataType}, +}; +pub fn struct_to_xml( + item: &T, +) -> Result>, errors::ConnectorError> { + let xml_content = quick_xml::se::to_string_with_root("txn", &item).map_err(|e| { + router_env::logger::error!("Error serializing Struct: {:?}", e); + errors::ConnectorError::ResponseDeserializationFailed + })?; + + let mut result = HashMap::new(); + result.insert( + "xmldata".to_string(), + Secret::<_, WithoutType>::new(xml_content), + ); + Ok(result) +} #[derive(Clone)] pub struct Elavon { - amount_converter: &'static (dyn AmountConvertor + Sync), + amount_converter: &'static (dyn AmountConvertor + Sync), } impl Elavon { pub fn new() -> &'static Self { &Self { - amount_converter: &StringMinorUnitForConnector, + amount_converter: &StringMajorUnitForConnector, } } } @@ -96,62 +118,18 @@ impl ConnectorCommon for Elavon { fn get_currency_unit(&self) -> api::CurrencyUnit { api::CurrencyUnit::Base - // TODO! Check connector documentation, on which unit they are processing the currency. - // If the connector accepts amount in lower unit ( i.e cents for USD) then return api::CurrencyUnit::Minor, - // if connector accepts amount in base unit (i.e dollars for USD) then return api::CurrencyUnit::Base } fn common_get_content_type(&self) -> &'static str { - "application/json" + "application/x-www-form-urlencoded" } fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str { connectors.elavon.base_url.as_ref() } - - fn get_auth_header( - &self, - auth_type: &ConnectorAuthType, - ) -> CustomResult)>, errors::ConnectorError> { - let auth = elavon::ElavonAuthType::try_from(auth_type) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - Ok(vec![( - headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), - )]) - } - - fn build_error_response( - &self, - res: Response, - event_builder: Option<&mut ConnectorEvent>, - ) -> CustomResult { - let response: elavon::ElavonErrorResponse = res - .response - .parse_struct("ElavonErrorResponse") - .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.code, - message: response.message, - reason: response.reason, - attempt_status: None, - connector_transaction_id: None, - }) - } } -impl ConnectorValidation for Elavon { - //TODO: implement functions when support enabled -} - -impl ConnectorIntegration for Elavon { - //TODO: implement sessions flow -} +impl ConnectorIntegration for Elavon {} impl ConnectorIntegration for Elavon {} @@ -173,9 +151,9 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}processxml.do", self.base_url(connectors))) } fn get_request_body( @@ -191,7 +169,10 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult { - let response: elavon::ElavonPaymentsResponse = res - .response - .parse_struct("Elavon PaymentsAuthorizeResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let response: elavon::ElavonPaymentsResponse = + utils::deserialize_xml_to_struct(&res.response)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); RouterData::try_from(ResponseRouterData { @@ -260,9 +239,21 @@ impl ConnectorIntegration for Ela fn get_url( &self, _req: &PaymentsSyncRouterData, - _connectors: &Connectors, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}processxml.do", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &PaymentsSyncRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let connector_req = elavon::SyncRequest::try_from(req)?; + router_env::logger::info!(raw_connector_request=?connector_req); + Ok(RequestContent::FormUrlEncoded(Box::new(struct_to_xml( + &connector_req, + )?))) } fn build_request( @@ -272,9 +263,12 @@ impl ConnectorIntegration for Ela ) -> CustomResult, errors::ConnectorError> { Ok(Some( RequestBuilder::new() - .method(Method::Get) + .method(Method::Post) .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) .attach_default_headers() + .set_body(types::PaymentsSyncType::get_request_body( + self, req, connectors, + )?) .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) .build(), )) @@ -286,10 +280,7 @@ impl ConnectorIntegration for Ela event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: elavon::ElavonPaymentsResponse = res - .response - .parse_struct("elavon PaymentsSyncResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let response: elavon::ElavonSyncResponse = utils::deserialize_xml_to_struct(&res.response)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); RouterData::try_from(ResponseRouterData { @@ -324,17 +315,27 @@ impl ConnectorIntegration fo fn get_url( &self, _req: &PaymentsCaptureRouterData, - _connectors: &Connectors, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}processxml.do", self.base_url(connectors))) } fn get_request_body( &self, - _req: &PaymentsCaptureRouterData, + req: &PaymentsCaptureRouterData, _connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let amount = utils::convert_amount( + self.amount_converter, + req.request.minor_amount_to_capture, + req.request.currency, + )?; + let connector_router_data = elavon::ElavonRouterData::from((amount, req)); + let connector_req = elavon::PaymentsCaptureRequest::try_from(&connector_router_data)?; + router_env::logger::info!(raw_connector_request=?connector_req); + Ok(RequestContent::FormUrlEncoded(Box::new(struct_to_xml( + &connector_req, + )?))) } fn build_request( @@ -363,10 +364,8 @@ impl ConnectorIntegration fo event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: elavon::ElavonPaymentsResponse = res - .response - .parse_struct("Elavon PaymentsCaptureResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let response: elavon::ElavonPaymentsResponse = + utils::deserialize_xml_to_struct(&res.response)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); RouterData::try_from(ResponseRouterData { @@ -385,7 +384,15 @@ impl ConnectorIntegration fo } } -impl ConnectorIntegration for Elavon {} +impl ConnectorIntegration for Elavon { + fn build_request( + &self, + _req: &PaymentsCancelRouterData, + _connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented("Cancel/Void flow".to_string()).into()) + } +} impl ConnectorIntegration for Elavon { fn get_headers( @@ -403,9 +410,9 @@ impl ConnectorIntegration for Elavon fn get_url( &self, _req: &RefundsRouterData, - _connectors: &Connectors, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}processxml.do", self.base_url(connectors))) } fn get_request_body( @@ -421,7 +428,10 @@ impl ConnectorIntegration for Elavon let connector_router_data = elavon::ElavonRouterData::from((refund_amount, req)); let connector_req = elavon::ElavonRefundRequest::try_from(&connector_router_data)?; - Ok(RequestContent::Json(Box::new(connector_req))) + router_env::logger::info!(raw_connector_request=?connector_req); + Ok(RequestContent::FormUrlEncoded(Box::new(struct_to_xml( + &connector_req, + )?))) } fn build_request( @@ -449,10 +459,8 @@ impl ConnectorIntegration for Elavon event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: elavon::RefundResponse = - res.response - .parse_struct("elavon RefundResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let response: elavon::ElavonPaymentsResponse = + utils::deserialize_xml_to_struct(&res.response)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); RouterData::try_from(ResponseRouterData { @@ -487,9 +495,20 @@ impl ConnectorIntegration for Elavon { fn get_url( &self, _req: &RefundSyncRouterData, - _connectors: &Connectors, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}processxml.do", self.base_url(connectors))) + } + fn get_request_body( + &self, + req: &RefundSyncRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let connector_req = elavon::SyncRequest::try_from(req)?; + router_env::logger::info!(raw_connector_request=?connector_req); + Ok(RequestContent::FormUrlEncoded(Box::new(struct_to_xml( + &connector_req, + )?))) } fn build_request( @@ -499,7 +518,7 @@ impl ConnectorIntegration for Elavon { ) -> CustomResult, errors::ConnectorError> { Ok(Some( RequestBuilder::new() - .method(Method::Get) + .method(Method::Post) .url(&types::RefundSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundSyncType::get_headers(self, req, connectors)?) @@ -516,10 +535,7 @@ impl ConnectorIntegration for Elavon { event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: elavon::RefundResponse = res - .response - .parse_struct("elavon RefundSyncResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let response: elavon::ElavonSyncResponse = utils::deserialize_xml_to_struct(&res.response)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); RouterData::try_from(ResponseRouterData { @@ -561,3 +577,27 @@ impl webhooks::IncomingWebhook for Elavon { Err(report!(errors::ConnectorError::WebhooksNotImplemented)) } } + +impl ConnectorValidation for Elavon { + fn validate_capture_method( + &self, + capture_method: Option, + _pmt: Option, + ) -> CustomResult<(), errors::ConnectorError> { + let capture_method = capture_method.unwrap_or_default(); + match capture_method { + CaptureMethod::Automatic | CaptureMethod::Manual => Ok(()), + CaptureMethod::ManualMultiple | CaptureMethod::Scheduled => Err( + utils::construct_not_implemented_error_report(capture_method, self.id()), + ), + } + } + fn validate_mandate_payment( + &self, + pm_type: Option, + pm_data: PaymentMethodData, + ) -> CustomResult<(), errors::ConnectorError> { + let mandate_supported_pmd = std::collections::HashSet::from([PaymentMethodDataType::Card]); + utils::is_mandate_supported(pm_data, pm_type, mandate_supported_pmd, self.id()) + } +} diff --git a/crates/hyperswitch_connectors/src/connectors/elavon/transformers.rs b/crates/hyperswitch_connectors/src/connectors/elavon/transformers.rs index 1dbf03ea2757..67561acf8db4 100644 --- a/crates/hyperswitch_connectors/src/connectors/elavon/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/elavon/transformers.rs @@ -1,52 +1,92 @@ -use common_enums::enums; -use common_utils::types::StringMinorUnit; +use cards::CardNumber; +use common_enums::{enums, Currency}; +use common_utils::{pii::Email, types::StringMajorUnit}; +use error_stack::ResultExt; use hyperswitch_domain_models::{ payment_method_data::PaymentMethodData, - router_data::{ConnectorAuthType, RouterData}, + router_data::{ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::refunds::{Execute, RSync}, - router_request_types::ResponseId, - router_response_types::{PaymentsResponseData, RefundsResponseData}, - types::{PaymentsAuthorizeRouterData, RefundsRouterData}, + router_request_types::{PaymentsAuthorizeData, ResponseId}, + router_response_types::{MandateReference, PaymentsResponseData, RefundsResponseData}, + types::{ + PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, + RefundSyncRouterData, RefundsRouterData, + }, }; use hyperswitch_interfaces::errors; -use masking::Secret; +use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; use crate::{ - types::{RefundsResponseRouterData, ResponseRouterData}, - utils::PaymentsAuthorizeRequestData, + types::{ + PaymentsCaptureResponseRouterData, PaymentsSyncResponseRouterData, + RefundsResponseRouterData, ResponseRouterData, + }, + utils::{CardData, PaymentsAuthorizeRequestData, RefundsRequestData, RouterData as _}, }; -//TODO: Fill the struct with respective fields pub struct ElavonRouterData { - pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub amount: StringMajorUnit, pub router_data: T, } -impl From<(StringMinorUnit, T)> for ElavonRouterData { - fn from((amount, item): (StringMinorUnit, T)) -> Self { - //Todo : use utils to convert the amount to the type of amount that a connector accepts +impl From<(StringMajorUnit, T)> for ElavonRouterData { + fn from((amount, item): (StringMajorUnit, T)) -> Self { Self { amount, router_data: item, } } } - -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, PartialEq)] -pub struct ElavonPaymentsRequest { - amount: StringMinorUnit, - card: ElavonCard, +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "lowercase")] +pub enum TransactionType { + CcSale, + CcAuthOnly, + CcComplete, + CcReturn, + TxnQuery, +} +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "UPPERCASE")] +pub enum SyncTransactionType { + Sale, + AuthOnly, + Return, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct ElavonCard { - number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum ElavonPaymentsRequest { + Card(CardPaymentRequest), + MandatePayment(MandatePaymentRequest), +} +#[derive(Debug, Serialize)] +pub struct CardPaymentRequest { + pub ssl_transaction_type: TransactionType, + pub ssl_account_id: Secret, + pub ssl_user_id: Secret, + pub ssl_pin: Secret, + pub ssl_amount: StringMajorUnit, + pub ssl_card_number: CardNumber, + pub ssl_exp_date: Secret, + pub ssl_cvv2cvc2: Secret, + pub ssl_email: Email, + #[serde(skip_serializing_if = "Option::is_none")] + pub ssl_add_token: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ssl_get_token: Option, + pub ssl_transaction_currency: Currency, +} +#[derive(Debug, Serialize)] +pub struct MandatePaymentRequest { + pub ssl_transaction_type: TransactionType, + pub ssl_account_id: Secret, + pub ssl_user_id: Secret, + pub ssl_pin: Secret, + pub ssl_amount: StringMajorUnit, + pub ssl_email: Email, + pub ssl_token: Secret, } impl TryFrom<&ElavonRouterData<&PaymentsAuthorizeRouterData>> for ElavonPaymentsRequest { @@ -54,175 +94,506 @@ impl TryFrom<&ElavonRouterData<&PaymentsAuthorizeRouterData>> for ElavonPayments fn try_from( item: &ElavonRouterData<&PaymentsAuthorizeRouterData>, ) -> Result { + let auth = ElavonAuthType::try_from(&item.router_data.connector_auth_type)?; match item.router_data.request.payment_method_data.clone() { - PaymentMethodData::Card(req_card) => { - let card = ElavonCard { - 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.router_data.request.is_auto_capture()?, - }; - Ok(Self { - amount: item.amount.clone(), - card, - }) - } + PaymentMethodData::Card(req_card) => Ok(Self::Card(CardPaymentRequest { + ssl_transaction_type: match item.router_data.request.is_auto_capture()? { + true => TransactionType::CcSale, + false => TransactionType::CcAuthOnly, + }, + ssl_account_id: auth.account_id.clone(), + ssl_user_id: auth.user_id.clone(), + ssl_pin: auth.pin.clone(), + ssl_amount: item.amount.clone(), + ssl_card_number: req_card.card_number.clone(), + ssl_exp_date: req_card.get_expiry_date_as_mmyy()?, + ssl_cvv2cvc2: req_card.card_cvc, + ssl_email: item.router_data.get_billing_email()?, + ssl_add_token: match item.router_data.request.is_mandate_payment() { + true => Some("Y".to_string()), + false => None, + }, + ssl_get_token: match item.router_data.request.is_mandate_payment() { + true => Some("Y".to_string()), + false => None, + }, + ssl_transaction_currency: item.router_data.request.currency, + })), + PaymentMethodData::MandatePayment => Ok(Self::MandatePayment(MandatePaymentRequest { + ssl_transaction_type: match item.router_data.request.is_auto_capture()? { + true => TransactionType::CcSale, + false => TransactionType::CcAuthOnly, + }, + ssl_account_id: auth.account_id.clone(), + ssl_user_id: auth.user_id.clone(), + ssl_pin: auth.pin.clone(), + ssl_amount: item.amount.clone(), + ssl_email: item.router_data.get_billing_email()?, + ssl_token: Secret::new(item.router_data.request.get_connector_mandate_id()?), + })), _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), } } } -//TODO: Fill the struct with respective fields -// Auth Struct pub struct ElavonAuthType { - pub(super) api_key: Secret, + pub(super) account_id: Secret, + pub(super) user_id: Secret, + pub(super) pin: Secret, } impl TryFrom<&ConnectorAuthType> for ElavonAuthType { type Error = error_stack::Report; fn try_from(auth_type: &ConnectorAuthType) -> Result { match auth_type { - ConnectorAuthType::HeaderKey { api_key } => Ok(Self { - api_key: api_key.to_owned(), + ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Ok(Self { + account_id: api_key.to_owned(), + user_id: key1.to_owned(), + pin: api_secret.to_owned(), }), _ => 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 ElavonPaymentStatus { - Succeeded, - Failed, - #[default] - Processing, -} - -impl From for common_enums::AttemptStatus { - fn from(item: ElavonPaymentStatus) -> Self { - match item { - ElavonPaymentStatus::Succeeded => Self::Charged, - ElavonPaymentStatus::Failed => Self::Failure, - ElavonPaymentStatus::Processing => Self::Authorizing, - } - } + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +enum SslResult { + #[serde(rename = "0")] + ImportedBatchFile, + #[serde(other)] + DeclineOrUnauthorized, } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ElavonPaymentsResponse { - status: ElavonPaymentStatus, - id: String, +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ElavonPaymentsResponse { + #[serde(rename = "txn")] + Success(PaymentResponse), + #[serde(rename = "txn")] + Error(ElavonErrorResponse), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ElavonErrorResponse { + error_code: String, + error_message: String, + error_name: String, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentResponse { + ssl_result: SslResult, + ssl_txn_id: String, + ssl_result_message: String, + ssl_token: Option>, } -impl TryFrom> - for RouterData +impl + TryFrom< + ResponseRouterData, + > for RouterData { type Error = error_stack::Report; fn try_from( - item: ResponseRouterData, + item: ResponseRouterData< + F, + ElavonPaymentsResponse, + PaymentsAuthorizeData, + PaymentsResponseData, + >, ) -> Result { - Ok(Self { - status: common_enums::AttemptStatus::from(item.response.status), - response: Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(item.response.id), - redirection_data: Box::new(None), - mandate_reference: Box::new(None), - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charge_id: None, + let status = get_payment_status(&item.response, item.data.request.is_auto_capture()?); + let response = match &item.response { + ElavonPaymentsResponse::Error(error) => Err(ErrorResponse { + code: error.error_code.clone(), + message: error.error_message.clone(), + reason: Some(error.error_message.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, }), + ElavonPaymentsResponse::Success(response) => { + if status == enums::AttemptStatus::Failure { + Err(ErrorResponse { + code: response.ssl_result_message.clone(), + message: response.ssl_result_message.clone(), + reason: Some(response.ssl_result_message.clone()), + attempt_status: None, + connector_transaction_id: Some(response.ssl_txn_id.clone()), + status_code: item.http_code, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + response.ssl_txn_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(Some(MandateReference { + connector_mandate_id: response + .ssl_token + .as_ref() + .map(|secret| secret.clone().expose()), + payment_method_id: None, + mandate_metadata: None, + connector_mandate_request_reference_id: None, + })), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(response.ssl_txn_id.clone()), + incremental_authorization_allowed: None, + charge_id: None, + }) + } + } + }; + Ok(Self { + status, + response, ..item.data }) } } -//TODO: Fill the struct with respective fields -// REFUND : -// Type definition for RefundRequest -#[derive(Default, Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] +pub enum TransactionSyncStatus { + PEN, // Pended + OPN, // Unpended / release / open + REV, // Review + STL, // Settled + PST, // Failed due to post-auth rule + FPR, // Failed due to fraud prevention rules + PRE, // Failed due to pre-auth rule +} + +#[derive(Debug, Serialize)] +#[serde(rename = "txn")] +pub struct PaymentsCaptureRequest { + pub ssl_transaction_type: TransactionType, + pub ssl_account_id: Secret, + pub ssl_user_id: Secret, + pub ssl_pin: Secret, + pub ssl_amount: StringMajorUnit, + pub ssl_txn_id: String, +} +#[derive(Debug, Serialize)] +#[serde(rename = "txn")] +pub struct PaymentsVoidRequest { + pub ssl_transaction_type: TransactionType, + pub ssl_account_id: Secret, + pub ssl_user_id: Secret, + pub ssl_pin: Secret, + pub ssl_txn_id: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename = "txn")] pub struct ElavonRefundRequest { - pub amount: StringMinorUnit, + pub ssl_transaction_type: TransactionType, + pub ssl_account_id: Secret, + pub ssl_user_id: Secret, + pub ssl_pin: Secret, + pub ssl_amount: StringMajorUnit, + pub ssl_txn_id: String, } +#[derive(Debug, Serialize)] +#[serde(rename = "txn")] +pub struct SyncRequest { + pub ssl_transaction_type: TransactionType, + pub ssl_account_id: Secret, + pub ssl_user_id: Secret, + pub ssl_pin: Secret, + pub ssl_txn_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename = "txn")] +pub struct ElavonSyncResponse { + pub ssl_trans_status: TransactionSyncStatus, + pub ssl_transaction_type: SyncTransactionType, + pub ssl_txn_id: String, +} +impl TryFrom<&RefundSyncRouterData> for SyncRequest { + type Error = error_stack::Report; + fn try_from(item: &RefundSyncRouterData) -> Result { + let auth = ElavonAuthType::try_from(&item.connector_auth_type)?; + Ok(Self { + ssl_txn_id: item.request.get_connector_refund_id()?, + ssl_transaction_type: TransactionType::TxnQuery, + ssl_account_id: auth.account_id.clone(), + ssl_user_id: auth.user_id.clone(), + ssl_pin: auth.pin.clone(), + }) + } +} +impl TryFrom<&PaymentsSyncRouterData> for SyncRequest { + type Error = error_stack::Report; + fn try_from(item: &PaymentsSyncRouterData) -> Result { + let auth = ElavonAuthType::try_from(&item.connector_auth_type)?; + Ok(Self { + ssl_txn_id: item + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?, + ssl_transaction_type: TransactionType::TxnQuery, + ssl_account_id: auth.account_id.clone(), + ssl_user_id: auth.user_id.clone(), + ssl_pin: auth.pin.clone(), + }) + } +} impl TryFrom<&ElavonRouterData<&RefundsRouterData>> for ElavonRefundRequest { type Error = error_stack::Report; fn try_from(item: &ElavonRouterData<&RefundsRouterData>) -> Result { + let auth = ElavonAuthType::try_from(&item.router_data.connector_auth_type)?; Ok(Self { - amount: item.amount.to_owned(), + ssl_txn_id: item.router_data.request.connector_transaction_id.clone(), + ssl_amount: item.amount.clone(), + ssl_transaction_type: TransactionType::CcReturn, + ssl_account_id: auth.account_id.clone(), + ssl_user_id: auth.user_id.clone(), + ssl_pin: auth.pin.clone(), }) } } -// 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 - } +impl TryFrom<&ElavonRouterData<&PaymentsCaptureRouterData>> for PaymentsCaptureRequest { + type Error = error_stack::Report; + fn try_from(item: &ElavonRouterData<&PaymentsCaptureRouterData>) -> Result { + let auth = ElavonAuthType::try_from(&item.router_data.connector_auth_type)?; + Ok(Self { + ssl_txn_id: item.router_data.request.connector_transaction_id.clone(), + ssl_amount: item.amount.clone(), + ssl_transaction_type: TransactionType::CcComplete, + ssl_account_id: auth.account_id.clone(), + ssl_user_id: auth.user_id.clone(), + ssl_pin: auth.pin.clone(), + }) } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse { - id: String, - status: RefundStatus, +impl TryFrom> for PaymentsSyncRouterData { + type Error = error_stack::Report; + fn try_from( + item: PaymentsSyncResponseRouterData, + ) -> Result { + Ok(Self { + status: get_sync_status(item.data.status, &item.response), + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.ssl_txn_id), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }) + } } - -impl TryFrom> for RefundsRouterData { +impl TryFrom> for RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + connector_refund_id: item.response.ssl_txn_id.clone(), + refund_status: get_refund_status(item.data.request.refund_status, &item.response), }), ..item.data }) } } -impl TryFrom> for RefundsRouterData { +impl TryFrom> + for PaymentsCaptureRouterData +{ type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: PaymentsCaptureResponseRouterData, ) -> Result { + let status = map_payment_status(&item.response, enums::AttemptStatus::Charged); + let response = match &item.response { + ElavonPaymentsResponse::Error(error) => Err(ErrorResponse { + code: error.error_code.clone(), + message: error.error_message.clone(), + reason: Some(error.error_message.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }), + ElavonPaymentsResponse::Success(response) => { + if status == enums::AttemptStatus::Failure { + Err(ErrorResponse { + code: response.ssl_result_message.clone(), + message: response.ssl_result_message.clone(), + reason: Some(response.ssl_result_message.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + response.ssl_txn_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(response.ssl_txn_id.clone()), + incremental_authorization_allowed: None, + charge_id: None, + }) + } + } + }; Ok(Self { - response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + status, + response, + ..item.data + }) + } +} +impl TryFrom> + for RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: RefundsResponseRouterData, + ) -> Result { + let status = enums::RefundStatus::from(&item.response); + let response = match &item.response { + ElavonPaymentsResponse::Error(error) => Err(ErrorResponse { + code: error.error_code.clone(), + message: error.error_message.clone(), + reason: Some(error.error_message.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, }), + ElavonPaymentsResponse::Success(response) => { + if status == enums::RefundStatus::Failure { + Err(ErrorResponse { + code: response.ssl_result_message.clone(), + message: response.ssl_result_message.clone(), + reason: Some(response.ssl_result_message.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }) + } else { + Ok(RefundsResponseData { + connector_refund_id: response.ssl_txn_id.clone(), + refund_status: enums::RefundStatus::from(&item.response), + }) + } + } + }; + Ok(Self { + response, ..item.data }) } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct ElavonErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, - pub reason: Option, +trait ElavonResponseValidator { + fn is_successful(&self) -> bool; +} +impl ElavonResponseValidator for ElavonPaymentsResponse { + fn is_successful(&self) -> bool { + matches!(self, Self::Success(response) if response.ssl_result == SslResult::ImportedBatchFile) + } +} + +fn map_payment_status( + item: &ElavonPaymentsResponse, + success_status: enums::AttemptStatus, +) -> enums::AttemptStatus { + if item.is_successful() { + success_status + } else { + enums::AttemptStatus::Failure + } +} + +impl From<&ElavonPaymentsResponse> for enums::RefundStatus { + fn from(item: &ElavonPaymentsResponse) -> Self { + if item.is_successful() { + Self::Success + } else { + Self::Failure + } + } +} +fn get_refund_status( + prev_status: enums::RefundStatus, + item: &ElavonSyncResponse, +) -> enums::RefundStatus { + match item.ssl_trans_status { + TransactionSyncStatus::REV | TransactionSyncStatus::OPN | TransactionSyncStatus::PEN => { + prev_status + } + TransactionSyncStatus::STL => enums::RefundStatus::Success, + TransactionSyncStatus::PST | TransactionSyncStatus::FPR | TransactionSyncStatus::PRE => { + enums::RefundStatus::Failure + } + } +} +impl From<&ElavonSyncResponse> for enums::AttemptStatus { + fn from(item: &ElavonSyncResponse) -> Self { + match item.ssl_trans_status { + TransactionSyncStatus::REV + | TransactionSyncStatus::OPN + | TransactionSyncStatus::PEN => Self::Pending, + TransactionSyncStatus::STL => match item.ssl_transaction_type { + SyncTransactionType::Sale => Self::Charged, + SyncTransactionType::AuthOnly => Self::Authorized, + SyncTransactionType::Return => Self::Pending, + }, + TransactionSyncStatus::PST + | TransactionSyncStatus::FPR + | TransactionSyncStatus::PRE => Self::Failure, + } + } +} +fn get_sync_status( + prev_status: enums::AttemptStatus, + item: &ElavonSyncResponse, +) -> enums::AttemptStatus { + match item.ssl_trans_status { + TransactionSyncStatus::REV | TransactionSyncStatus::OPN | TransactionSyncStatus::PEN => { + prev_status + } + TransactionSyncStatus::STL => match item.ssl_transaction_type { + SyncTransactionType::Sale => enums::AttemptStatus::Charged, + SyncTransactionType::AuthOnly => enums::AttemptStatus::Authorized, + SyncTransactionType::Return => enums::AttemptStatus::Pending, + }, + TransactionSyncStatus::PST | TransactionSyncStatus::FPR | TransactionSyncStatus::PRE => { + enums::AttemptStatus::Failure + } + } +} + +fn get_payment_status( + item: &ElavonPaymentsResponse, + is_auto_capture: bool, +) -> enums::AttemptStatus { + if item.is_successful() { + if is_auto_capture { + enums::AttemptStatus::Charged + } else { + enums::AttemptStatus::Authorized + } + } else { + enums::AttemptStatus::Failure + } } diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index d928bbfa4eb4..a7fe1cc4cd95 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -2250,3 +2250,20 @@ impl WalletData for hyperswitch_domain_models::payment_method_data::WalletData { } } } + +pub fn deserialize_xml_to_struct( + xml_data: &[u8], +) -> Result { + let response_str = std::str::from_utf8(xml_data) + .map_err(|e| { + router_env::logger::error!("Error converting response data to UTF-8: {:?}", e); + errors::ConnectorError::ResponseDeserializationFailed + })? + .trim(); + let result: T = quick_xml::de::from_str(response_str).map_err(|e| { + router_env::logger::error!("Error deserializing XML response: {:?}", e); + errors::ConnectorError::ResponseDeserializationFailed + })?; + + Ok(result) +} diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index c645d0b4b532..c282afa9d0d6 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -617,6 +617,7 @@ pub struct RefundsData { pub minor_payment_amount: MinorUnit, pub minor_refund_amount: MinorUnit, pub integrity_object: Option, + pub refund_status: storage_enums::RefundStatus, } #[derive(Debug, Clone, PartialEq)] diff --git a/crates/router/src/configs/defaults/payment_connector_required_fields.rs b/crates/router/src/configs/defaults/payment_connector_required_fields.rs index 8d5a4fe89b94..4e80ccf027ec 100644 --- a/crates/router/src/configs/defaults/payment_connector_required_fields.rs +++ b/crates/router/src/configs/defaults/payment_connector_required_fields.rs @@ -1355,6 +1355,63 @@ impl Default for settings::RequiredFields { ), } ), + ( + enums::Connector::Elavon, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.card.card_cvc".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_cvc".to_string(), + display_name: "card_cvc".to_string(), + field_type: enums::FieldType::UserCardCvc, + value: None, + } + ), + ( + "billing.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + + ] + ), + common: HashMap::new(), + } + ), ( enums::Connector::Fiserv, RequiredFieldFinal { @@ -4399,6 +4456,63 @@ impl Default for settings::RequiredFields { ), } ), + ( + enums::Connector::Elavon, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.card.card_cvc".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_cvc".to_string(), + display_name: "card_cvc".to_string(), + field_type: enums::FieldType::UserCardCvc, + value: None, + } + ), + ( + "billing.email".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.billing.email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + + ] + ), + common: HashMap::new(), + } + ), ( enums::Connector::Fiserv, RequiredFieldFinal { diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 97a40f38ac4b..a8d14187dc1a 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1347,6 +1347,10 @@ impl<'a> ConnectorAuthTypeAndMetadataValidation<'a> { ebanx::transformers::EbanxAuthType::try_from(self.auth_type)?; Ok(()) } + api_enums::Connector::Elavon => { + elavon::transformers::ElavonAuthType::try_from(self.auth_type)?; + Ok(()) + } api_enums::Connector::Fiserv => { fiserv::transformers::FiservAuthType::try_from(self.auth_type)?; fiserv::transformers::FiservSessionObject::try_from(self.connector_meta_data)?; diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 167a4c590068..776692d6bec1 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -363,6 +363,7 @@ pub async fn construct_refund_router_data<'a, F>( browser_info, charges, integrity_object: None, + refund_status: refund.refund_status, }, response: Ok(types::RefundsResponseData { diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index f7909a971958..ad40f83a554e 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -410,7 +410,9 @@ impl ConnectorData { enums::Connector::Ebanx => { Ok(ConnectorEnum::Old(Box::new(connector::Ebanx::new()))) } - // enums::Connector::Elavon => Ok(ConnectorEnum::Old(Box::new(connector::Elavon))), + enums::Connector::Elavon => { + Ok(ConnectorEnum::Old(Box::new(connector::Elavon::new()))) + } enums::Connector::Fiserv => Ok(ConnectorEnum::Old(Box::new(&connector::Fiserv))), enums::Connector::Fiservemea => { Ok(ConnectorEnum::Old(Box::new(connector::Fiservemea::new()))) diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index ecec8211446a..c8a9cbea1ddd 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -234,7 +234,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Digitalvirgo => Self::Digitalvirgo, api_enums::Connector::Dlocal => Self::Dlocal, api_enums::Connector::Ebanx => Self::Ebanx, - // api_enums::Connector::Elavon => Self::Elavon, + api_enums::Connector::Elavon => Self::Elavon, api_enums::Connector::Fiserv => Self::Fiserv, api_enums::Connector::Fiservemea => Self::Fiservemea, api_enums::Connector::Fiuu => Self::Fiuu, diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 3402b532fbfb..a03148956ba2 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -406,6 +406,7 @@ pub trait ConnectorActions: Connector { browser_info: None, charges: None, integrity_object: None, + refund_status: enums::RefundStatus::Pending, }), payment_info, ); @@ -1035,6 +1036,7 @@ impl Default for PaymentRefundType { browser_info: None, charges: None, integrity_object: None, + refund_status: enums::RefundStatus::Pending, }; Self(data) } diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Elavon.js b/cypress-tests/cypress/e2e/PaymentUtils/Elavon.js new file mode 100644 index 000000000000..5b985df29dab --- /dev/null +++ b/cypress-tests/cypress/e2e/PaymentUtils/Elavon.js @@ -0,0 +1,571 @@ +const successfulNo3DSCardDetails = { + card_number: "4111111111111111", + card_exp_month: "06", + card_exp_year: "25", + card_holder_name: "joseph Doe", + card_cvc: "123", +}; +const singleUseMandateData = { + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + mandate_type: { + single_use: { + amount: 8000, + currency: "USD", + }, + }, +}; + +const multiUseMandateData = { + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + mandate_type: { + multi_use: { + amount: 8000, + currency: "USD", + }, + }, +}; +export const connectorDetails = { + card_pm: { + PaymentIntent: { + Request: { + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + billing: { + address: { + line1: "1467", + line2: "CA", + line3: "CA", + city: "Florence", + state: "Tuscany", + zip: "12345", + country: "IT", + first_name: "Max", + last_name: "Mustermann", + }, + email: "mauro.morandi@nexi.it", + phone: { + number: "9123456789", + country_code: "+91", + }, + }, + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + }, + }, + }, + No3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + email: "mauro.morandi@nexi.it", + }, + }, + billing: { + email: "mauro.morandi@nexi.it", + }, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + No3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + email: "mauro.morandi@nexi.it", + }, + }, + billing: { + email: "mauro.morandi@nexi.it", + }, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, manualPaymentPartialRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + manualPaymentRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MandateMultiUseNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + email: "johndoe@gmail.com" + }, + }, + currency: "USD", + mandate_data: multiUseMandateData, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MandateMultiUseNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + email: "johndoe@gmail.com" + }, + }, + currency: "USD", + mandate_data: multiUseMandateData, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + SaveCardUseNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + email: "mauro.morandi@nexi.it", + + }, + }, + currency: "USD", + setup_future_usage: "on_session", + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "127.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SaveCardUseNo3DSAutoCaptureOffSession: { + Request: { + payment_method: "card", + payment_method_type: "debit", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + phone: { + number: "9123456789", + country_code: "+91", + }, + email: "mauro.morandi@nexi.it", + }, + }, + setup_future_usage: "off_session", + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "127.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SaveCardUseNo3DSManualCaptureOffSession: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + phone: { + number: "9123456789", + country_code: "+91", + }, + email: "mauro.morandi@nexi.it", + }, + }, + setup_future_usage: "off_session", + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "127.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + SaveCardConfirmAutoCaptureOffSession: { + Request: { + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SaveCardConfirmManualCaptureOffSession: { + Request: { + setup_future_usage: "off_session", + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + SaveCardUseNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + address: { + line1: "1467", + line2: "Harrison Street", + line3: "Harrison Street", + city: "San Fransico", + state: "California", + zip: "94122", + country: "NL", + first_name: "joseph", + last_name: "Doe", + }, + phone: { + number: "9123456789", + country_code: "+91", + }, + email: "mauro.morandi@nexi.it", + }, + }, + currency: "USD", + setup_future_usage: "on_session", + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "127.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + MandateSingleUseNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + email: "mauro.morandi@nexi.it", + }, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + MandateSingleUseNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + billing: { + email: "mauro.morandi@nexi.it", + }, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + Capture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + amount: 6500, + amount_capturable: 0, + amount_received: 6500, + }, + }, + }, + PartialCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "partially_captured", + amount: 6500, + amount_capturable: 0, + amount_received: 100, + }, + }, + }, + Refund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + VoidAfterConfirm: { + Request: {}, + Response: { + status: 501, + body: { + error: { + type: "invalid_request", + message: "Cancel/Void flow is not implemented", + code: "IR_00" + } + } + }, + }, + PartialRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + SyncRefund: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + PaymentMethodIdMandateNo3DSAutoCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + billing: { + email: "mauro.morandi@nexi.it", + }, + mandate_data: null, + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + PaymentMethodIdMandateNo3DSManualCapture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + billing: { + email: "mauro.morandi@nexi.it", + }, + currency: "USD", + mandate_data: null, + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + }, +}; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js index fc1685a66213..39d8ab6c8af7 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js @@ -22,6 +22,7 @@ import { connectorDetails as wellsfargoConnectorDetails } from "./WellsFargo.js" import { connectorDetails as fiuuConnectorDetails } from "./Fiuu.js"; import { connectorDetails as worldpayConnectorDetails } from "./WorldPay.js"; import { connectorDetails as checkoutConnectorDetails } from "./Checkout.js"; +import { connectorDetails as elavonConnectorDetails } from "./Elavon.js"; const connectorDetails = { adyen: adyenConnectorDetails, @@ -39,6 +40,7 @@ const connectorDetails = { paybox: payboxConnectorDetails, paypal: paypalConnectorDetails, stripe: stripeConnectorDetails, + elavon: elavonConnectorDetails, trustpay: trustpayConnectorDetails, datatrans: datatransConnectorDetails, wellsfargo: wellsfargoConnectorDetails, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index b7a6fca7de45..8aeacaeca1fc 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -103,7 +103,7 @@ digitalvirgo.base_url = "https://dcb-integration-service-sandbox-external.stagin dlocal.base_url = "https://sandbox.dlocal.com/" dummyconnector.base_url = "http://localhost:8080/dummy-connector" ebanx.base_url = "https://sandbox.ebanxpay.com/" -elavon.base_url = "https://api.demo.convergepay.com" +elavon.base_url = "https://api.demo.convergepay.com/VirtualMerchantDemo/" fiserv.base_url = "https://cert.api.fiservapps.com/" fiservemea.base_url = "https://prod.emea.api.fiservapps.com/sandbox" fiuu.base_url = "https://sandbox.merchant.razer.com/"