diff --git a/config/Development.toml b/config/Development.toml index 4a74b76895..723d408265 100644 --- a/config/Development.toml +++ b/config/Development.toml @@ -65,6 +65,7 @@ cards = [ "stripe", "worldline", "worldpay", + "trustpay", ] [refund] @@ -103,6 +104,8 @@ shift4.base_url = "https://api.shift4.com/" stripe.base_url = "https://api.stripe.com/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldpay.base_url = "https://try.access.worldpay.com/" +trustpay.base_url = "https://test-tpgw.trustpay.eu/" +trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" [scheduler] stream = "SCHEDULER_STREAM" diff --git a/config/config.example.toml b/config/config.example.toml index 33923c6eb0..1c24ec16b4 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -144,6 +144,8 @@ shift4.base_url = "https://api.shift4.com/" stripe.base_url = "https://api.stripe.com/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldpay.base_url = "https://try.access.worldpay.com/" +trustpay.base_url = "https://test-tpgw.trustpay.eu/" +trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" # This data is used to call respective connectors for wallets and cards [connectors.supported] diff --git a/config/docker_compose.toml b/config/docker_compose.toml index e7128d3d6b..62f525b0c9 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -89,6 +89,8 @@ shift4.base_url = "https://api.shift4.com/" stripe.base_url = "https://api.stripe.com/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldpay.base_url = "https://try.access.worldpay.com/" +trustpay.base_url = "https://test-tpgw.trustpay.eu/" +trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" [connectors.supported] wallets = ["klarna", "braintree", "applepay"] @@ -112,6 +114,7 @@ cards = [ "stripe", "worldline", "worldpay", + "trustpay", ] diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index fbef9cd240..2b876972b9 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -539,6 +539,7 @@ pub enum MandateStatus { strum::Display, strum::EnumString, frunk::LabelledGeneric, + Hash, )] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] @@ -567,11 +568,18 @@ pub enum Connector { Stripe, Worldline, Worldpay, + Trustpay, } impl Connector { - pub fn supports_access_token(&self) -> bool { - matches!(self, Self::Airwallex | Self::Globalpay | Self::Payu) + pub fn supports_access_token(&self, payment_method: PaymentMethod) -> bool { + matches!( + (self, payment_method), + (Self::Airwallex, _) + | (Self::Globalpay, _) + | (Self::Payu, _) + | (Self::Trustpay, PaymentMethod::BankRedirect) + ) } } @@ -611,6 +619,7 @@ pub enum RoutableConnectors { Worldline, Worldpay, Multisafepay, + Trustpay, } /// Wallets which support obtaining session object diff --git a/crates/common_utils/src/ext_traits.rs b/crates/common_utils/src/ext_traits.rs index 101b11920c..f8a62187a1 100644 --- a/crates/common_utils/src/ext_traits.rs +++ b/crates/common_utils/src/ext_traits.rs @@ -44,7 +44,7 @@ where /// Functionality, for specifically encoding `Self` into `String` /// after serialization by using `serde::Serialize` /// - fn encode(&'e self) -> CustomResult + fn url_encode(&'e self) -> CustomResult where Self: Serialize; @@ -103,7 +103,7 @@ where } // Check without two functions can we combine this - fn encode(&'e self) -> CustomResult + fn url_encode(&'e self) -> CustomResult where Self: Serialize, { diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index d889b55a1b..8ed56a1f57 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -251,6 +251,7 @@ pub struct Connectors { pub stripe: ConnectorParams, pub worldline: ConnectorParams, pub worldpay: ConnectorParams, + pub trustpay: ConnectorParamsWithMoreUrls, // Keep this field separate from the remaining fields pub supported: SupportedConnectors, @@ -262,6 +263,13 @@ pub struct ConnectorParams { pub base_url: String, } +#[derive(Debug, Deserialize, Clone, Default)] +#[serde(default)] +pub struct ConnectorParamsWithMoreUrls { + pub base_url: String, + pub base_url_bank_redirects: String, +} + #[derive(Debug, Clone, Deserialize)] #[serde(default)] pub struct SchedulerSettings { diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 116a7b7752..67419ed05b 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -18,6 +18,7 @@ pub mod payu; pub mod rapyd; pub mod shift4; pub mod stripe; +pub mod trustpay; pub mod utils; pub mod worldline; pub mod worldpay; @@ -27,5 +28,6 @@ pub use self::{ authorizedotnet::Authorizedotnet, bambora::Bambora, bluesnap::Bluesnap, braintree::Braintree, checkout::Checkout, cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv, globalpay::Globalpay, klarna::Klarna, multisafepay::Multisafepay, nuvei::Nuvei, payu::Payu, - rapyd::Rapyd, shift4::Shift4, stripe::Stripe, worldline::Worldline, worldpay::Worldpay, + rapyd::Rapyd, shift4::Shift4, stripe::Stripe, trustpay::Trustpay, worldline::Worldline, + worldpay::Worldpay, }; diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index ab508b8eac..1ae8722b67 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -346,7 +346,7 @@ impl req: &types::PaymentsAuthorizeRouterData, ) -> CustomResult, errors::ConnectorError> { let req = stripe::PaymentIntentRequest::try_from(req)?; - let stripe_req = utils::Encode::::encode(&req) + let stripe_req = utils::Encode::::url_encode(&req) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(stripe_req)) } diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs new file mode 100644 index 0000000000..902ebeee4e --- /dev/null +++ b/crates/router/src/connector/trustpay.rs @@ -0,0 +1,630 @@ +mod transformers; + +use std::fmt::Debug; + +use base64::Engine; +use error_stack::{IntoReport, ResultExt}; +use transformers as trustpay; + +use crate::{ + configs::settings, + consts, + core::{ + errors::{self, CustomResult}, + payments, + }, + headers, + services::{self, ConnectorIntegration}, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Trustpay; + +impl ConnectorCommonExt for Trustpay +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + match req.payment_method { + storage_models::enums::PaymentMethod::BankRedirect => { + let token = req + .access_token + .clone() + .ok_or(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![ + ( + headers::CONTENT_TYPE.to_string(), + "application/json".to_owned(), + ), + ( + headers::AUTHORIZATION.to_string(), + format!("Bearer {}", token.token), + ), + ]) + } + _ => { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } + } + } +} + +impl ConnectorCommon for Trustpay { + fn id(&self) -> &'static str { + "trustpay" + } + + fn common_get_content_type(&self) -> &'static str { + "application/x-www-form-urlencoded" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.trustpay.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult, errors::ConnectorError> { + let auth = trustpay::TrustpayAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![(headers::X_API_KEY.to_string(), auth.api_key)]) + } + + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: trustpay::TrustpayErrorResponse = res + .response + .parse_struct("trustpay ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let default_error = trustpay::Errors { + code: 0, + description: consts::NO_ERROR_CODE.to_string(), + }; + Ok(ErrorResponse { + status_code: res.status_code, + code: response.status.to_string(), + message: format!("{:?}", response.errors.first().unwrap_or(&default_error)), + reason: None, + }) + } +} + +impl api::Payment for Trustpay {} + +impl api::PreVerify for Trustpay {} +impl ConnectorIntegration + for Trustpay +{ +} + +impl api::PaymentVoid for Trustpay {} + +impl ConnectorIntegration + for Trustpay +{ +} + +impl api::ConnectorAccessToken for Trustpay {} + +impl ConnectorIntegration + for Trustpay +{ + fn get_url( + &self, + _req: &types::RefreshTokenRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + connectors.trustpay.base_url_bank_redirects, "api/oauth2/token" + )) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_headers( + &self, + req: &types::RefreshTokenRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let auth = trustpay::TrustpayAuthType::try_from(&req.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let auth_value = format!( + "Basic {}", + consts::BASE64_ENGINE.encode(format!("{}:{}", auth.project_id, auth.secret_key)) + ); + Ok(vec![ + ( + headers::CONTENT_TYPE.to_string(), + types::RefreshTokenType::get_content_type(self).to_string(), + ), + (headers::AUTHORIZATION.to_string(), auth_value), + ]) + } + + fn get_request_body( + &self, + req: &types::RefreshTokenRouterData, + ) -> CustomResult, errors::ConnectorError> { + let trustpay_req = + utils::Encode::::convert_and_url_encode(req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(trustpay_req)) + } + + fn build_request( + &self, + req: &types::RefreshTokenRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req = Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .headers(types::RefreshTokenType::get_headers(self, req, connectors)?) + .url(&types::RefreshTokenType::get_url(self, req, connectors)?) + .body(types::RefreshTokenType::get_request_body(self, req)?) + .build(), + ); + Ok(req) + } + + fn handle_response( + &self, + data: &types::RefreshTokenRouterData, + res: Response, + ) -> CustomResult { + let response: trustpay::TrustpayAuthUpdateResponse = res + .response + .parse_struct("trustpay TrustpayAuthUpdateResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: trustpay::TrustpayAccessTokenErrorResponse = res + .response + .parse_struct("Trustpay AccessTokenErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(ErrorResponse { + status_code: res.status_code, + code: response.result_info.result_code.to_string(), + message: response.result_info.additional_info.unwrap_or_default(), + reason: None, + }) + } +} + +impl api::PaymentSync for Trustpay {} +impl ConnectorIntegration + for Trustpay +{ + fn get_headers( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let id = req.request.connector_transaction_id.clone(); + match req.payment_method { + storage_models::enums::PaymentMethod::BankRedirect => Ok(format!( + "{}{}/{}", + connectors.trustpay.base_url_bank_redirects, + "api/Payments/Payment", + id.get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)? + )), + _ => Ok(format!( + "{}{}/{}", + self.base_url(connectors), + "api/v1/instance", + id.get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)? + )), + } + } + + fn build_request( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } + + fn handle_response( + &self, + data: &types::PaymentsSyncRouterData, + res: Response, + ) -> CustomResult { + let response: trustpay::TrustpayPaymentsResponse = res + .response + .parse_struct("trustpay PaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } +} + +impl api::PaymentCapture for Trustpay {} +impl ConnectorIntegration + for Trustpay +{ +} + +impl api::PaymentSession for Trustpay {} + +impl ConnectorIntegration + for Trustpay +{ +} + +impl api::PaymentAuthorize for Trustpay {} + +impl ConnectorIntegration + for Trustpay +{ + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + match req.payment_method { + storage_models::enums::PaymentMethod::BankRedirect => Ok(format!( + "{}{}", + connectors.trustpay.base_url_bank_redirects, "api/Payments/Payment" + )), + _ => Ok(format!( + "{}{}", + self.base_url(connectors), + "api/v1/purchase" + )), + } + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let trustpay_req = trustpay::TrustpayPaymentsRequest::try_from(req)?; + let trustpay_req_string = match req.payment_method { + storage_models::enums::PaymentMethod::BankRedirect => { + utils::Encode::::encode_to_string_of_json( + &trustpay_req, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)? + } + _ => utils::Encode::::url_encode(&trustpay_req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?, + }; + Ok(Some(trustpay_req_string)) + } + + fn build_request( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + res: Response, + ) -> CustomResult { + let response: trustpay::TrustpayPaymentsResponse = res + .response + .parse_struct("trustpay PaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl api::Refund for Trustpay {} +impl api::RefundExecute for Trustpay {} +impl api::RefundSync for Trustpay {} + +impl ConnectorIntegration + for Trustpay +{ + fn get_headers( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + match req.payment_method { + storage_models::enums::PaymentMethod::BankRedirect => Ok(format!( + "{}{}{}{}", + connectors.trustpay.base_url_bank_redirects, + "api/Payments/Payment/", + req.request.connector_transaction_id, + "/Refund" + )), + _ => Ok(format!("{}{}", self.base_url(connectors), "api/v1/Reverse")), + } + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let trustpay_req = trustpay::TrustpayRefundRequest::try_from(req)?; + let trustpay_req_string = match req.payment_method { + storage_models::enums::PaymentMethod::BankRedirect => utils::Encode::< + trustpay::TrustpayRefundRequestBankRedirect, + >::encode_to_string_of_json( + &trustpay_req + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?, + _ => utils::Encode::::url_encode(&trustpay_req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?, + }; + Ok(Some(trustpay_req_string)) + } + + fn build_request( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .body(types::RefundExecuteType::get_request_body(self, req)?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::RefundsRouterData, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + let response: trustpay::RefundResponse = res + .response + .parse_struct("trustpay RefundResponse") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration for Trustpay { + fn get_headers( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let id = req + .request + .connector_refund_id + .to_owned() + .ok_or_else(|| errors::ConnectorError::MissingConnectorRefundID)?; + match req.payment_method { + storage_models::enums::PaymentMethod::BankRedirect => Ok(format!( + "{}{}/{}", + connectors.trustpay.base_url_bank_redirects, "api/Payments/Payment", id + )), + _ => Ok(format!( + "{}{}/{}", + self.base_url(connectors), + "api/v1/instance", + id + )), + } + } + + fn build_request( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::RefundSyncRouterData, + res: Response, + ) -> CustomResult { + let response: trustpay::RefundResponse = res + .response + .parse_struct("trustpay RefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Trustpay { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} + +impl services::ConnectorRedirectResponse for Trustpay { + fn get_flow_type( + &self, + query_params: &str, + ) -> CustomResult { + let query = + serde_urlencoded::from_str::(query_params) + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + crate::logger::debug!(trustpay_redirect_response=?query); + Ok(query.status.map_or( + payments::CallConnectorAction::Trigger, + |status| match status.as_str() { + "SuccessOk" => payments::CallConnectorAction::StatusUpdate( + storage_models::enums::AttemptStatus::Charged, + ), + _ => payments::CallConnectorAction::Trigger, + }, + )) + } +} diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs new file mode 100644 index 0000000000..de0e476d6c --- /dev/null +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -0,0 +1,1070 @@ +use std::collections::HashMap; + +use api_models::payments::BankRedirectData; +use common_utils::{errors::CustomResult, pii::Email}; +use error_stack::ResultExt; +use masking::Secret; +use reqwest::Url; +use serde::{Deserialize, Serialize}; + +use crate::{ + connector::utils::{self, AddressDetailsData, CardData, RouterData}, + consts, + core::errors, + pii::{self}, + services, + types::{self, api, storage::enums, BrowserInformation}, +}; + +pub struct TrustpayAuthType { + pub(super) api_key: String, + pub(super) project_id: String, + pub(super) secret_key: String, +} + +impl TryFrom<&types::ConnectorAuthType> for TrustpayAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + if let types::ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } = auth_type + { + Ok(Self { + api_key: api_key.to_string(), + project_id: key1.to_string(), + secret_key: api_secret.to_string(), + }) + } else { + Err(errors::ConnectorError::FailedToObtainAuthType.into()) + } + } +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] +pub enum TrustpayPaymentMethod { + #[serde(rename = "EPS")] + Eps, + Giropay, + IDeal, + Sofort, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct MerchantIdentification { + pub project_id: String, +} + +#[derive(Default, Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct References { + pub merchant_reference: String, +} + +#[derive(Default, Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct Amount { + pub amount: String, + pub currency: String, +} + +#[derive(Default, Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct Reason { + pub code: String, + pub reject_reason: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct StatusReasonInformation { + pub reason: Reason, +} + +#[derive(Default, Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct BankPaymentInformation { + pub amount: Amount, + pub references: References, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct BankPaymentInformationResponse { + pub status: TrustpayBankRedirectPaymentStatus, + pub status_reason_information: Option, + pub references: ReferencesResponse, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +pub struct CallbackURLs { + pub success: String, + pub cancel: String, + pub error: String, +} + +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct PaymentRequestCards { + pub amount: String, + pub currency: String, + pub pan: Secret, + pub cvv: Secret, + #[serde(rename = "exp")] + pub expiry_date: String, + pub cardholder: Secret, + pub reference: String, + #[serde(rename = "redirectUrl")] + pub redirect_url: String, + #[serde(rename = "billing[city]")] + pub billing_city: String, + #[serde(rename = "billing[country]")] + pub billing_country: String, + #[serde(rename = "billing[street1]")] + pub billing_street1: Secret, + #[serde(rename = "billing[postcode]")] + pub billing_postcode: Secret, + #[serde(rename = "customer[email]")] + pub customer_email: Option>, + #[serde(rename = "customer[ipAddress]")] + pub customer_ip_address: Option, + #[serde(rename = "browser[acceptHeader]")] + pub browser_accept_header: String, + #[serde(rename = "browser[language]")] + pub browser_language: String, + #[serde(rename = "browser[screenHeight]")] + pub browser_screen_height: String, + #[serde(rename = "browser[screenWidth]")] + pub browser_screen_width: String, + #[serde(rename = "browser[timezone]")] + pub browser_timezone: String, + #[serde(rename = "browser[userAgent]")] + pub browser_user_agent: String, + #[serde(rename = "browser[javaEnabled]")] + pub browser_java_enabled: String, + #[serde(rename = "browser[javaScriptEnabled]")] + pub browser_java_script_enabled: String, + #[serde(rename = "browser[screenColorDepth]")] + pub browser_screen_color_depth: String, + #[serde(rename = "browser[challengeWindow]")] + pub browser_challenge_window: String, + #[serde(rename = "browser[paymentAction]")] + pub payment_action: Option, + #[serde(rename = "browser[paymentType]")] + pub payment_type: String, +} + +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PaymentRequestBankRedirect { + pub payment_method: TrustpayPaymentMethod, + pub merchant_identification: MerchantIdentification, + pub payment_information: BankPaymentInformation, + pub callback_urls: CallbackURLs, +} + +#[derive(Debug, Serialize, PartialEq)] +#[serde(untagged)] +pub enum TrustpayPaymentsRequest { + CardsPaymentRequest(Box), + BankRedirectPaymentRequest(Box), +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +pub struct TrustpayMandatoryParams { + pub billing_city: String, + pub billing_country: String, + pub billing_street1: Secret, + pub billing_postcode: Secret, +} + +fn get_trustpay_payment_method(bank_redirection_data: &BankRedirectData) -> TrustpayPaymentMethod { + match bank_redirection_data { + api_models::payments::BankRedirectData::Giropay { .. } => TrustpayPaymentMethod::Giropay, + api_models::payments::BankRedirectData::Eps { .. } => TrustpayPaymentMethod::Eps, + api_models::payments::BankRedirectData::Ideal { .. } => TrustpayPaymentMethod::IDeal, + api_models::payments::BankRedirectData::Sofort { .. } => TrustpayPaymentMethod::Sofort, + } +} + +fn get_mandatory_fields( + item: &types::PaymentsAuthorizeRouterData, +) -> Result> { + let billing_address = item + .get_billing()? + .address + .as_ref() + .ok_or_else(utils::missing_field_err("billing.address"))?; + Ok(TrustpayMandatoryParams { + billing_city: billing_address.get_city()?.to_owned(), + billing_country: billing_address.get_country()?.to_owned(), + billing_street1: billing_address.get_line1()?.to_owned(), + billing_postcode: billing_address.get_zip()?.to_owned(), + }) +} + +fn get_card_request_data( + item: &types::PaymentsAuthorizeRouterData, + browser_info: &BrowserInformation, + params: TrustpayMandatoryParams, + amount: String, + ccard: &api_models::payments::Card, + return_url: String, +) -> TrustpayPaymentsRequest { + TrustpayPaymentsRequest::CardsPaymentRequest(Box::new(PaymentRequestCards { + amount, + currency: item.request.currency.to_string(), + pan: ccard.card_number.clone(), + cvv: ccard.card_cvc.clone(), + expiry_date: ccard.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()), + cardholder: ccard.card_holder_name.clone(), + reference: item.payment_id.clone(), + redirect_url: return_url, + billing_city: params.billing_city, + billing_country: params.billing_country, + billing_street1: params.billing_street1, + billing_postcode: params.billing_postcode, + customer_email: item.request.email.clone(), + customer_ip_address: browser_info.ip_address, + browser_accept_header: browser_info.accept_header.clone(), + browser_language: browser_info.language.clone(), + browser_screen_height: browser_info.screen_height.clone().to_string(), + browser_screen_width: browser_info.screen_width.clone().to_string(), + browser_timezone: browser_info.time_zone.clone().to_string(), + browser_user_agent: browser_info.user_agent.clone(), + browser_java_enabled: browser_info.java_enabled.clone().to_string(), + browser_java_script_enabled: browser_info.java_script_enabled.clone().to_string(), + browser_screen_color_depth: browser_info.color_depth.clone().to_string(), + browser_challenge_window: "1".to_string(), + payment_action: None, + payment_type: "Plain".to_string(), + })) +} + +fn get_bank_redirection_request_data( + item: &types::PaymentsAuthorizeRouterData, + bank_redirection_data: &BankRedirectData, + amount: String, + return_url: String, + auth: TrustpayAuthType, +) -> TrustpayPaymentsRequest { + TrustpayPaymentsRequest::BankRedirectPaymentRequest(Box::new(PaymentRequestBankRedirect { + payment_method: get_trustpay_payment_method(bank_redirection_data), + merchant_identification: MerchantIdentification { + project_id: auth.project_id, + }, + payment_information: BankPaymentInformation { + amount: Amount { + amount, + currency: item.request.currency.to_string(), + }, + references: References { + merchant_reference: item.payment_id.clone(), + }, + }, + callback_urls: CallbackURLs { + success: format!("{}?status=SuccessOk", return_url), + cancel: return_url.clone(), + error: return_url, + }, + })) +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for TrustpayPaymentsRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + let default_browser_info = BrowserInformation { + color_depth: 24, + java_enabled: false, + java_script_enabled: true, + language: "en-US".to_string(), + screen_height: 1080, + screen_width: 1920, + time_zone: 3600, + accept_header: "*".to_string(), + user_agent: "none".to_string(), + ip_address: None, + }; + let browser_info = item + .request + .browser_info + .as_ref() + .unwrap_or(&default_browser_info); + let params = get_mandatory_fields(item)?; + let amount = format!( + "{:.2}", + utils::to_currency_base_unit(item.request.amount, item.request.currency)? + .parse::() + .ok() + .ok_or_else(|| errors::ConnectorError::RequestEncodingFailed)? + ); + let auth = TrustpayAuthType::try_from(&item.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(match item.request.payment_method_data { + api::PaymentMethodData::Card(ref ccard) => Ok(get_card_request_data( + item, + browser_info, + params, + amount, + ccard, + item.get_return_url()?, + )), + api::PaymentMethodData::BankRedirect(ref bank_redirection_data) => { + Ok(get_bank_redirection_request_data( + item, + bank_redirection_data, + amount, + item.get_return_url()?, + auth, + )) + } + _ => Err(errors::ConnectorError::NotImplemented(format!( + "Current Payment Method - {:?}", + item.request.payment_method_data + ))), + }?) + } +} + +fn is_payment_failed(payment_status: &str) -> (bool, &'static str) { + match payment_status { + "100.100.600" => (true, "Empty CVV for VISA, MASTER not allowed"), + "100.350.100" => (true, "Referenced session is rejected (no action possible)."), + "100.380.401" => (true, "User authentication failed."), + "100.380.501" => (true, "Risk management transaction timeout."), + "100.390.103" => (true, "PARes validation failed - problem with signature."), + "100.390.111" => ( + true, + "Communication error to VISA/Mastercard Directory Server", + ), + "100.390.112" => (true, "Technical error in 3D system"), + "100.390.115" => (true, "Authentication failed due to invalid message format"), + "100.390.118" => (true, "Authentication failed due to suspected fraud"), + "100.400.304" => (true, "Invalid input data"), + "200.300.404" => (true, "Invalid or missing parameter"), + "300.100.100" => ( + true, + "Transaction declined (additional customer authentication required)", + ), + "400.001.301" => (true, "Card not enrolled in 3DS"), + "400.001.600" => (true, "Authentication error"), + "400.001.601" => (true, "Transaction declined (auth. declined)"), + "400.001.602" => (true, "Invalid transaction"), + "400.001.603" => (true, "Invalid transaction"), + "700.400.200" => ( + true, + "Cannot refund (refund volume exceeded or tx reversed or invalid workflow)", + ), + "700.500.001" => (true, "Referenced session contains too many transactions"), + "700.500.003" => (true, "Test accounts not allowed in production"), + "800.100.151" => (true, "Transaction declined (invalid card)"), + "800.100.152" => (true, "Transaction declined by authorization system"), + "800.100.153" => (true, "Transaction declined (invalid CVV)"), + "800.100.155" => (true, "Transaction declined (amount exceeds credit)"), + "800.100.157" => (true, "Transaction declined (wrong expiry date)"), + "800.100.162" => (true, "Transaction declined (limit exceeded)"), + "800.100.163" => ( + true, + "Transaction declined (maximum transaction frequency exceeded)", + ), + "800.100.168" => (true, "Transaction declined (restricted card)"), + "800.100.170" => (true, "Transaction declined (transaction not permitted)"), + "800.100.190" => (true, "Transaction declined (invalid configuration data)"), + "800.120.100" => (true, "Rejected by throttling"), + "800.300.401" => (true, "Bin blacklisted"), + "800.700.100" => ( + true, + "Transaction for the same session is currently being processed, please try again later", + ), + "900.100.300" => (true, "Timeout, uncertain result"), + _ => (false, ""), + } +} + +fn is_payment_successful(payment_status: &str) -> CustomResult { + match payment_status { + "000.400.100" => Ok(true), + _ => { + let allowed_prefixes = [ + "000.000.", + "000.100.1", + "000.3", + "000.6", + "000.400.01", + "000.400.02", + "000.400.04", + "000.400.05", + "000.400.06", + "000.400.07", + "000.400.08", + "000.400.09", + ]; + let is_valid = allowed_prefixes + .iter() + .any(|&prefix| payment_status.starts_with(prefix)); + Ok(is_valid) + } + } +} + +fn get_pending_status_based_on_redirect_url(redirect_url: Option) -> enums::AttemptStatus { + match redirect_url { + Some(_url) => enums::AttemptStatus::AuthenticationPending, + None => enums::AttemptStatus::Authorizing, + } +} + +fn get_transaction_status( + payment_status: &str, + redirect_url: Option, +) -> CustomResult<(enums::AttemptStatus, Option), errors::ConnectorError> { + let (is_failed, failure_message) = is_payment_failed(payment_status); + let pending_status = get_pending_status_based_on_redirect_url(redirect_url); + if payment_status == "000.200.000" { + return Ok((pending_status, None)); + } + if is_failed { + return Ok(( + enums::AttemptStatus::AuthorizationFailed, + Some(failure_message.to_string()), + )); + } + if is_payment_successful(payment_status)? { + return Ok((enums::AttemptStatus::Charged, None)); + } + Ok((pending_status, None)) +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +pub enum TrustpayBankRedirectPaymentStatus { + Paid, + Authorized, + Rejected, + Authorizing, + Pending, +} + +impl From for enums::AttemptStatus { + fn from(item: TrustpayBankRedirectPaymentStatus) -> Self { + match item { + TrustpayBankRedirectPaymentStatus::Paid => Self::Charged, + TrustpayBankRedirectPaymentStatus::Rejected => Self::AuthorizationFailed, + TrustpayBankRedirectPaymentStatus::Authorized => Self::Authorized, + TrustpayBankRedirectPaymentStatus::Authorizing => Self::Authorizing, + TrustpayBankRedirectPaymentStatus::Pending => Self::Authorizing, + } + } +} + +impl From for enums::RefundStatus { + fn from(item: TrustpayBankRedirectPaymentStatus) -> Self { + match item { + TrustpayBankRedirectPaymentStatus::Paid => Self::Success, + TrustpayBankRedirectPaymentStatus::Rejected => Self::Failure, + _ => Self::Pending, + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PaymentsResponseCards { + pub status: i64, + pub description: Option, + pub instance_id: String, + pub payment_status: String, + pub payment_description: Option, + pub redirect_url: Option, + pub redirect_params: Option>, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct PaymentsResponseBankRedirect { + pub payment_request_id: i64, + pub gateway_url: Url, + pub payment_result_info: Option, + pub payment_method_response: Option, + pub merchant_identification_response: Option, + pub payment_information_response: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct ErrorResponseBankRedirect { + #[serde(rename = "ResultInfo")] + pub payment_result_info: ResultInfo, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct ReferencesResponse { + pub payment_request_id: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct SyncResponseBankRedirect { + pub payment_information: BankPaymentInformationResponse, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum TrustpayPaymentsResponse { + CardsPayments(Box), + BankRedirectPayments(Box), + BankRedirectSync(Box), + BankRedirectError(Box), +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData< + F, + TrustpayPaymentsResponse, + T, + types::PaymentsResponseData, + >, + ) -> Result { + let (status, error, payment_response_data) = + get_trustpay_response(item.response, item.http_code)?; + Ok(Self { + status, + response: error.map_or_else(|| Ok(payment_response_data), Err), + ..item.data + }) + } +} + +fn handle_cards_response( + response: PaymentsResponseCards, + status_code: u16, +) -> CustomResult< + ( + enums::AttemptStatus, + Option, + types::PaymentsResponseData, + ), + errors::ConnectorError, +> { + let (status, msg) = get_transaction_status( + response.payment_status.as_str(), + response.redirect_url.clone(), + )?; + let form_fields = response + .redirect_params + .unwrap_or(std::collections::HashMap::new()); + let redirection_data = response.redirect_url.map(|url| services::RedirectForm { + endpoint: url.to_string(), + method: services::Method::Post, + form_fields, + }); + let error = if msg.is_some() { + Some(types::ErrorResponse { + code: response.payment_status, + message: msg.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: None, + status_code, + }) + } else { + None + }; + let payment_response_data = types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(response.instance_id), + redirection_data, + mandate_reference: None, + connector_metadata: None, + }; + Ok((status, error, payment_response_data)) +} + +fn handle_bank_redirects_response( + response: PaymentsResponseBankRedirect, +) -> CustomResult< + ( + enums::AttemptStatus, + Option, + types::PaymentsResponseData, + ), + errors::ConnectorError, +> { + let status = enums::AttemptStatus::AuthenticationPending; + let error = None; + let payment_response_data = types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + response.payment_request_id.to_string(), + ), + redirection_data: Some(services::RedirectForm::from(( + response.gateway_url, + services::Method::Get, + ))), + mandate_reference: None, + connector_metadata: None, + }; + Ok((status, error, payment_response_data)) +} + +fn handle_bank_redirects_error_response( + response: ErrorResponseBankRedirect, + status_code: u16, +) -> CustomResult< + ( + enums::AttemptStatus, + Option, + types::PaymentsResponseData, + ), + errors::ConnectorError, +> { + let status = enums::AttemptStatus::AuthorizationFailed; + let error = Some(types::ErrorResponse { + code: response.payment_result_info.result_code.to_string(), + message: response + .payment_result_info + .additional_info + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: None, + status_code, + }); + let payment_response_data = types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + }; + Ok((status, error, payment_response_data)) +} + +fn handle_bank_redirects_sync_response( + response: SyncResponseBankRedirect, + status_code: u16, +) -> CustomResult< + ( + enums::AttemptStatus, + Option, + types::PaymentsResponseData, + ), + errors::ConnectorError, +> { + let status = enums::AttemptStatus::from(response.payment_information.status); + let error = if status == enums::AttemptStatus::AuthorizationFailed { + let reason_info = response + .payment_information + .status_reason_information + .unwrap_or_default(); + Some(types::ErrorResponse { + code: reason_info.reason.code, + message: reason_info + .reason + .reject_reason + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: None, + status_code, + }) + } else { + None + }; + let payment_response_data = types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + response.payment_information.references.payment_request_id, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + }; + Ok((status, error, payment_response_data)) +} + +pub fn get_trustpay_response( + response: TrustpayPaymentsResponse, + status_code: u16, +) -> CustomResult< + ( + enums::AttemptStatus, + Option, + types::PaymentsResponseData, + ), + errors::ConnectorError, +> { + match response { + TrustpayPaymentsResponse::CardsPayments(response) => { + handle_cards_response(*response, status_code) + } + TrustpayPaymentsResponse::BankRedirectPayments(response) => { + handle_bank_redirects_response(*response) + } + TrustpayPaymentsResponse::BankRedirectSync(response) => { + handle_bank_redirects_sync_response(*response, status_code) + } + TrustpayPaymentsResponse::BankRedirectError(response) => { + handle_bank_redirects_error_response(*response, status_code) + } + } +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +pub struct TrustpayAuthUpdateRequest { + pub grant_type: String, +} + +impl TryFrom<&types::RefreshTokenRouterData> for TrustpayAuthUpdateRequest { + type Error = error_stack::Report; + fn try_from(_item: &types::RefreshTokenRouterData) -> Result { + Ok(Self { + grant_type: "client_credentials".to_string(), + }) + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct ResultInfo { + pub result_code: i64, + pub additional_info: Option, + pub correlation_id: Option, +} + +#[derive(Default, Debug, Clone, Deserialize, PartialEq)] +pub struct TrustpayAuthUpdateResponse { + pub access_token: Option, + pub token_type: Option, + pub expires_in: Option, + #[serde(rename = "ResultInfo")] + pub result_info: ResultInfo, +} + +#[derive(Default, Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct TrustpayAccessTokenErrorResponse { + pub result_info: ResultInfo, +} + +impl TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + match (item.response.access_token, item.response.expires_in) { + (Some(access_token), Some(expires_in)) => Ok(Self { + response: Ok(types::AccessToken { + token: access_token, + expires: expires_in, + }), + ..item.data + }), + _ => Ok(Self { + response: Err(types::ErrorResponse { + code: item.response.result_info.result_code.to_string(), + message: item + .response + .result_info + .additional_info + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: None, + status_code: item.http_code, + }), + ..item.data + }), + } + } +} + +#[derive(Default, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TrustpayRefundRequestCards { + instance_id: String, + amount: String, + currency: String, + reference: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TrustpayRefundRequestBankRedirect { + pub merchant_identification: MerchantIdentification, + pub payment_information: BankPaymentInformation, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum TrustpayRefundRequest { + CardsRefund(Box), + BankRedirectRefund(Box), +} + +impl TryFrom<&types::RefundsRouterData> for TrustpayRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundsRouterData) -> Result { + match item.payment_method { + storage_models::enums::PaymentMethod::BankRedirect => { + let auth = TrustpayAuthType::try_from(&item.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let amount = + utils::to_currency_base_unit(item.request.amount, item.request.currency)? + .parse::() + .ok() + .ok_or_else(|| errors::ConnectorError::RequestEncodingFailed)?; + Ok(Self::BankRedirectRefund(Box::new( + TrustpayRefundRequestBankRedirect { + merchant_identification: MerchantIdentification { + project_id: auth.project_id, + }, + payment_information: BankPaymentInformation { + amount: Amount { + amount: format!("{:.2}", amount), + currency: item.request.currency.to_string(), + }, + references: References { + merchant_reference: format!("{}_{}", item.payment_id, "1"), + }, + }, + }, + ))) + } + _ => Ok(Self::CardsRefund(Box::new(TrustpayRefundRequestCards { + instance_id: item.request.connector_transaction_id.clone(), + amount: utils::to_currency_base_unit(item.request.amount, item.request.currency)?, + currency: item.request.currency.to_string(), + reference: item.payment_id.clone(), + }))), + } + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CardsRefundResponse { + pub status: i64, + pub description: Option, + pub instance_id: String, + pub payment_status: String, + pub payment_description: Option, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct BankRedirectRefundResponse { + pub payment_request_id: i64, + pub result_info: ResultInfo, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum RefundResponse { + CardsRefund(Box), + BankRedirectRefund(Box), + BankRedirectRefundSyncResponse(Box), + BankRedirectError(Box), +} + +fn handle_cards_refund_response( + response: CardsRefundResponse, + status_code: u16, +) -> CustomResult<(Option, types::RefundsResponseData), errors::ConnectorError> +{ + let (refund_status, msg) = get_refund_status(&response.payment_status)?; + let error = if msg.is_some() { + Some(types::ErrorResponse { + code: response.payment_status, + message: msg.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: None, + status_code, + }) + } else { + None + }; + let refund_response_data = types::RefundsResponseData { + connector_refund_id: response.instance_id, + refund_status, + }; + Ok((error, refund_response_data)) +} + +fn handle_bank_redirects_refund_response( + response: BankRedirectRefundResponse, + status_code: u16, +) -> (Option, types::RefundsResponseData) { + let (refund_status, msg) = get_refund_status_from_result_info(response.result_info.result_code); + let error = if msg.is_some() { + Some(types::ErrorResponse { + code: response.result_info.result_code.to_string(), + message: msg.unwrap_or(consts::NO_ERROR_MESSAGE).to_owned(), + reason: None, + status_code, + }) + } else { + None + }; + let refund_response_data = types::RefundsResponseData { + connector_refund_id: response.payment_request_id.to_string(), + refund_status, + }; + (error, refund_response_data) +} + +fn handle_bank_redirects_refund_sync_response( + response: SyncResponseBankRedirect, + status_code: u16, +) -> (Option, types::RefundsResponseData) { + let refund_status = enums::RefundStatus::from(response.payment_information.status); + let error = if refund_status == enums::RefundStatus::Failure { + let reason_info = response + .payment_information + .status_reason_information + .unwrap_or_default(); + Some(types::ErrorResponse { + code: reason_info.reason.code, + message: reason_info + .reason + .reject_reason + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: None, + status_code, + }) + } else { + None + }; + let refund_response_data = types::RefundsResponseData { + connector_refund_id: response.payment_information.references.payment_request_id, + refund_status, + }; + (error, refund_response_data) +} + +fn handle_bank_redirects_refund_sync_error_response( + response: ErrorResponseBankRedirect, + status_code: u16, +) -> (Option, types::RefundsResponseData) { + let error = Some(types::ErrorResponse { + code: response.payment_result_info.result_code.to_string(), + message: response + .payment_result_info + .additional_info + .unwrap_or(consts::NO_ERROR_MESSAGE.to_owned()), + reason: None, + status_code, + }); + //unreachable case as we are sending error as Some() + let refund_response_data = types::RefundsResponseData { + connector_refund_id: "".to_string(), + refund_status: enums::RefundStatus::Failure, + }; + (error, refund_response_data) +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + let (error, response) = match item.response { + RefundResponse::CardsRefund(response) => { + handle_cards_refund_response(*response, item.http_code)? + } + RefundResponse::BankRedirectRefund(response) => { + handle_bank_redirects_refund_response(*response, item.http_code) + } + RefundResponse::BankRedirectRefundSyncResponse(response) => { + handle_bank_redirects_refund_sync_response(*response, item.http_code) + } + RefundResponse::BankRedirectError(response) => { + handle_bank_redirects_refund_sync_error_response(*response, item.http_code) + } + }; + Ok(Self { + response: error.map_or_else(|| Ok(response), Err), + ..item.data + }) + } +} + +fn get_refund_status( + payment_status: &str, +) -> CustomResult<(enums::RefundStatus, Option), errors::ConnectorError> { + let (is_failed, failure_message) = is_payment_failed(payment_status); + if payment_status == "000.200.000" { + Ok((enums::RefundStatus::Pending, None)) + } else if is_failed { + Ok(( + enums::RefundStatus::Failure, + Some(failure_message.to_string()), + )) + } else if is_payment_successful(payment_status)? { + Ok((enums::RefundStatus::Success, None)) + } else { + Ok((enums::RefundStatus::Pending, None)) + } +} + +fn get_refund_status_from_result_info( + result_code: i64, +) -> (enums::RefundStatus, Option<&'static str>) { + match result_code { + 1001000 => (enums::RefundStatus::Success, None), + 1130001 => (enums::RefundStatus::Pending, Some("MapiPending")), + 1130000 => (enums::RefundStatus::Pending, Some("MapiSuccess")), + 1130004 => (enums::RefundStatus::Pending, Some("MapiProcessing")), + 1130002 => (enums::RefundStatus::Pending, Some("MapiAnnounced")), + 1130003 => (enums::RefundStatus::Pending, Some("MapiAuthorized")), + 1130005 => (enums::RefundStatus::Pending, Some("MapiAuthorizedOnly")), + 1112008 => (enums::RefundStatus::Failure, Some("InvalidPaymentState")), + 1112009 => (enums::RefundStatus::Failure, Some("RefundRejected")), + 1122006 => ( + enums::RefundStatus::Failure, + Some("AccountCurrencyNotAllowed"), + ), + 1132000 => (enums::RefundStatus::Failure, Some("InvalidMapiRequest")), + 1132001 => (enums::RefundStatus::Failure, Some("UnknownAccount")), + 1132002 => ( + enums::RefundStatus::Failure, + Some("MerchantAccountDisabled"), + ), + 1132003 => (enums::RefundStatus::Failure, Some("InvalidSign")), + 1132004 => (enums::RefundStatus::Failure, Some("DisposableBalance")), + 1132005 => (enums::RefundStatus::Failure, Some("TransactionNotFound")), + 1132006 => (enums::RefundStatus::Failure, Some("UnsupportedTransaction")), + 1132007 => (enums::RefundStatus::Failure, Some("GeneralMapiError")), + 1132008 => ( + enums::RefundStatus::Failure, + Some("UnsupportedCurrencyConversion"), + ), + 1132009 => (enums::RefundStatus::Failure, Some("UnknownMandate")), + 1132010 => (enums::RefundStatus::Failure, Some("CanceledMandate")), + 1132011 => (enums::RefundStatus::Failure, Some("MissingCid")), + 1132012 => (enums::RefundStatus::Failure, Some("MandateAlreadyPaid")), + 1132013 => (enums::RefundStatus::Failure, Some("AccountIsTesting")), + 1132014 => (enums::RefundStatus::Failure, Some("RequestThrottled")), + 1133000 => (enums::RefundStatus::Failure, Some("InvalidAuthentication")), + 1133001 => (enums::RefundStatus::Failure, Some("ServiceNotAllowed")), + 1133002 => (enums::RefundStatus::Failure, Some("PaymentRequestNotFound")), + 1133003 => (enums::RefundStatus::Failure, Some("UnexpectedGateway")), + 1133004 => (enums::RefundStatus::Failure, Some("MissingExternalId")), + 1152000 => (enums::RefundStatus::Failure, Some("RiskDecline")), + _ => (enums::RefundStatus::Pending, None), + } +} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct TrustpayRedirectResponse { + pub status: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct Errors { + pub code: i64, + pub description: String, +} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct TrustpayErrorResponse { + pub status: i64, + pub description: Option, + pub errors: Vec, +} diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 83fdfb9be7..752e8d9c8a 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -222,6 +222,7 @@ pub enum CardIssuer { pub trait CardData { fn get_card_expiry_year_2_digit(&self) -> Secret; fn get_card_issuer(&self) -> Result; + fn get_card_expiry_month_year_2_digit_with_delimiter(&self, delimiter: String) -> String; } impl CardData for api::Card { @@ -237,6 +238,15 @@ impl CardData for api::Card { .map(|card| card.split_whitespace().collect()); get_card_issuer(card.peek().clone().as_str()) } + fn get_card_expiry_month_year_2_digit_with_delimiter(&self, delimiter: String) -> String { + let year = self.get_card_expiry_year_2_digit(); + format!( + "{}{}{}", + self.card_exp_month.peek().clone(), + delimiter, + year.peek() + ) + } } fn get_card_issuer(card_number: &str) -> Result { diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 750c969ced..8011fb07c7 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -86,7 +86,7 @@ pub fn mk_add_card_request( name_on_card: Some("John Doe".to_string().into()), // [#256] nickname: Some("router".to_string()), // }; - let body = utils::Encode::>::encode(&add_card_req) + let body = utils::Encode::>::url_encode(&add_card_req) .change_context(errors::VaultError::RequestEncodingFailed)?; let mut url = locker.host.to_owned(); url.push_str("/card/addCard"); @@ -139,7 +139,7 @@ pub fn mk_get_card_request<'a>( card_id, }; - let body = utils::Encode::>::encode(&get_card_req) + let body = utils::Encode::>::url_encode(&get_card_req) .change_context(errors::VaultError::RequestEncodingFailed)?; let mut url = locker.host.to_owned(); url.push_str("/card/getCard"); @@ -158,7 +158,7 @@ pub fn mk_delete_card_request<'a>( merchant_id, card_id, }; - let body = utils::Encode::>::encode(&delete_card_req) + let body = utils::Encode::>::url_encode(&delete_card_req) .change_context(errors::VaultError::RequestEncodingFailed)?; let mut url = locker.host.to_owned(); url.push_str("/card/deleteCard"); diff --git a/crates/router/src/core/payments/access_token.rs b/crates/router/src/core/payments/access_token.rs index 10487b2cf9..38d8320d9d 100644 --- a/crates/router/src/core/payments/access_token.rs +++ b/crates/router/src/core/payments/access_token.rs @@ -10,7 +10,7 @@ use crate::{ }, routes::AppState, services, - types::{self, api as api_types, storage}, + types::{self, api as api_types, storage, transformers::ForeignInto}, }; /// This function replaces the request and response type of routerdata with the @@ -82,7 +82,10 @@ pub async fn add_access_token< merchant_account: &storage::MerchantAccount, router_data: &types::RouterData, ) -> RouterResult { - if connector.connector_name.supports_access_token() { + if connector + .connector_name + .supports_access_token(router_data.payment_method.foreign_into()) + { let merchant_id = &merchant_account.merchant_id; let store = &*state.store; let old_access_token = store diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 4f9acb773d..c0f1dde3af 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -182,6 +182,7 @@ impl ConnectorData { "worldline" => Ok(Box::new(&connector::Worldline)), "worldpay" => Ok(Box::new(&connector::Worldpay)), "multisafepay" => Ok(Box::new(&connector::Multisafepay)), + "trustpay" => Ok(Box::new(&connector::Trustpay)), _ => Err(report!(errors::ConnectorError::InvalidConnectorName) .attach_printable(format!("invalid connector name: {connector_name}"))) .change_context(errors::ApiErrorResponse::InternalServerError), diff --git a/crates/router/tests/connectors/connector_auth.rs b/crates/router/tests/connectors/connector_auth.rs index a330842bd5..e75e1473da 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -22,6 +22,7 @@ pub(crate) struct ConnectorAuthentication { pub stripe: Option, pub worldpay: Option, pub worldline: Option, + pub trustpay: Option, } impl ConnectorAuthentication { diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index b85dd3b629..33be93a7ff 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -18,6 +18,7 @@ mod payu; mod rapyd; mod shift4; mod stripe; +mod trustpay; mod utils; mod worldline; mod worldpay; diff --git a/crates/router/tests/connectors/trustpay.rs b/crates/router/tests/connectors/trustpay.rs new file mode 100644 index 0000000000..8c530cd6fe --- /dev/null +++ b/crates/router/tests/connectors/trustpay.rs @@ -0,0 +1,245 @@ +use masking::Secret; +use router::types::{self, api, storage::enums, BrowserInformation}; + +use crate::{ + connector_auth, + utils::{self, ConnectorActions}, +}; + +#[derive(Clone, Copy)] +struct TrustpayTest; +impl ConnectorActions for TrustpayTest {} +impl utils::Connector for TrustpayTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Trustpay; + types::api::ConnectorData { + connector: Box::new(&Trustpay), + connector_name: types::Connector::Trustpay, + get_token: types::api::GetToken::Connector, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + types::ConnectorAuthType::from( + connector_auth::ConnectorAuthentication::new() + .trustpay + .expect("Missing connector authentication configuration"), + ) + } + + fn get_name(&self) -> String { + "trustpay".to_string() + } +} + +fn get_default_browser_info() -> BrowserInformation { + BrowserInformation { + color_depth: 24, + java_enabled: false, + java_script_enabled: true, + language: "en-US".to_string(), + screen_height: 1080, + screen_width: 1920, + time_zone: 3600, + accept_header: "*".to_string(), + user_agent: "none".to_string(), + ip_address: None, + } +} + +fn get_default_payment_authorize_data() -> Option { + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: Secret::new("4200000000000000".to_string()), + card_exp_year: Secret::new("25".to_string()), + card_cvc: Secret::new("123".to_string()), + ..utils::CCardType::default().0 + }), + browser_info: Some(get_default_browser_info()), + ..utils::PaymentAuthorizeType::default().0 + }) +} + +fn get_default_payment_info() -> Option { + Some(utils::PaymentInfo { + address: Some(types::PaymentAddress { + billing: Some(api::Address { + address: Some(api::AddressDetails { + first_name: Some(Secret::new("first".to_string())), + last_name: Some(Secret::new("last".to_string())), + line1: Some(Secret::new("line1".to_string())), + line2: Some(Secret::new("line2".to_string())), + city: Some("city".to_string()), + zip: Some(Secret::new("zip".to_string())), + country: Some("IN".to_string()), + ..Default::default() + }), + phone: None, + }), + ..Default::default() + }), + router_return_url: Some(String::from("http://localhost:8080")), + ..Default::default() + }) +} + +static CONNECTOR: TrustpayTest = TrustpayTest {}; + +// Cards Positive Tests +// Creates a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR + .make_payment( + get_default_payment_authorize_data(), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_auto_captured_payment() { + let authorize_response = CONNECTOR + .make_payment( + get_default_payment_authorize_data(), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + encoded_data: None, + capture_method: None, + }), + None, + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_auto_captured_payment() { + let response = CONNECTOR + .make_payment_and_refund( + get_default_payment_authorize_data(), + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_refund() { + let refund_response = CONNECTOR + .make_payment_and_refund( + get_default_payment_authorize_data(), + None, + get_default_payment_info(), + ) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Cards Negative scenerios +// Creates a payment with incorrect card number. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_card_number() { + let payment_authorize_data = types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: Secret::new("1234567891011".to_string()), + card_exp_year: Secret::new("25".to_string()), + card_cvc: Secret::new("123".to_string()), + ..utils::CCardType::default().0 + }), + browser_info: Some(get_default_browser_info()), + ..utils::PaymentAuthorizeType::default().0 + }; + let response = CONNECTOR + .make_payment(Some(payment_authorize_data), get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Errors { code: 61, description: \"invalid payment data (country or brand)\" }".to_string(), + ); +} + +// Creates a payment with empty card number. +#[actix_web::test] +async fn should_fail_payment_for_empty_card_number() { + let payment_authorize_data = types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: Secret::new("".to_string()), + card_exp_year: Secret::new("25".to_string()), + card_cvc: Secret::new("123".to_string()), + ..utils::CCardType::default().0 + }), + browser_info: Some(get_default_browser_info()), + ..utils::PaymentAuthorizeType::default().0 + }; + let response = CONNECTOR + .make_payment(Some(payment_authorize_data), get_default_payment_info()) + .await + .unwrap(); + let x = response.response.unwrap_err(); + assert_eq!( + x.message, + "Errors { code: 61, description: \"invalid payment data (country or brand)\" }", + ); +} + +// Creates a payment with incorrect expiry year. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_expiry_year() { + let payment_authorize_data = Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: Secret::new("4200000000000000".to_string()), + card_exp_year: Secret::new("22".to_string()), + card_cvc: Secret::new("123".to_string()), + ..utils::CCardType::default().0 + }), + browser_info: Some(get_default_browser_info()), + ..utils::PaymentAuthorizeType::default().0 + }); + let response = CONNECTOR + .make_payment(payment_authorize_data, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Errors { code: 15, description: \"the provided expiration year is not valid\" }" + .to_string(), + ); +} diff --git a/loadtest/config/Development.toml b/loadtest/config/Development.toml index b011c3866b..5a73d19d4f 100644 --- a/loadtest/config/Development.toml +++ b/loadtest/config/Development.toml @@ -75,6 +75,8 @@ shift4.base_url = "https://api.shift4.com/" stripe.base_url = "https://api.stripe.com/" worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldpay.base_url = "https://try.access.worldpay.com/" +trustpay.base_url = "https://test-tpgw.trustpay.eu/" +trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" [connectors.supported] wallets = ["klarna", "braintree", "applepay"] @@ -98,4 +100,5 @@ cards = [ "stripe", "worldline", "worldpay", + "trustpay", ]