From 06c30967cf626e7406aa9be8643fb73288aae383 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Sat, 9 Mar 2024 14:48:45 +0530 Subject: [PATCH] feat(connector): add threedsecureio three_ds authentication connector (#4004) Co-authored-by: hrithikesh026 Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> --- Cargo.lock | 30 + config/config.example.toml | 2 + config/development.toml | 2 + config/docker_compose.toml | 2 + crates/api_models/src/enums.rs | 1 + crates/connector_configs/src/connector.rs | 19 +- .../connector_configs/toml/development.toml | 10 +- crates/connector_configs/toml/sandbox.toml | 10 +- crates/euclid_wasm/src/lib.rs | 8 + crates/router/Cargo.toml | 2 + crates/router/src/configs/settings.rs | 1 + crates/router/src/connector.rs | 5 +- crates/router/src/connector/threedsecureio.rs | 533 +++++++++++++ .../connector/threedsecureio/transformers.rs | 747 ++++++++++++++++++ crates/router/src/core/admin.rs | 4 + crates/router/src/core/payments/flows.rs | 27 + crates/router/src/types/api.rs | 3 +- crates/router/src/types/api/authentication.rs | 9 +- crates/router/src/types/transformers.rs | 6 + .../router/tests/connectors/sample_auth.toml | 5 + crates/test_utils/src/connector_auth.rs | 1 + loadtest/config/development.toml | 2 + openapi/openapi_spec.json | 1 + scripts/add_connector.sh | 2 +- 24 files changed, 1418 insertions(+), 14 deletions(-) create mode 100644 crates/router/src/connector/threedsecureio.rs create mode 100644 crates/router/src/connector/threedsecureio/transformers.rs diff --git a/Cargo.lock b/Cargo.lock index 4571d80fd8..2e4d3324d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3382,6 +3382,34 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "iso_country" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20633e788d3948ea7336861fdb09ec247f5dae4267e8f0743fa97de26c28624d" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "iso_currency" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f07181be95c82347a07cf4caf43d2acd8a7e8d08ef1db75e10ed5a9aec3c1b" +dependencies = [ + "iso_country", +] + +[[package]] +name = "isocountry" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ea1dc4bf0fb4904ba83ffdb98af3d9c325274e92e6e295e4151e86c96363e04" +dependencies = [ + "serde", + "thiserror", +] + [[package]] name = "itertools" version = "0.10.5" @@ -5210,6 +5238,8 @@ dependencies = [ "hyperswitch_interfaces", "image", "infer 0.13.0", + "iso_currency", + "isocountry", "josekit", "jsonwebtoken", "kgraph_utils", diff --git a/config/config.example.toml b/config/config.example.toml index e9e80911e8..2ffb69c3e3 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -216,6 +216,7 @@ square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" stripe.base_url = "https://api.stripe.com/" +threedsecureio.base_url = "https://service.sandbox.3dsecure.io" stripe.base_url_file_upload = "https://files.stripe.com/" trustpay.base_url = "https://test-tpgw.trustpay.eu/" trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" @@ -273,6 +274,7 @@ cards = [ "square", "stax", "stripe", + "threedsecureio", "worldpay", "zen", ] diff --git a/config/development.toml b/config/development.toml index c7bfefc343..6d4914af8b 100644 --- a/config/development.toml +++ b/config/development.toml @@ -133,6 +133,7 @@ cards = [ "square", "stax", "stripe", + "threedsecureio", "trustpay", "tsys", "volt", @@ -210,6 +211,7 @@ square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" stripe.base_url = "https://api.stripe.com/" +threedsecureio.base_url = "https://service.sandbox.3dsecure.io" stripe.base_url_file_upload = "https://files.stripe.com/" wise.base_url = "https://api.sandbox.transferwise.tech/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 892086a7b9..2f94d69790 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -150,6 +150,7 @@ square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" stripe.base_url = "https://api.stripe.com/" +threedsecureio.base_url = "https://service.sandbox.3dsecure.io" stripe.base_url_file_upload = "https://files.stripe.com/" trustpay.base_url = "https://test-tpgw.trustpay.eu/" trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" @@ -211,6 +212,7 @@ cards = [ "square", "stax", "stripe", + "threedsecureio", "trustpay", "tsys", "volt", diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index a5a2db4f60..459f4d7b0e 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -116,6 +116,7 @@ pub enum Connector { Square, Stax, Stripe, + Threedsecureio, Trustpay, // Tsys, Tsys, diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index 357ee0cb73..bbde98b7d4 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -2,7 +2,10 @@ use std::collections::HashMap; #[cfg(feature = "payouts")] use api_models::enums::PayoutConnectors; -use api_models::{enums::Connector, payments}; +use api_models::{ + enums::{AuthenticationConnectors, Connector}, + payments, +}; use serde::Deserialize; #[cfg(any(feature = "sandbox", feature = "development", feature = "production"))] use toml; @@ -75,6 +78,9 @@ pub struct ConfigMetadata { pub apple_pay: Option, pub merchant_id: Option, pub endpoint_prefix: Option, + pub mcc: Option, + pub merchant_country_code: Option, + pub merchant_name: Option, } #[serde_with::skip_serializing_none] @@ -147,6 +153,7 @@ pub struct ConnectorConfig { pub stripe: Option, pub signifyd: Option, pub trustpay: Option, + pub threedsecureio: Option, pub tsys: Option, pub volt: Option, #[cfg(feature = "payouts")] @@ -199,6 +206,15 @@ impl ConnectorConfig { } } + pub fn get_authentication_connector_config( + connector: AuthenticationConnectors, + ) -> Result, String> { + let connector_data = Self::new()?; + match connector { + AuthenticationConnectors::Threedsecureio => Ok(connector_data.threedsecureio), + } + } + pub fn get_connector_config( connector: Connector, ) -> Result, String> { @@ -250,6 +266,7 @@ impl ConnectorConfig { Connector::Stax => Ok(connector_data.stax), Connector::Stripe => Ok(connector_data.stripe), Connector::Trustpay => Ok(connector_data.trustpay), + Connector::Threedsecureio => Ok(connector_data.threedsecureio), Connector::Tsys => Ok(connector_data.tsys), Connector::Volt => Ok(connector_data.volt), Connector::Wise => Err("Use get_payout_connector_config".to_string()), diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 2bc5e76fa1..9868346d49 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -2564,4 +2564,12 @@ key1 = "Adyen Account Id" payment_method_type = "sepa" [wise_payout.connector_auth.BodyKey] api_key = "Wise API Key" -key1 = "Wise Account Id" \ No newline at end of file +key1 = "Wise Account Id" + +[threedsecureio] +[threedsecureio.connector_auth.HeaderKey] +api_key="Api Key" +[threedsecureio.metadata] +mcc="MCC" +merchant_country_code="3 digit numeric country code" +merchant_name="Name of the merchant" \ No newline at end of file diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index b76119a158..8921ca3bd3 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -2566,4 +2566,12 @@ key1 = "Adyen Account Id" payment_method_type = "sepa" [wise_payout.connector_auth.BodyKey] api_key = "Wise API Key" -key1 = "Wise Account Id" \ No newline at end of file +key1 = "Wise Account Id" + +[threedsecureio] +[threedsecureio.connector_auth.HeaderKey] +api_key="Api Key" +[threedsecureio.metadata] +mcc="MCC" +merchant_country_code="3 digit numeric country code" +merchant_name="Name of the merchant" \ No newline at end of file diff --git a/crates/euclid_wasm/src/lib.rs b/crates/euclid_wasm/src/lib.rs index 1143ea8a28..c2ff3efc71 100644 --- a/crates/euclid_wasm/src/lib.rs +++ b/crates/euclid_wasm/src/lib.rs @@ -313,6 +313,14 @@ pub fn get_payout_connector_config(key: &str) -> JsResult { Ok(serde_wasm_bindgen::to_value(&res)?) } +#[wasm_bindgen(js_name = getAuthenticationConnectorConfig)] +pub fn get_authentication_connector_config(key: &str) -> JsResult { + let key = api_model_enums::AuthenticationConnectors::from_str(key) + .map_err(|_| "Invalid key received".to_string())?; + let res = connector::ConnectorConfig::get_authentication_connector_config(key)?; + Ok(serde_wasm_bindgen::to_value(&res)?) +} + #[wasm_bindgen(js_name = getRequestPayload)] pub fn get_request_payload(input: JsValue, response: JsValue) -> JsResult { let input: DashboardRequestPayload = serde_wasm_bindgen::from_value(input)?; diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index a748b6b273..9cb7913211 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -120,6 +120,8 @@ openapi = { version = "0.1.0", path = "../openapi", optional = true } erased-serde = "0.3.31" quick-xml = { version = "0.31.0", features = ["serialize"] } rdkafka = "0.36.0" +isocountry = "0.3.2" +iso_currency = "0.4.4" actix-http = "3.3.1" [build-dependencies] diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 70a173f1d9..dc4826dfed 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -520,6 +520,7 @@ pub struct Connectors { pub square: ConnectorParams, pub stax: ConnectorParams, pub stripe: ConnectorParamsWithFileUploadUrl, + pub threedsecureio: ConnectorParams, pub trustpay: ConnectorParamsWithMoreUrls, pub tsys: ConnectorParams, pub volt: ConnectorParams, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index de6e250842..eccdf3f9dd 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -46,6 +46,7 @@ pub mod signifyd; pub mod square; pub mod stax; pub mod stripe; +pub mod threedsecureio; pub mod trustpay; pub mod tsys; pub mod utils; @@ -68,6 +69,6 @@ pub use self::{ payeezy::Payeezy, payme::Payme, paypal::Paypal, payu::Payu, placetopay::Placetopay, powertranz::Powertranz, prophetpay::Prophetpay, rapyd::Rapyd, riskified::Riskified, shift4::Shift4, signifyd::Signifyd, square::Square, stax::Stax, stripe::Stripe, - trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, worldline::Worldline, - worldpay::Worldpay, zen::Zen, + threedsecureio::Threedsecureio, trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, + worldline::Worldline, worldpay::Worldpay, zen::Zen, }; diff --git a/crates/router/src/connector/threedsecureio.rs b/crates/router/src/connector/threedsecureio.rs new file mode 100644 index 0000000000..71c23ab732 --- /dev/null +++ b/crates/router/src/connector/threedsecureio.rs @@ -0,0 +1,533 @@ +pub mod transformers; + +use std::fmt::Debug; + +use error_stack::{IntoReport, ResultExt}; +use masking::ExposeInterface; +use pm_auth::consts; +use transformers as threedsecureio; + +use crate::{ + configs::settings, + core::errors::{self, CustomResult}, + events::connector_api_logs::ConnectorEvent, + headers, + services::{ + self, + request::{self, Mask}, + ConnectorIntegration, ConnectorValidation, + }, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, RequestContent, Response, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Threedsecureio; + +impl api::Payment for Threedsecureio {} +impl api::PaymentSession for Threedsecureio {} +impl api::ConnectorAccessToken for Threedsecureio {} +impl api::MandateSetup for Threedsecureio {} +impl api::PaymentAuthorize for Threedsecureio {} +impl api::PaymentSync for Threedsecureio {} +impl api::PaymentCapture for Threedsecureio {} +impl api::PaymentVoid for Threedsecureio {} +impl api::Refund for Threedsecureio {} +impl api::RefundExecute for Threedsecureio {} +impl api::RefundSync for Threedsecureio {} +impl api::PaymentToken for Threedsecureio {} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Threedsecureio +{ +} + +impl ConnectorCommonExt for Threedsecureio +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + "application/json; charset=utf-8".to_string().into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + +impl ConnectorCommon for Threedsecureio { + fn id(&self) -> &'static str { + "threedsecureio" + } + + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Minor + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.threedsecureio.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = threedsecureio::ThreedsecureioAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![( + headers::APIKEY.to_string(), + auth.api_key.expose().into_masked(), + )]) + } + + fn build_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + let response_result: Result< + threedsecureio::ThreedsecureioErrorResponse, + error_stack::Report, + > = res.response.parse_struct("ThreedsecureioErrorResponse"); + + match response_result { + Ok(response) => { + event_builder.map(|i| i.set_error_response_body(&response)); + router_env::logger::info!(connector_response=?response); + Ok(ErrorResponse { + status_code: res.status_code, + code: response.error_code, + message: response + .error_description + .clone() + .unwrap_or(consts::NO_ERROR_MESSAGE.to_owned()), + reason: response.error_description, + attempt_status: None, + connector_transaction_id: None, + }) + } + Err(err) => { + router_env::logger::error!(deserialization_error =? err); + utils::handle_json_response_deserialization_failure( + res, + "threedsecureio".to_owned(), + ) + } + } + } +} + +impl ConnectorValidation for Threedsecureio {} + +impl ConnectorIntegration + for Threedsecureio +{ +} + +impl ConnectorIntegration + for Threedsecureio +{ +} + +impl + ConnectorIntegration< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for Threedsecureio +{ +} + +impl ConnectorIntegration + for Threedsecureio +{ +} + +impl ConnectorIntegration + for Threedsecureio +{ +} + +impl ConnectorIntegration + for Threedsecureio +{ +} + +impl ConnectorIntegration + for Threedsecureio +{ +} + +impl ConnectorIntegration + for Threedsecureio +{ +} + +impl ConnectorIntegration + for Threedsecureio +{ +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Threedsecureio { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} + +impl api::ConnectorPreAuthentication for Threedsecureio {} +impl api::ExternalAuthentication for Threedsecureio {} +impl api::ConnectorAuthentication for Threedsecureio {} +impl api::ConnectorPostAuthentication for Threedsecureio {} + +impl + ConnectorIntegration< + api::Authentication, + types::authentication::ConnectorAuthenticationRequestData, + types::authentication::AuthenticationResponseData, + > for Threedsecureio +{ + fn get_headers( + &self, + req: &types::authentication::ConnectorAuthenticationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::authentication::ConnectorAuthenticationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}/auth", self.base_url(connectors),)) + } + + fn get_request_body( + &self, + req: &types::authentication::ConnectorAuthenticationRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = threedsecureio::ThreedsecureioRouterData::try_from(( + &self.get_currency_unit(), + req.request + .currency + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })?, + req.request + .amount + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "amount", + })?, + req, + ))?; + let req_obj = + threedsecureio::ThreedsecureioAuthenticationRequest::try_from(&connector_router_data); + Ok(RequestContent::Json(Box::new(req_obj?))) + } + + fn build_request( + &self, + req: &types::authentication::ConnectorAuthenticationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url( + &types::authentication::ConnectorAuthenticationType::get_url( + self, req, connectors, + )?, + ) + .attach_default_headers() + .headers( + types::authentication::ConnectorAuthenticationType::get_headers( + self, req, connectors, + )?, + ) + .set_body( + types::authentication::ConnectorAuthenticationType::get_request_body( + self, req, connectors, + )?, + ) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::authentication::ConnectorAuthenticationRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult< + types::authentication::ConnectorAuthenticationRouterData, + errors::ConnectorError, + > { + let response = res + .response + .parse_struct("ThreedsecureioAuthenticationResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl + ConnectorIntegration< + api::PreAuthentication, + types::authentication::PreAuthNRequestData, + types::authentication::AuthenticationResponseData, + > for Threedsecureio +{ + fn get_headers( + &self, + req: &types::authentication::PreAuthNRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::authentication::PreAuthNRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}/preauth", self.base_url(connectors),)) + } + + fn get_request_body( + &self, + req: &types::authentication::PreAuthNRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let connector_router_data = threedsecureio::ThreedsecureioRouterData::try_from((0, req))?; + let req_obj = threedsecureio::ThreedsecureioPreAuthenticationRequest::try_from( + &connector_router_data, + )?; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &types::authentication::PreAuthNRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url( + &types::authentication::ConnectorPreAuthenticationType::get_url( + self, req, connectors, + )?, + ) + .attach_default_headers() + .headers( + types::authentication::ConnectorPreAuthenticationType::get_headers( + self, req, connectors, + )?, + ) + .set_body( + types::authentication::ConnectorPreAuthenticationType::get_request_body( + self, req, connectors, + )?, + ) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::authentication::PreAuthNRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: threedsecureio::ThreedsecureioPreAuthenticationResponse = res + .response + .parse_struct("threedsecureio ThreedsecureioPreAuthenticationResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + // Ok(types::authentication::PreAuthNRouterData { + // response, + // ..data.clone() + // }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl + ConnectorIntegration< + api::PostAuthentication, + types::authentication::ConnectorPostAuthenticationRequestData, + types::authentication::AuthenticationResponseData, + > for Threedsecureio +{ + fn get_headers( + &self, + req: &types::authentication::ConnectorPostAuthenticationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::authentication::ConnectorPostAuthenticationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}/postauth", self.base_url(connectors),)) + } + + fn get_request_body( + &self, + req: &types::authentication::ConnectorPostAuthenticationRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult { + let req_obj = threedsecureio::ThreedsecureioPostAuthenticationRequest { + three_ds_server_trans_id: req + .request + .authentication_data + .threeds_server_transaction_id + .clone(), + }; + Ok(RequestContent::Json(Box::new(req_obj))) + } + + fn build_request( + &self, + req: &types::authentication::ConnectorPostAuthenticationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url( + &types::authentication::ConnectorPostAuthenticationType::get_url( + self, req, connectors, + )?, + ) + .attach_default_headers() + .headers( + types::authentication::ConnectorPostAuthenticationType::get_headers( + self, req, connectors, + )?, + ) + .set_body( + types::authentication::ConnectorPostAuthenticationType::get_request_body( + self, req, connectors, + )?, + ) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::authentication::ConnectorPostAuthenticationRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult< + types::authentication::ConnectorPostAuthenticationRouterData, + errors::ConnectorError, + > { + let response: threedsecureio::ThreedsecureioPostAuthenticationResponse = res + .response + .parse_struct("threedsecureio PaymentsSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + Ok( + types::authentication::ConnectorPostAuthenticationRouterData { + response: Ok( + types::authentication::AuthenticationResponseData::PostAuthNResponse { + trans_status: response.trans_status.into(), + authentication_value: response.authentication_value, + eci: response.eci, + }, + ), + ..data.clone() + }, + ) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} diff --git a/crates/router/src/connector/threedsecureio/transformers.rs b/crates/router/src/connector/threedsecureio/transformers.rs new file mode 100644 index 0000000000..b61196ab9d --- /dev/null +++ b/crates/router/src/connector/threedsecureio/transformers.rs @@ -0,0 +1,747 @@ +use api_models::payments::{DeviceChannel, ThreeDsCompletionIndicator}; +use base64::Engine; +use common_utils::date_time; +use error_stack::{report, IntoReport, ResultExt}; +use iso_currency::Currency; +use isocountry; +use masking::{ExposeInterface, Secret}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, to_string}; + +use crate::{ + connector::utils::{to_connector_meta, AddressDetailsData, CardData, SELECTED_PAYMENT_METHOD}, + consts::{BASE64_ENGINE, NO_ERROR_MESSAGE}, + core::errors, + types::{ + self, + api::{self, MessageCategory}, + authentication::ChallengeParams, + transformers::ForeignTryFrom, + }, + utils::OptionExt, +}; + +pub struct ThreedsecureioRouterData { + pub amount: String, + pub router_data: T, +} + +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for ThreedsecureioRouterData +{ + type Error = error_stack::Report; + fn try_from( + (_currency_unit, _currency, amount, item): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + Ok(Self { + amount: amount.to_string(), + router_data: item, + }) + } +} + +impl TryFrom<(i64, T)> for ThreedsecureioRouterData { + type Error = error_stack::Report; + fn try_from((amount, router_data): (i64, T)) -> Result { + Ok(Self { + amount: amount.to_string(), + router_data, + }) + } +} + +impl + TryFrom< + types::ResponseRouterData< + api::PreAuthentication, + ThreedsecureioPreAuthenticationResponse, + types::authentication::PreAuthNRequestData, + types::authentication::AuthenticationResponseData, + >, + > for types::authentication::PreAuthNRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + api::PreAuthentication, + ThreedsecureioPreAuthenticationResponse, + types::authentication::PreAuthNRequestData, + types::authentication::AuthenticationResponseData, + >, + ) -> Result { + let response = match item.response { + ThreedsecureioPreAuthenticationResponse::Success(pre_authn_response) => { + let three_ds_method_data = json!({ + "threeDSServerTransID": pre_authn_response.threeds_server_trans_id, + }); + let three_ds_method_data_str = to_string(&three_ds_method_data) + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .attach_printable("error while constructing three_ds_method_data_str")?; + let three_ds_method_data_base64 = BASE64_ENGINE.encode(three_ds_method_data_str); + let connector_metadata = serde_json::json!(ThreeDSecureIoConnectorMetaData { + ds_start_protocol_version: pre_authn_response.ds_start_protocol_version, + ds_end_protocol_version: pre_authn_response.ds_end_protocol_version, + acs_start_protocol_version: pre_authn_response.acs_start_protocol_version, + acs_end_protocol_version: pre_authn_response.acs_end_protocol_version.clone(), + }); + Ok( + types::authentication::AuthenticationResponseData::PreAuthNResponse { + threeds_server_transaction_id: pre_authn_response + .threeds_server_trans_id + .clone(), + maximum_supported_3ds_version: ForeignTryFrom::foreign_try_from( + pre_authn_response.acs_end_protocol_version.clone(), + )?, + connector_authentication_id: pre_authn_response.threeds_server_trans_id, + three_ds_method_data: three_ds_method_data_base64, + three_ds_method_url: pre_authn_response.threeds_method_url, + message_version: pre_authn_response.acs_end_protocol_version.clone(), + connector_metadata: Some(connector_metadata), + }, + ) + } + ThreedsecureioPreAuthenticationResponse::Failure(error_response) => { + Err(types::ErrorResponse { + code: error_response.error_code, + message: error_response + .error_description + .clone() + .unwrap_or(NO_ERROR_MESSAGE.to_owned()), + reason: error_response.error_description, + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: None, + }) + } + }; + Ok(Self { + response, + ..item.data.clone() + }) + } +} + +impl + TryFrom< + types::ResponseRouterData< + api::Authentication, + ThreedsecureioAuthenticationResponse, + types::authentication::ConnectorAuthenticationRequestData, + types::authentication::AuthenticationResponseData, + >, + > for types::authentication::ConnectorAuthenticationRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + api::Authentication, + ThreedsecureioAuthenticationResponse, + types::authentication::ConnectorAuthenticationRequestData, + types::authentication::AuthenticationResponseData, + >, + ) -> Result { + let response = match item.response { + ThreedsecureioAuthenticationResponse::Success(response) => { + let creq = serde_json::json!({ + "threeDSServerTransID": response.three_dsserver_trans_id, + "acsTransID": response.acs_trans_id, + "messageVersion": response.message_version, + "messageType": "CReq", + "challengeWindowSize": "01", + }); + let creq_str = to_string(&creq) + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .attach_printable("error while constructing creq_str")?; + let creq_base64 = base64::Engine::encode(&BASE64_ENGINE, creq_str) + .trim_end_matches('=') + .to_owned(); + Ok( + types::authentication::AuthenticationResponseData::AuthNResponse { + trans_status: response.trans_status.clone().into(), + authn_flow_type: if response.trans_status == ThreedsecureioTransStatus::C { + types::authentication::AuthNFlowType::Challenge(Box::new( + ChallengeParams { + acs_url: response.acs_url, + challenge_request: Some(creq_base64), + acs_reference_number: Some( + response.acs_reference_number.clone(), + ), + acs_trans_id: Some(response.acs_trans_id.clone()), + three_dsserver_trans_id: Some(response.three_dsserver_trans_id), + acs_signed_content: response.acs_signed_content, + }, + )) + } else { + types::authentication::AuthNFlowType::Frictionless + }, + authentication_value: response.authentication_value, + }, + ) + } + ThreedsecureioAuthenticationResponse::Error(err_response) => match *err_response { + ThreedsecureioErrorResponseWrapper::ErrorResponse(resp) => { + Err(types::ErrorResponse { + code: resp.error_code, + message: resp + .error_description + .clone() + .unwrap_or(NO_ERROR_MESSAGE.to_owned()), + reason: resp.error_description, + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: None, + }) + } + ThreedsecureioErrorResponseWrapper::ErrorString(error) => { + Err(types::ErrorResponse { + code: error.clone(), + message: error.clone(), + reason: Some(error), + status_code: item.http_code, + attempt_status: None, + connector_transaction_id: None, + }) + } + }, + }; + Ok(Self { + response, + ..item.data.clone() + }) + } +} + +pub struct ThreedsecureioAuthType { + pub(super) api_key: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for ThreedsecureioAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { + api_key: api_key.to_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} + +fn get_card_details( + payment_method_data: api_models::payments::PaymentMethodData, +) -> Result { + match payment_method_data { + api_models::payments::PaymentMethodData::Card(details) => Ok(details), + _ => Err(errors::ConnectorError::NotSupported { + message: SELECTED_PAYMENT_METHOD.to_string(), + connector: "threedsecureio", + })?, + } +} + +impl TryFrom<&ThreedsecureioRouterData<&types::authentication::ConnectorAuthenticationRouterData>> + for ThreedsecureioAuthenticationRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &ThreedsecureioRouterData<&types::authentication::ConnectorAuthenticationRouterData>, + ) -> Result { + let request = &item.router_data.request; + //browser_details are mandatory for Browser flows + let browser_details = match request.browser_details.clone() { + Some(details) => Ok::, Self::Error>(Some(details)), + None => { + if request.device_channel == DeviceChannel::Browser { + Err(errors::ConnectorError::MissingRequiredField { + field_name: "browser_info", + })? + } else { + Ok(None) + } + } + }?; + let card_details = get_card_details(request.payment_method_data.clone())?; + let currency = request + .currency + .map(|currency| currency.to_string()) + .ok_or(errors::ConnectorError::RequestEncodingFailed) + .into_report() + .attach_printable("missing field currency")?; + let purchase_currency: Currency = iso_currency::Currency::from_code(¤cy) + .ok_or(errors::ConnectorError::RequestEncodingFailed) + .into_report() + .attach_printable("error while parsing Currency")?; + let billing_address = request.billing_address.address.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "billing_address.address", + }, + )?; + let billing_state = billing_address.clone().to_state_code()?; + let billing_country = isocountry::CountryCode::for_alpha2( + &billing_address + .country + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "billing_address.address.country", + })? + .to_string(), + ) + .into_report() + .change_context(errors::ConnectorError::RequestEncodingFailed) + .attach_printable("Error parsing billing_address.address.country")?; + let connector_meta_data: ThreeDSecureIoMetaData = item + .router_data + .connector_meta_data + .clone() + .parse_value("ThreeDSecureIoMetaData") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + let authentication_data = &request.authentication_data.0; + let sdk_information = match request.device_channel { + DeviceChannel::App => Some(item.router_data.request.sdk_information.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "sdk_information", + }, + )?), + DeviceChannel::Browser => None, + }; + let acquirer_details = authentication_data + .acquirer_details + .clone() + .get_required_value("acquirer_details") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "acquirer_details", + })?; + let meta: ThreeDSecureIoConnectorMetaData = + to_connector_meta(request.authentication_data.1.connector_metadata.clone())?; + Ok(Self { + ds_start_protocol_version: meta.ds_start_protocol_version.clone(), + ds_end_protocol_version: meta.ds_end_protocol_version.clone(), + acs_start_protocol_version: meta.acs_start_protocol_version.clone(), + acs_end_protocol_version: meta.acs_end_protocol_version.clone(), + three_dsserver_trans_id: authentication_data.threeds_server_transaction_id.clone(), + acct_number: card_details.card_number.clone(), + notification_url: request + .return_url + .clone() + .ok_or(errors::ConnectorError::RequestEncodingFailed) + .into_report() + .attach_printable("missing return_url")?, + three_dscomp_ind: ThreeDSecureIoThreeDsCompletionIndicator::from( + request.threeds_method_comp_ind.clone(), + ), + three_dsrequestor_url: request.three_ds_requestor_url.clone(), + acquirer_bin: acquirer_details.acquirer_bin, + acquirer_merchant_id: acquirer_details.acquirer_merchant_id, + card_expiry_date: card_details.get_expiry_date_as_yymm()?.expose(), + bill_addr_city: billing_address + .city + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "billing_address.address.city", + })? + .to_string(), + bill_addr_country: billing_country.numeric_id().to_string().into(), + bill_addr_line1: billing_address.line1.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "billing_address.address.line1", + }, + )?, + bill_addr_post_code: billing_address.zip.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "billing_address.address.zip", + }, + )?, + bill_addr_state: billing_state, + // Indicates the type of Authentication request, "01" for Payment transaction + three_dsrequestor_authentication_ind: "01".to_string(), + device_channel: match item.router_data.request.device_channel.clone() { + DeviceChannel::App => "01", + DeviceChannel::Browser => "02", + } + .to_string(), + message_category: match item.router_data.request.message_category.clone() { + MessageCategory::Payment => "01", + MessageCategory::NonPayment => "02", + } + .to_string(), + browser_javascript_enabled: browser_details + .as_ref() + .and_then(|details| details.java_script_enabled), + browser_accept_header: browser_details + .as_ref() + .and_then(|details| details.accept_header.clone()), + browser_ip: browser_details + .clone() + .and_then(|details| details.ip_address.map(|ip| Secret::new(ip.to_string()))), + browser_java_enabled: browser_details + .as_ref() + .and_then(|details| details.java_enabled), + browser_language: browser_details + .as_ref() + .and_then(|details| details.language.clone()), + browser_color_depth: browser_details + .as_ref() + .and_then(|details| details.color_depth.map(|a| a.to_string())), + browser_screen_height: browser_details + .as_ref() + .and_then(|details| details.screen_height.map(|a| a.to_string())), + browser_screen_width: browser_details + .as_ref() + .and_then(|details| details.screen_width.map(|a| a.to_string())), + browser_tz: browser_details + .as_ref() + .and_then(|details| details.time_zone.map(|a| a.to_string())), + browser_user_agent: browser_details + .as_ref() + .and_then(|details| details.user_agent.clone().map(|a| a.to_string())), + mcc: connector_meta_data.mcc, + merchant_country_code: connector_meta_data.merchant_country_code, + merchant_name: connector_meta_data.merchant_name, + message_type: "AReq".to_string(), + message_version: authentication_data.message_version.clone(), + purchase_amount: item.amount.clone(), + purchase_currency: purchase_currency.numeric().to_string(), + trans_type: "01".to_string(), + purchase_exponent: purchase_currency + .exponent() + .ok_or(errors::ConnectorError::RequestEncodingFailed) + .into_report() + .attach_printable("missing purchase_exponent")? + .to_string(), + purchase_date: date_time::DateTime::::from(date_time::now()) + .to_string(), + sdk_app_id: sdk_information + .as_ref() + .map(|sdk_info| sdk_info.sdk_app_id.clone()), + sdk_enc_data: sdk_information + .as_ref() + .map(|sdk_info| sdk_info.sdk_enc_data.clone()), + sdk_ephem_pub_key: sdk_information + .as_ref() + .map(|sdk_info| sdk_info.sdk_ephem_pub_key.clone()), + sdk_reference_number: sdk_information + .as_ref() + .map(|sdk_info| sdk_info.sdk_reference_number.clone()), + sdk_trans_id: sdk_information + .as_ref() + .map(|sdk_info| sdk_info.sdk_trans_id.clone()), + sdk_max_timeout: sdk_information + .as_ref() + .map(|sdk_info| sdk_info.sdk_max_timeout.to_string()), + device_render_options: match request.device_channel { + DeviceChannel::App => Some(DeviceRenderOptions { + // SDK Interface types that the device supports for displaying specific challenge user interfaces within the SDK, 01 for Native + sdk_interface: "01".to_string(), + // UI types that the device supports for displaying specific challenge user interfaces within the SDK, 01 for Text + sdk_ui_type: vec!["01".to_string()], + }), + DeviceChannel::Browser => None, + }, + cardholder_name: card_details.card_holder_name, + email: request.email.clone(), + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ThreedsecureioErrorResponse { + pub error_code: String, + pub error_component: Option, + pub error_description: Option, + pub error_detail: Option, + pub error_message_type: Option, + pub message_type: Option, + pub message_version: Option, + pub three_dsserver_trans_id: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum ThreedsecureioErrorResponseWrapper { + ErrorResponse(ThreedsecureioErrorResponse), + ErrorString(String), +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ThreedsecureioAuthenticationResponse { + Success(Box), + Error(Box), +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ThreedsecureioAuthenticationSuccessResponse { + #[serde(rename = "acsChallengeMandated")] + pub acs_challenge_mandated: Option, + #[serde(rename = "acsOperatorID")] + pub acs_operator_id: Option, + #[serde(rename = "acsReferenceNumber")] + pub acs_reference_number: String, + #[serde(rename = "acsTransID")] + pub acs_trans_id: String, + #[serde(rename = "acsURL")] + pub acs_url: Option, + #[serde(rename = "authenticationType")] + pub authentication_type: Option, + #[serde(rename = "dsReferenceNumber")] + pub ds_reference_number: String, + #[serde(rename = "dsTransID")] + pub ds_trans_id: String, + #[serde(rename = "messageType")] + pub message_type: Option, + #[serde(rename = "messageVersion")] + pub message_version: String, + #[serde(rename = "threeDSServerTransID")] + pub three_dsserver_trans_id: String, + #[serde(rename = "transStatus")] + pub trans_status: ThreedsecureioTransStatus, + #[serde(rename = "acsSignedContent")] + pub acs_signed_content: Option, + #[serde(rename = "authenticationValue")] + pub authentication_value: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum ThreeDSecureIoThreeDsCompletionIndicator { + Y, + N, + U, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ThreedsecureioAuthenticationRequest { + pub ds_start_protocol_version: String, + pub ds_end_protocol_version: String, + pub acs_start_protocol_version: String, + pub acs_end_protocol_version: String, + pub three_dsserver_trans_id: String, + pub acct_number: cards::CardNumber, + pub notification_url: String, + pub three_dscomp_ind: ThreeDSecureIoThreeDsCompletionIndicator, + pub three_dsrequestor_url: String, + pub acquirer_bin: String, + pub acquirer_merchant_id: String, + pub card_expiry_date: String, + pub bill_addr_city: String, + pub bill_addr_country: Secret, + pub bill_addr_line1: Secret, + pub bill_addr_post_code: Secret, + pub bill_addr_state: Secret, + pub email: Option, + pub three_dsrequestor_authentication_ind: String, + pub cardholder_name: Option>, + pub device_channel: String, + pub browser_javascript_enabled: Option, + pub browser_accept_header: Option, + pub browser_ip: Option>, + pub browser_java_enabled: Option, + pub browser_language: Option, + pub browser_color_depth: Option, + pub browser_screen_height: Option, + pub browser_screen_width: Option, + pub browser_tz: Option, + pub browser_user_agent: Option, + pub sdk_app_id: Option, + pub sdk_enc_data: Option, + pub sdk_ephem_pub_key: Option>, + pub sdk_reference_number: Option, + pub sdk_trans_id: Option, + pub mcc: String, + pub merchant_country_code: String, + pub merchant_name: String, + pub message_category: String, + pub message_type: String, + pub message_version: String, + pub purchase_amount: String, + pub purchase_currency: String, + pub purchase_exponent: String, + pub purchase_date: String, + pub trans_type: String, + pub sdk_max_timeout: Option, + pub device_render_options: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ThreeDSecureIoMetaData { + pub mcc: String, + pub merchant_country_code: String, + pub merchant_name: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ThreeDSecureIoConnectorMetaData { + pub ds_start_protocol_version: String, + pub ds_end_protocol_version: String, + pub acs_start_protocol_version: String, + pub acs_end_protocol_version: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeviceRenderOptions { + pub sdk_interface: String, + pub sdk_ui_type: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ThreedsecureioPreAuthenticationRequest { + acct_number: cards::CardNumber, + ds: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ThreedsecureioPostAuthenticationRequest { + pub three_ds_server_trans_id: String, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ThreedsecureioPostAuthenticationResponse { + pub authentication_value: Option, + pub trans_status: ThreedsecureioTransStatus, + pub eci: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub enum ThreedsecureioTransStatus { + /// Authentication/ Account Verification Successful + Y, + /// Not Authenticated /Account Not Verified; Transaction denied + N, + /// Authentication/ Account Verification Could Not Be Performed; Technical or other problem, as indicated in ARes or RReq + U, + /// Attempts Processing Performed; Not Authenticated/Verified , but a proof of attempted authentication/verification is provided + A, + /// Authentication/ Account Verification Rejected; Issuer is rejecting authentication/verification and request that authorisation not be attempted. + R, + C, +} + +impl From for ThreeDSecureIoThreeDsCompletionIndicator { + fn from(value: ThreeDsCompletionIndicator) -> Self { + match value { + ThreeDsCompletionIndicator::Success => Self::Y, + ThreeDsCompletionIndicator::Failure => Self::N, + ThreeDsCompletionIndicator::NotAvailable => Self::U, + } + } +} + +impl From for api_models::payments::TransactionStatus { + fn from(value: ThreedsecureioTransStatus) -> Self { + match value { + ThreedsecureioTransStatus::Y => Self::Success, + ThreedsecureioTransStatus::N => Self::Failure, + ThreedsecureioTransStatus::U => Self::VerificationNotPerformed, + ThreedsecureioTransStatus::A => Self::NotVerified, + ThreedsecureioTransStatus::R => Self::Rejected, + ThreedsecureioTransStatus::C => Self::ChallengeRequired, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum DirectoryServer { + Standin, + Visa, + Mastercard, + Jcb, + Upi, + Amex, + Protectbuy, + Sbn, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum ThreedsecureioPreAuthenticationResponse { + Success(Box), + Failure(Box), +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ThreedsecureioPreAuthenticationResponseData { + pub ds_start_protocol_version: String, + pub ds_end_protocol_version: String, + pub acs_start_protocol_version: String, + pub acs_end_protocol_version: String, + #[serde(rename = "threeDSMethodURL")] + pub threeds_method_url: Option, + #[serde(rename = "threeDSServerTransID")] + pub threeds_server_trans_id: String, + pub scheme: Option, + pub message_type: Option, +} + +impl TryFrom<&ThreedsecureioRouterData<&types::authentication::PreAuthNRouterData>> + for ThreedsecureioPreAuthenticationRequest +{ + type Error = error_stack::Report; + + fn try_from( + value: &ThreedsecureioRouterData<&types::authentication::PreAuthNRouterData>, + ) -> Result { + let router_data = value.router_data; + Ok(Self { + acct_number: router_data.request.card_holder_account_number.clone(), + ds: None, + }) + } +} + +impl ForeignTryFrom for (i64, i64, i64) { + type Error = error_stack::Report; + fn foreign_try_from(value: String) -> Result { + let mut split_version = value.split('.'); + let version_string = { + let major_version = split_version.next().ok_or(report!( + errors::ConnectorError::ResponseDeserializationFailed + ))?; + let minor_version = split_version.next().ok_or(report!( + errors::ConnectorError::ResponseDeserializationFailed + ))?; + let patch_version = split_version.next().ok_or(report!( + errors::ConnectorError::ResponseDeserializationFailed + ))?; + (major_version, minor_version, patch_version) + }; + let int_representation = { + let major_version = version_string + .0 + .parse() + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let minor_version = version_string + .1 + .parse() + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let patch_version = version_string + .2 + .parse() + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + (major_version, minor_version, patch_version) + }; + Ok(int_representation) + } +} diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 8a12fee5da..0eaabf8652 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1922,6 +1922,10 @@ pub(crate) fn validate_auth_and_metadata_type( PlaidAuthType::foreign_try_from(val)?; Ok(()) } + api_enums::Connector::Threedsecureio => { + threedsecureio::transformers::ThreedsecureioAuthType::try_from(val)?; + Ok(()) + } } } diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index eac16fa339..fe4a504861 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -145,6 +145,7 @@ impl } default_imp_for_complete_authorize!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Bitpay, @@ -210,6 +211,7 @@ impl { } default_imp_for_webhook_source_verification!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -292,6 +294,7 @@ impl } default_imp_for_create_customer!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -374,6 +377,7 @@ impl services::ConnectorRedirectResponse for connector::DummyConnec } default_imp_for_connector_redirect_response!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Bitpay, @@ -425,6 +429,7 @@ macro_rules! default_imp_for_connector_request_id { impl api::ConnectorTransactionId for connector::DummyConnector {} default_imp_for_connector_request_id!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -509,6 +514,7 @@ impl } default_imp_for_accept_dispute!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -613,6 +619,7 @@ impl } default_imp_for_file_upload!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -694,6 +701,7 @@ impl } default_imp_for_submit_evidence!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -775,6 +783,7 @@ impl } default_imp_for_defend_dispute!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -857,6 +866,7 @@ impl } default_imp_for_pre_processing_steps!( + connector::Threedsecureio, connector::Aci, connector::Airwallex, connector::Authorizedotnet, @@ -915,6 +925,7 @@ macro_rules! default_imp_for_payouts { impl api::Payouts for connector::DummyConnector {} default_imp_for_payouts!( + connector::Threedsecureio, connector::Aci, connector::Airwallex, connector::Authorizedotnet, @@ -997,6 +1008,7 @@ impl #[cfg(feature = "payouts")] default_imp_for_payouts_create!( + connector::Threedsecureio, connector::Aci, connector::Airwallex, connector::Authorizedotnet, @@ -1082,6 +1094,7 @@ impl #[cfg(feature = "payouts")] default_imp_for_payouts_eligibility!( + connector::Threedsecureio, connector::Aci, connector::Airwallex, connector::Authorizedotnet, @@ -1164,6 +1177,7 @@ impl #[cfg(feature = "payouts")] default_imp_for_payouts_fulfill!( + connector::Threedsecureio, connector::Aci, connector::Airwallex, connector::Authorizedotnet, @@ -1246,6 +1260,7 @@ impl #[cfg(feature = "payouts")] default_imp_for_payouts_cancel!( + connector::Threedsecureio, connector::Aci, connector::Airwallex, connector::Authorizedotnet, @@ -1328,6 +1343,7 @@ impl #[cfg(feature = "payouts")] default_imp_for_payouts_quote!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -1411,6 +1427,7 @@ impl #[cfg(feature = "payouts")] default_imp_for_payouts_recipient!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -1493,6 +1510,7 @@ impl } default_imp_for_approve!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -1576,6 +1594,7 @@ impl } default_imp_for_reject!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -1643,6 +1662,7 @@ macro_rules! default_imp_for_fraud_check { impl api::FraudCheck for connector::DummyConnector {} default_imp_for_fraud_check!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -1726,6 +1746,7 @@ impl #[cfg(feature = "frm")] default_imp_for_frm_sale!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -1809,6 +1830,7 @@ impl #[cfg(feature = "frm")] default_imp_for_frm_checkout!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -1892,6 +1914,7 @@ impl #[cfg(feature = "frm")] default_imp_for_frm_transaction!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -1975,6 +1998,7 @@ impl #[cfg(feature = "frm")] default_imp_for_frm_fulfillment!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -2058,6 +2082,7 @@ impl #[cfg(feature = "frm")] default_imp_for_frm_record_return!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -2139,6 +2164,7 @@ impl } default_imp_for_incremental_authorization!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, @@ -2219,6 +2245,7 @@ impl { } default_imp_for_revoking_mandates!( + connector::Threedsecureio, connector::Aci, connector::Adyen, connector::Airwallex, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 9b9e242431..50d256b034 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -382,7 +382,8 @@ impl ConnectorData { enums::Connector::Zen => Ok(Box::new(&connector::Zen)), enums::Connector::Signifyd | enums::Connector::Plaid - | enums::Connector::Riskified => { + | enums::Connector::Riskified + | enums::Connector::Threedsecureio => { Err(report!(errors::ConnectorError::InvalidConnectorName) .attach_printable(format!("invalid connector name: {connector_name}"))) .change_context(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/types/api/authentication.rs b/crates/router/src/types/api/authentication.rs index ab00872fe8..f98688c559 100644 --- a/crates/router/src/types/api/authentication.rs +++ b/crates/router/src/types/api/authentication.rs @@ -15,7 +15,7 @@ pub struct Authentication; #[derive(Debug, Clone)] pub struct PostAuthentication; -use crate::{services, types}; +use crate::{connector, services, types}; #[derive(Clone, serde::Deserialize, Debug, serde::Serialize)] pub struct AcquirerDetails { @@ -106,12 +106,7 @@ impl AuthenticationConnectorData { ) -> CustomResult { match connector_name { enums::AuthenticationConnectors::Threedsecureio => { - Err(errors::ApiErrorResponse::NotImplemented { - message: errors::NotImplementedMessage::Reason( - "external 3ds authentication is not fully implemented".to_string(), - ), - } - .into()) + Ok(Box::new(&connector::Threedsecureio)) } } } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index f30682500a..29f924fc65 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -262,6 +262,12 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::DummyConnector6 => Self::DummyConnector6, #[cfg(feature = "dummy_connector")] api_enums::Connector::DummyConnector7 => Self::DummyConnector7, + api_enums::Connector::Threedsecureio => { + Err(common_utils::errors::ValidationError::InvalidValue { + message: "threedsecureio is not a routable connector".to_string(), + }) + .into_report()? + } }) } } diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 68cf6f6803..3f78ff7f49 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -193,3 +193,8 @@ api_secret = "Secret key" [placetopay] api_key= "Login" key1= "Trankey" + + +[threedsecureio] +api_key="API Key" + diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index d95d6ed94f..a2753a2c51 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -56,6 +56,7 @@ pub struct ConnectorAuthentication { pub square: Option, pub stax: Option, pub stripe: Option, + pub threedsecureio: Option, pub stripe_au: Option, pub stripe_uk: Option, pub trustpay: Option, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 8ab6d33c98..98a2613f3f 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -117,6 +117,7 @@ square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" stripe.base_url = "https://api.stripe.com/" +threedsecureio.base_url = "https://service.sandbox.3dsecure.io" stripe.base_url_file_upload = "https://files.stripe.com/" trustpay.base_url = "https://test-tpgw.trustpay.eu/" trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" @@ -177,6 +178,7 @@ cards = [ "square", "stax", "stripe", + "threedsecureio", "trustpay", "tsys", "volt", diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 4949d31b53..4f6a4410cb 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -6875,6 +6875,7 @@ "square", "stax", "stripe", + "threedsecureio", "trustpay", "tsys", "volt", diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index c1f59cb363..2be49d123b 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -6,7 +6,7 @@ function find_prev_connector() { git checkout $self cp $self $self.tmp # Add new connector to existing list and sort it - connectors=(aci adyen airwallex applepay authorizedotnet bambora bankofamerica bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector fiserv forte globalpay globepay gocardless helcim iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu placetopay powertranz prophetpay rapyd shift4 square stax stripe trustpay tsys volt wise worldline worldpay "$1") + connectors=(aci adyen airwallex applepay authorizedotnet bambora bankofamerica bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector fiserv forte globalpay globepay gocardless helcim iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu placetopay powertranz prophetpay rapyd shift4 square stax stripe threedsecureio trustpay tsys volt wise worldline worldpay "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res=`echo ${sorted[@]}` sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp