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 27b4158ba619..1dddb017b8e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4010,6 +4010,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 f00998f154f4..190524a2556e 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -5829,6 +5829,7 @@ "digitalvirgo", "dlocal", "ebanx", + "elavon", "fiserv", "fiservemea", "fiuu", @@ -18060,6 +18061,7 @@ "digitalvirgo", "dlocal", "ebanx", + "elavon", "fiserv", "fiservemea", "fiuu", diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index d4c5717d2156..085fb3d87475 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -8654,6 +8654,7 @@ "digitalvirgo", "dlocal", "ebanx", + "elavon", "fiserv", "fiservemea", "fiuu", @@ -22618,6 +22619,7 @@ "digitalvirgo", "dlocal", "ebanx", + "elavon", "fiserv", "fiservemea", "fiuu", diff --git a/config/config.example.toml b/config/config.example.toml index 289087f4a333..51d9277a6c24 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 1c7a40ea5398..13a7e8c7c909 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 73e5794f0420..a59cf45d6afe 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 98e1e7e00d9c..224298c2d846 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 b6638fe1d476..f3667e1ab50f 100644 --- a/config/development.toml +++ b/config/development.toml @@ -221,7 +221,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 7adeee8a3763..8f6802bbc958 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 77081f49957b..bd0cbbb5c708 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, @@ -215,6 +216,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 9beaed759600..2bc0edaf5ad8 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 3634fe730ca2..25563f075f5b 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, @@ -345,6 +346,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..e1d290fca387 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}, @@ -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( @@ -286,10 +277,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 +312,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 +361,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 { @@ -403,9 +399,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 +417,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 +448,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 +484,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( @@ -516,10 +524,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 +566,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..59d787088219 100644 --- a/crates/hyperswitch_connectors/src/connectors/elavon/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/elavon/transformers.rs @@ -1,52 +1,91 @@ -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, } -#[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 +93,486 @@ 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: None, + 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: enums::AttemptStatus::from(&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: enums::RefundStatus::from(&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: None, + 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 + } + } +} +impl From<&ElavonSyncResponse> for enums::RefundStatus { + fn from(item: &ElavonSyncResponse) -> Self { + match item.ssl_trans_status { + TransactionSyncStatus::REV + | TransactionSyncStatus::OPN + | TransactionSyncStatus::PEN => Self::Pending, + TransactionSyncStatus::STL => Self::Success, + TransactionSyncStatus::PST + | TransactionSyncStatus::FPR + | TransactionSyncStatus::PRE => Self::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, + }, + TransactionSyncStatus::PST + | TransactionSyncStatus::FPR + | TransactionSyncStatus::PRE => Self::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 14596c0f2a21..1e7633431f40 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -2241,3 +2241,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/router/src/configs/defaults/payment_connector_required_fields.rs b/crates/router/src/configs/defaults/payment_connector_required_fields.rs index fc37d937d00a..6b90a9649662 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 { @@ -4448,6 +4505,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/types/api.rs b/crates/router/src/types/api.rs index b432c57c5d1d..3ebb597e3364 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 36865f5ce09b..c395e2c320b3 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/cypress-tests/cypress/e2e/PaymentUtils/Elavon.js b/cypress-tests/cypress/e2e/PaymentUtils/Elavon.js new file mode 100644 index 000000000000..5a23d588c9be --- /dev/null +++ b/cypress-tests/cypress/e2e/PaymentUtils/Elavon.js @@ -0,0 +1,168 @@ +const successfulNo3DSCardDetails = { + card_number: "4111111111111111", + card_exp_month: "06", + card_exp_year: "25", + card_holder_name: "joseph Doe", + card_cvc: "123", +}; + +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", + }, + }, + }, + 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", + }, + }, + }, + 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", + }, + }, + }, + }, +}; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js index 618f5864f6d5..67389d77fd06 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js @@ -21,6 +21,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, @@ -43,6 +44,7 @@ const connectorDetails = { wellsfargo: wellsfargoConnectorDetails, fiuu: fiuuConnectorDetails, worldpay: worldpayConnectorDetails, + elavon: elavonConnectorDetails }; export default function getConnectorDetails(connectorId) { diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 934b072f14a3..ca1d47f665a2 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/"