diff --git a/config/config.example.toml b/config/config.example.toml index 30e3bcb9fb..aa6c627c29 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -174,6 +174,7 @@ 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/" +zen.base_url = "https://api.zen-test.com/" # This data is used to call respective connectors for wallets and cards [connectors.supported] @@ -185,11 +186,13 @@ cards = [ "braintree", "checkout", "cybersource", + "globalpay", "mollie", "paypal", "shift4", "stripe", "worldpay", + "zen", ] # Scheduler settings provides a point to modify the behaviour of scheduler flow. diff --git a/config/development.toml b/config/development.toml index 2ca4ba2777..b84e742b3d 100644 --- a/config/development.toml +++ b/config/development.toml @@ -79,6 +79,7 @@ cards = [ "trustpay", "worldline", "worldpay", + "zen", ] [refund] @@ -128,6 +129,9 @@ 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.zen] +base_url = "https://api.zen-test.com/" + [scheduler] stream = "SCHEDULER_STREAM" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index d1882c6140..73ef413e52 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -101,6 +101,7 @@ 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/" +zen.base_url = "https://api.zen-test.com/" [connectors.supported] @@ -133,6 +134,7 @@ cards = [ "trustpay", "worldline", "worldpay", + "zen", ] diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 39e04d243d..229c034f46 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -609,9 +609,10 @@ pub enum Connector { Rapyd, Shift4, Stripe, + Trustpay, Worldline, Worldpay, - Trustpay, + Zen, } impl Connector { @@ -674,6 +675,7 @@ pub enum RoutableConnectors { Trustpay, Worldline, Worldpay, + Zen, } /// Name of banks supported by Hyperswitch diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 9eebc58c8c..bc274da9d4 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -309,9 +309,10 @@ pub struct Connectors { pub rapyd: ConnectorParams, pub shift4: ConnectorParams, pub stripe: ConnectorParamsWithFileUploadUrl, + pub trustpay: ConnectorParamsWithMoreUrls, pub worldline: ConnectorParams, pub worldpay: ConnectorParams, - pub trustpay: ConnectorParamsWithMoreUrls, + pub zen: ConnectorParams, // Keep this field separate from the remaining fields pub supported: SupportedConnectors, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 659dade2f8..751d4b68a8 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -27,6 +27,7 @@ pub mod trustpay; pub mod utils; pub mod worldline; pub mod worldpay; +pub mod zen; pub mod mollie; @@ -37,5 +38,5 @@ pub use self::{ globalpay::Globalpay, klarna::Klarna, mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, nuvei::Nuvei, opennode::Opennode, payeezy::Payeezy, paypal::Paypal, payu::Payu, rapyd::Rapyd, shift4::Shift4, stripe::Stripe, trustpay::Trustpay, - worldline::Worldline, worldpay::Worldpay, + worldline::Worldline, worldpay::Worldpay, zen::Zen, }; diff --git a/crates/router/src/connector/opennode/transformers.rs b/crates/router/src/connector/opennode/transformers.rs index e2c9e84090..4458b13eb0 100644 --- a/crates/router/src/connector/opennode/transformers.rs +++ b/crates/router/src/connector/opennode/transformers.rs @@ -224,7 +224,7 @@ fn get_crypto_specific_payment_data( let description = item.get_description()?; let auto_settle = true; let success_url = item.get_return_url()?; - let callback_url = item.request.get_webhook_url()?; + let callback_url = item.request.get_router_return_url()?; Ok(OpennodePaymentsRequest { amount, diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index f0591e9b26..822dde6cf8 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use api_models::payments; +use api_models::payments::{self, OrderDetails}; use base64::Engine; use common_utils::{ date_time, @@ -151,11 +151,13 @@ pub trait PaymentsAuthorizeRequestData { fn is_auto_capture(&self) -> Result; fn get_email(&self) -> Result, Error>; fn get_browser_info(&self) -> Result; + fn get_order_details(&self) -> Result; fn get_card(&self) -> Result; fn get_return_url(&self) -> Result; fn connector_mandate_id(&self) -> Option; fn is_mandate_payment(&self) -> bool; fn get_webhook_url(&self) -> Result; + fn get_router_return_url(&self) -> Result; } impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { @@ -174,6 +176,12 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { .clone() .ok_or_else(missing_field_err("browser_info")) } + fn get_order_details(&self) -> Result { + self.order_details + .clone() + .ok_or_else(missing_field_err("order_details")) + } + fn get_card(&self) -> Result { match self.payment_method_data.clone() { api::PaymentMethodData::Card(card) => Ok(card), @@ -199,12 +207,27 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { .is_some() } fn get_webhook_url(&self) -> Result { + self.webhook_url + .clone() + .ok_or_else(missing_field_err("webhook_url")) + } + fn get_router_return_url(&self) -> Result { self.router_return_url .clone() .ok_or_else(missing_field_err("webhook_url")) } } +pub trait BrowserInformationData { + fn get_ip_address(&self) -> Result; +} + +impl BrowserInformationData for types::BrowserInformation { + fn get_ip_address(&self) -> Result { + self.ip_address.ok_or_else(missing_field_err("ip_address")) + } +} + pub trait PaymentsCompleteAuthorizeRequestData { fn is_auto_capture(&self) -> Result; } diff --git a/crates/router/src/connector/zen.rs b/crates/router/src/connector/zen.rs new file mode 100644 index 0000000000..c4ee6d1e9c --- /dev/null +++ b/crates/router/src/connector/zen.rs @@ -0,0 +1,583 @@ +mod transformers; + +use std::fmt::Debug; + +use common_utils::{crypto, ext_traits::ByteSliceExt}; +use error_stack::{IntoReport, ResultExt}; +use transformers as zen; +use uuid::Uuid; + +use self::transformers::{ZenPaymentStatus, ZenWebhookTxnType}; +use super::utils::RefundsRequestData; +use crate::{ + configs::settings, + core::{ + errors::{self, CustomResult}, + payments, + }, + db::StorageInterface, + headers, + services::{self, ConnectorIntegration}, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Zen; + +impl api::Payment for Zen {} +impl api::PaymentSession for Zen {} +impl api::ConnectorAccessToken for Zen {} +impl api::PreVerify for Zen {} +impl api::PaymentAuthorize for Zen {} +impl api::PaymentSync for Zen {} +impl api::PaymentCapture for Zen {} +impl api::PaymentVoid for Zen {} +impl api::PaymentToken for Zen {} +impl api::Refund for Zen {} +impl api::RefundExecute for Zen {} +impl api::RefundSync for Zen {} + +impl ConnectorCommonExt for Zen +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let mut headers = vec![ + ( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string(), + ), + ("request-id".to_string(), Uuid::new_v4().to_string()), + ]; + + let mut auth_header = self.get_auth_header(&req.connector_auth_type)?; + headers.append(&mut auth_header); + + Ok(headers) + } +} + +impl ConnectorCommon for Zen { + fn id(&self) -> &'static str { + "zen" + } + + fn common_get_content_type(&self) -> &'static str { + mime::APPLICATION_JSON.essence_str() + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.zen.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult, errors::ConnectorError> { + let auth = zen::ZenAuthType::try_from(auth_type)?; + Ok(vec![( + headers::AUTHORIZATION.to_string(), + format!("Bearer {}", auth.api_key), + )]) + } + + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: zen::ZenErrorResponse = res + .response + .parse_struct("Zen ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.error.code, + message: response.error.message, + reason: None, + }) + } +} + +impl ConnectorIntegration + for Zen +{ + //TODO: implement sessions flow +} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Zen +{ + // Not Implemented (R) +} + +impl ConnectorIntegration + for Zen +{ +} + +impl ConnectorIntegration + for Zen +{ +} + +impl ConnectorIntegration + for Zen +{ + 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 { + Ok(format!("{}v1/transactions", self.base_url(connectors),)) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = zen::ZenPaymentsRequest::try_from(req)?; + let zen_req = utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(zen_req)) + } + + 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, + )?) + .attach_default_headers() + .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: zen::ZenPaymentsResponse = res + .response + .parse_struct("Zen PaymentsAuthorizeResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Zen +{ + 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 payment_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + + Ok(format!( + "{}v1/transactions/{payment_id}", + self.base_url(connectors), + )) + } + + 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)?) + .attach_default_headers() + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsSyncRouterData, + res: Response, + ) -> CustomResult { + let response: zen::ZenPaymentsResponse = res + .response + .parse_struct("zen PaymentsSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Zen +{ +} + +impl ConnectorIntegration + for Zen +{ +} + +impl ConnectorIntegration for Zen { + 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 { + Ok(format!( + "{}v1/transactions/refund", + self.base_url(connectors), + )) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = zen::ZenRefundRequest::try_from(req)?; + let zen_req = utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(zen_req)) + } + + 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)?) + .attach_default_headers() + .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: zen::RefundResponse = res + .response + .parse_struct("zen RefundResponse") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration for Zen { + 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 { + Ok(format!( + "{}v1/transactions/{}", + self.base_url(connectors), + req.request.get_connector_refund_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)?) + .attach_default_headers() + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .body(types::RefundSyncType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::RefundSyncRouterData, + res: Response, + ) -> CustomResult { + let response: zen::RefundResponse = res + .response + .parse_struct("zen RefundSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Zen { + fn get_webhook_source_verification_algorithm( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::Sha256)) + } + + fn get_webhook_source_verification_signature( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + let webhook_body: zen::ZenWebhookSignature = request + .body + .parse_struct("ZenWebhookSignature") + .change_context(errors::ConnectorError::WebhookSignatureNotFound)?; + let signature = webhook_body.hash; + hex::decode(signature) + .into_report() + .change_context(errors::ConnectorError::WebhookSignatureNotFound) + } + + fn get_webhook_source_verification_message( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _merchant_id: &str, + _secret: &[u8], + ) -> CustomResult, errors::ConnectorError> { + let webhook_body: zen::ZenWebhookBody = request + .body + .parse_struct("ZenWebhookBody") + .change_context(errors::ConnectorError::WebhookSignatureNotFound)?; + let msg = webhook_body.merchant_transaction_id + + &webhook_body.currency + + &webhook_body.amount + + &webhook_body.status.to_string().to_uppercase(); + Ok(msg.into_bytes()) + } + + async fn get_webhook_source_verification_merchant_secret( + &self, + db: &dyn StorageInterface, + merchant_id: &str, + ) -> CustomResult, errors::ConnectorError> { + let key = format!("whsec_verification_{}_{}", self.id(), merchant_id); + let secret = db + .find_config_by_key(&key) + .await + .change_context(errors::ConnectorError::WebhookVerificationSecretNotFound)?; + + Ok(secret.config.into_bytes()) + } + + async fn verify_webhook_source( + &self, + db: &dyn StorageInterface, + request: &api::IncomingWebhookRequestDetails<'_>, + merchant_id: &str, + ) -> CustomResult { + let algorithm = self.get_webhook_source_verification_algorithm(request)?; + + let signature = self.get_webhook_source_verification_signature(request)?; + let mut secret = self + .get_webhook_source_verification_merchant_secret(db, merchant_id) + .await?; + let mut message = + self.get_webhook_source_verification_message(request, merchant_id, &secret)?; + message.append(&mut secret); + algorithm + .verify_signature(&secret, &signature, &message) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed) + } + + fn get_webhook_object_reference_id( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let webhook_body: zen::ZenWebhookObjectReference = request + .body + .parse_struct("ZenWebhookObjectReference") + .change_context(errors::ConnectorError::WebhookSignatureNotFound)?; + Ok(match &webhook_body.transaction_type { + ZenWebhookTxnType::TrtPurchase => api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId( + webhook_body.transaction_id, + ), + ), + ZenWebhookTxnType::TrtRefund => api_models::webhooks::ObjectReferenceId::RefundId( + api_models::webhooks::RefundIdType::ConnectorRefundId(webhook_body.transaction_id), + ), + }) + } + + fn get_webhook_event_type( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let details: zen::ZenWebhookEventType = request + .body + .parse_struct("ZenWebhookEventType") + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + + Ok(match &details.transaction_type { + ZenWebhookTxnType::TrtPurchase => match &details.status { + ZenPaymentStatus::Rejected => api::IncomingWebhookEvent::PaymentIntentFailure, + ZenPaymentStatus::Accepted => api::IncomingWebhookEvent::PaymentIntentSuccess, + _ => Err(errors::ConnectorError::WebhookEventTypeNotFound)?, + }, + ZenWebhookTxnType::TrtRefund => match &details.status { + ZenPaymentStatus::Rejected => api::IncomingWebhookEvent::RefundFailure, + ZenPaymentStatus::Accepted => api::IncomingWebhookEvent::RefundSuccess, + _ => Err(errors::ConnectorError::WebhookEventTypeNotFound)?, + }, + }) + } + + fn get_webhook_resource_object( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let reference_object: serde_json::Value = serde_json::from_slice(request.body) + .into_report() + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + Ok(reference_object) + } + fn get_webhook_api_response( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> + { + Ok(services::api::ApplicationResponse::Json( + serde_json::json!({ + "status": "ok" + }), + )) + } +} + +impl services::ConnectorRedirectResponse for Zen { + fn get_flow_type( + &self, + _query_params: &str, + _json_payload: Option, + _action: services::PaymentAction, + ) -> CustomResult { + Ok(payments::CallConnectorAction::Trigger) + } +} diff --git a/crates/router/src/connector/zen/transformers.rs b/crates/router/src/connector/zen/transformers.rs new file mode 100644 index 0000000000..df077c3bbf --- /dev/null +++ b/crates/router/src/connector/zen/transformers.rs @@ -0,0 +1,437 @@ +use std::net::IpAddr; + +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{ + connector::utils::{ + self, BrowserInformationData, CardData, PaymentsAuthorizeRequestData, RouterData, + }, + core::errors, + pii, + services::{self, Method}, + types::{self, api, storage::enums, transformers::ForeignTryFrom}, +}; + +// Auth Struct +pub struct ZenAuthType { + pub(super) api_key: String, +} + +impl TryFrom<&types::ConnectorAuthType> for ZenAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + if let types::ConnectorAuthType::HeaderKey { api_key } = auth_type { + Ok(Self { + api_key: api_key.to_string(), + }) + } else { + Err(errors::ConnectorError::FailedToObtainAuthType.into()) + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ZenPaymentsRequest { + merchant_transaction_id: String, + payment_channel: ZenPaymentChannels, + amount: String, + currency: enums::Currency, + payment_specific_data: ZenPaymentData, + customer: ZenCustomerDetails, + custom_ipn_url: String, + items: Vec, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[allow(clippy::enum_variant_names)] +pub enum ZenPaymentChannels { + PclCard, + PclGooglepay, + PclApplepay, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ZenCustomerDetails { + email: Secret, + ip: IpAddr, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ZenPaymentData { + browser_details: ZenBrowserDetails, + #[serde(rename = "type")] + payment_type: ZenPaymentTypes, + #[serde(skip_serializing_if = "Option::is_none")] + token: Option, + #[serde(skip_serializing_if = "Option::is_none")] + card: Option, + descriptor: String, + return_verify_url: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ZenBrowserDetails { + color_depth: String, + java_enabled: bool, + lang: String, + screen_height: String, + screen_width: String, + timezone: String, + accept_header: String, + window_size: String, + user_agent: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ZenPaymentTypes { + Onetime, + ExternalPaymentToken, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ZenCardDetails { + number: Secret, + expiry_date: Secret, + cvv: Secret, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ZenItemObject { + name: String, + price: String, + quantity: u16, + line_amount_total: String, +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for ZenPaymentsRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + let browser_info = item.request.get_browser_info()?; + let order_details = item.request.get_order_details()?; + let ip = browser_info.get_ip_address()?; + + let window_size = match (browser_info.screen_height, browser_info.screen_width) { + (250, 400) => "01", + (390, 400) => "02", + (500, 600) => "03", + (600, 400) => "04", + _ => "05", + } + .to_string(); + let browser_details = ZenBrowserDetails { + color_depth: browser_info.color_depth.to_string(), + java_enabled: browser_info.java_enabled, + lang: browser_info.language, + screen_height: browser_info.screen_height.to_string(), + screen_width: browser_info.screen_width.to_string(), + timezone: browser_info.time_zone.to_string(), + accept_header: browser_info.accept_header, + window_size, + user_agent: browser_info.user_agent, + }; + let (payment_specific_data, payment_channel) = match &item.request.payment_method_data { + api::PaymentMethodData::Card(ccard) => Ok(( + ZenPaymentData { + browser_details, + //Connector Specific for cards + payment_type: ZenPaymentTypes::Onetime, + token: None, + card: Some(ZenCardDetails { + number: ccard.card_number.clone(), + expiry_date: ccard + .get_card_expiry_month_year_2_digit_with_delimiter("".to_owned()), + cvv: ccard.card_cvc.clone(), + }), + descriptor: item.get_description()?.chars().take(24).collect(), + return_verify_url: item.request.router_return_url.clone(), + }, + //Connector Specific for cards + ZenPaymentChannels::PclCard, + )), + api::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + api_models::payments::WalletData::GooglePay(data) => Ok(( + ZenPaymentData { + browser_details, + //Connector Specific for wallet + payment_type: ZenPaymentTypes::ExternalPaymentToken, + token: Some(data.tokenization_data.token.clone()), + card: None, + descriptor: item.get_description()?.chars().take(24).collect(), + return_verify_url: item.request.router_return_url.clone(), + }, + ZenPaymentChannels::PclGooglepay, + )), + api_models::payments::WalletData::ApplePay(data) => Ok(( + ZenPaymentData { + browser_details, + //Connector Specific for wallet + payment_type: ZenPaymentTypes::ExternalPaymentToken, + token: Some(data.payment_data.clone()), + card: None, + descriptor: item.get_description()?.chars().take(24).collect(), + return_verify_url: item.request.router_return_url.clone(), + }, + ZenPaymentChannels::PclApplepay, + )), + _ => Err(errors::ConnectorError::NotImplemented( + "payment method".to_string(), + )), + }, + _ => Err(errors::ConnectorError::NotImplemented( + "payment method".to_string(), + )), + }?; + let order_amount = + utils::to_currency_base_unit(item.request.amount, item.request.currency)?; + Ok(Self { + merchant_transaction_id: item.payment_id.clone(), + payment_channel, + amount: order_amount.clone(), + currency: item.request.currency, + payment_specific_data, + customer: ZenCustomerDetails { + email: item.request.get_email()?, + ip, + }, + custom_ipn_url: item.request.get_webhook_url()?, + items: vec![ZenItemObject { + name: order_details.product_name, + price: order_amount.clone(), + quantity: 1, + line_amount_total: order_amount, + }], + }) + } +} + +// PaymentsResponse +#[derive(Debug, Default, Deserialize, Clone, PartialEq, strum::Display)] +#[serde(rename_all = "UPPERCASE")] +pub enum ZenPaymentStatus { + Authorized, + Accepted, + #[default] + Pending, + Rejected, + Canceled, +} + +impl ForeignTryFrom<(ZenPaymentStatus, Option)> for enums::AttemptStatus { + type Error = error_stack::Report; + fn foreign_try_from(item: (ZenPaymentStatus, Option)) -> Result { + let (item_txn_status, item_action_status) = item; + Ok(match item_txn_status { + // Payment has been authorized at connector end, They will send webhook when it gets accepted + ZenPaymentStatus::Authorized => Self::Pending, + ZenPaymentStatus::Accepted => Self::Charged, + ZenPaymentStatus::Pending => { + item_action_status.map_or(Self::Pending, |action| match action { + ZenActions::Redirect => Self::AuthenticationPending, + }) + } + ZenPaymentStatus::Rejected => Self::Failure, + ZenPaymentStatus::Canceled => Self::Voided, + }) + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ZenPaymentsResponse { + status: ZenPaymentStatus, + id: String, + merchant_action: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ZenMerchantAction { + action: ZenActions, + data: ZenMerchantActionData, +} +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum ZenActions { + Redirect, +} +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ZenMerchantActionData { + redirect_url: url::Url, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + let redirection_data_action = item.response.merchant_action.map(|merchant_action| { + ( + services::RedirectForm::from((merchant_action.data.redirect_url, Method::Get)), + merchant_action.action, + ) + }); + let (redirection_data, action) = match redirection_data_action { + Some((redirect_form, action)) => (Some(redirect_form), Some(action)), + None => (None, None), + }; + + Ok(Self { + status: enums::AttemptStatus::foreign_try_from((item.response.status, action))?, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + redirection_data, + mandate_reference: None, + connector_metadata: None, + }), + ..item.data + }) + } +} + +#[derive(Default, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ZenRefundRequest { + amount: String, + transaction_id: String, + currency: enums::Currency, + merchant_transaction_id: String, +} + +impl TryFrom<&types::RefundsRouterData> for ZenRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundsRouterData) -> Result { + Ok(Self { + amount: utils::to_currency_base_unit( + item.request.refund_amount, + item.request.currency, + )?, + transaction_id: item.request.connector_transaction_id.clone(), + currency: item.request.currency, + merchant_transaction_id: item.request.refund_id.clone(), + }) + } +} + +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum RefundStatus { + Authorized, + Accepted, + #[default] + Pending, + Rejected, +} + +impl From for enums::RefundStatus { + fn from(item: RefundStatus) -> Self { + match item { + RefundStatus::Accepted => Self::Success, + RefundStatus::Pending | RefundStatus::Authorized => Self::Pending, + RefundStatus::Rejected => Self::Failure, + } + } +} + +#[derive(Default, Debug, Deserialize)] +pub struct RefundResponse { + id: String, + status: RefundStatus, +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + let refund_status = enums::RefundStatus::from(item.response.status); + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.id, + refund_status, + }), + ..item.data + }) + } +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + let refund_status = enums::RefundStatus::from(item.response.status); + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.id, + refund_status, + }), + ..item.data + }) + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ZenWebhookBody { + pub merchant_transaction_id: String, + pub amount: String, + pub currency: String, + pub status: ZenPaymentStatus, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ZenWebhookSignature { + pub hash: String, +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ZenWebhookObjectReference { + #[serde(rename = "type")] + pub transaction_type: ZenWebhookTxnType, + pub transaction_id: String, +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ZenWebhookEventType { + #[serde(rename = "type")] + pub transaction_type: ZenWebhookTxnType, + pub transaction_id: String, + pub status: ZenPaymentStatus, +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ZenWebhookTxnType { + TrtPurchase, + TrtRefund, +} + +#[derive(Debug, Deserialize)] +pub struct ZenErrorResponse { + pub error: ZenErrorBody, +} + +#[derive(Debug, Deserialize)] +pub struct ZenErrorBody { + pub message: String, + pub code: String, +} diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 945351994b..cb85ef82a2 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -107,7 +107,8 @@ default_imp_for_complete_authorize!( connector::Stripe, connector::Trustpay, connector::Worldline, - connector::Worldpay + connector::Worldpay, + connector::Zen ); macro_rules! default_imp_for_connector_redirect_response{ @@ -186,7 +187,8 @@ default_imp_for_connector_request_id!( connector::Stripe, connector::Trustpay, connector::Worldline, - connector::Worldpay + connector::Worldpay, + connector::Zen ); macro_rules! default_imp_for_accept_dispute{ @@ -233,7 +235,8 @@ default_imp_for_accept_dispute!( connector::Trustpay, connector::Opennode, connector::Worldline, - connector::Worldpay + connector::Worldpay, + connector::Zen ); macro_rules! default_imp_for_file_upload{ @@ -279,7 +282,8 @@ default_imp_for_file_upload!( connector::Trustpay, connector::Opennode, connector::Worldline, - connector::Worldpay + connector::Worldpay, + connector::Zen ); macro_rules! default_imp_for_submit_evidence{ @@ -325,5 +329,6 @@ default_imp_for_submit_evidence!( connector::Trustpay, connector::Opennode, connector::Worldline, - connector::Worldpay + connector::Worldpay, + connector::Zen ); diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index ff1a895854..4206c29f0e 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -221,6 +221,7 @@ impl ConnectorData { // "nexinets" => Ok(Box::new(&connector::Nexinets)), added as template code for future use "paypal" => Ok(Box::new(&connector::Paypal)), "trustpay" => Ok(Box::new(&connector::Trustpay)), + "zen" => Ok(Box::new(&connector::Zen)), _ => 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 dbfa514772..7de71b2863 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -29,9 +29,10 @@ pub(crate) struct ConnectorAuthentication { pub rapyd: Option, pub shift4: Option, pub stripe: Option, + pub trustpay: Option, pub worldpay: Option, pub worldline: Option, - pub trustpay: Option, + pub zen: Option, } impl ConnectorAuthentication { diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index 55dbea1d5c..2ab6351d2b 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -31,3 +31,4 @@ mod trustpay; mod utils; mod worldline; mod worldpay; +mod zen; diff --git a/crates/router/tests/connectors/zen.rs b/crates/router/tests/connectors/zen.rs new file mode 100644 index 0000000000..11af6514d9 --- /dev/null +++ b/crates/router/tests/connectors/zen.rs @@ -0,0 +1,513 @@ +use api_models::payments::OrderDetails; +use masking::Secret; +use router::types::{self, api, storage::enums}; + +use crate::{ + connector_auth, + utils::{self, ConnectorActions}, +}; + +#[derive(Clone, Copy)] +struct ZenTest; +impl ConnectorActions for ZenTest {} +impl utils::Connector for ZenTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Zen; + types::api::ConnectorData { + connector: Box::new(&Zen), + connector_name: types::Connector::Zen, + get_token: types::api::GetToken::Connector, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + types::ConnectorAuthType::from( + connector_auth::ConnectorAuthentication::new() + .zen + .expect("Missing connector authentication configuration"), + ) + } + + fn get_name(&self) -> String { + "zen".to_string() + } +} + +static CONNECTOR: ZenTest = ZenTest {}; + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[ignore = "Connector triggers 3DS payment on test card"] +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(None, None) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} + +// Captures a payment using the manual capture flow (Non 3DS). +#[ignore = "Connector triggers 3DS payment on test card and capture is not supported"] +#[actix_web::test] +async fn should_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment(None, None, None) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Partially captures a payment using the manual capture flow (Non 3DS). +#[ignore = "Connector triggers 3DS payment on test card and capture is not supported"] +#[actix_web::test] +async fn should_partially_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment( + None, + Some(types::PaymentsCaptureData { + amount_to_capture: 50, + ..utils::PaymentCaptureType::default().0 + }), + None, + ) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the manual capture flow (Non 3DS). +#[ignore = "Connector triggers 3DS payment on test card"] +#[actix_web::test] +async fn should_sync_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(None, None) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + encoded_data: None, + capture_method: None, + connector_meta: None, + }), + None, + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized,); +} + +// Voids a payment using the manual capture flow (Non 3DS). +#[ignore = "Connector triggers 3DS payment on test card and void is not supported"] +#[actix_web::test] +async fn should_void_authorized_payment() { + let response = CONNECTOR + .authorize_and_void_payment( + None, + Some(types::PaymentsCancelData { + connector_transaction_id: String::from(""), + cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() + }), + None, + ) + .await + .expect("Void payment response"); + assert_eq!(response.status, enums::AttemptStatus::Voided); +} + +// Refunds a payment using the manual capture flow (Non 3DS). +#[ignore = "Connector triggers 3DS payment on test card and capture is not supported"] +#[actix_web::test] +async fn should_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund(None, None, None, None) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the manual capture flow (Non 3DS). +#[ignore = "Connector triggers 3DS payment on test card and capture is not supported"] +#[actix_web::test] +async fn should_partially_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + None, + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Synchronizes a refund using the manual capture flow (Non 3DS). +#[ignore = "Connector triggers 3DS payment on test card and capture is not supported"] +#[actix_web::test] +async fn should_sync_manually_captured_refund() { + let refund_response = CONNECTOR + .capture_payment_and_refund(None, None, None, None) + .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, + ); +} + +// Creates a payment using the automatic capture flow (Non 3DS). +#[ignore = "Connector triggers 3DS payment on test card"] +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR.make_payment(None, None).await.unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the automatic capture flow (Non 3DS). +#[ignore = "Connector triggers 3DS payment on test card"] +#[actix_web::test] +async fn should_sync_auto_captured_payment() { + let authorize_response = CONNECTOR.make_payment(None, None).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: Some(enums::CaptureMethod::Automatic), + connector_meta: None, + }), + None, + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). +#[ignore = "Connector triggers 3DS payment on test card"] +#[actix_web::test] +async fn should_refund_auto_captured_payment() { + let response = CONNECTOR + .make_payment_and_refund(None, None, None) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the automatic capture flow (Non 3DS). +#[ignore = "Connector triggers 3DS payment on test card"] +#[actix_web::test] +async fn should_partially_refund_succeeded_payment() { + let refund_response = CONNECTOR + .make_payment_and_refund( + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). +#[ignore = "Connector triggers 3DS payment on test card"] +#[actix_web::test] +async fn should_refund_succeeded_payment_multiple_times() { + CONNECTOR + .make_payment_and_multiple_refund( + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await; +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). +#[ignore = "Connector triggers 3DS payment on test card"] +#[actix_web::test] +async fn should_sync_refund() { + let refund_response = CONNECTOR + .make_payment_and_refund(None, None, None) + .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 response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: Secret::new("1234567891011".to_string()), + ..utils::CCardType::default().0 + }), + order_details: Some(OrderDetails { + product_name: "test".to_string(), + quantity: 1, + }), + email: Some(Secret::new("test@gmail.com".to_string())), + webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response + .response + .unwrap_err() + .message + .split_once(';') + .unwrap() + .0, + "Request data doesn't pass validation".to_string(), + ); +} + +// Creates a payment with empty card number. +#[actix_web::test] +async fn should_fail_payment_for_empty_card_number() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: Secret::new(String::from("")), + ..utils::CCardType::default().0 + }), + order_details: Some(OrderDetails { + product_name: "test".to_string(), + quantity: 1, + }), + email: Some(Secret::new("test@gmail.com".to_string())), + webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + let x = response.response.unwrap_err(); + assert_eq!( + x.message.split_once(';').unwrap().0, + "Request data doesn't pass validation", + ); +} + +// Creates a payment with incorrect CVC. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_cvc() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_cvc: Secret::new("12345".to_string()), + ..utils::CCardType::default().0 + }), + order_details: Some(OrderDetails { + product_name: "test".to_string(), + quantity: 1, + }), + email: Some(Secret::new("test@gmail.com".to_string())), + webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response + .response + .unwrap_err() + .message + .split_once(';') + .unwrap() + .0, + "Request data doesn't pass validation".to_string(), + ); +} + +// Creates a payment with incorrect expiry month. +#[actix_web::test] +async fn should_fail_payment_for_invalid_exp_month() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_exp_month: Secret::new("20".to_string()), + ..utils::CCardType::default().0 + }), + order_details: Some(OrderDetails { + product_name: "test".to_string(), + quantity: 1, + }), + email: Some(Secret::new("test@gmail.com".to_string())), + webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response + .response + .unwrap_err() + .message + .split_once(';') + .unwrap() + .0, + "Request data doesn't pass validation".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_expiry_year() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_exp_year: Secret::new("2000".to_string()), + ..utils::CCardType::default().0 + }), + order_details: Some(OrderDetails { + product_name: "test".to_string(), + quantity: 1, + }), + email: Some(Secret::new("test@gmail.com".to_string())), + webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response + .response + .unwrap_err() + .message + .split_once(';') + .unwrap() + .0, + "Request data doesn't pass validation".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[ignore = "Connector triggers 3DS payment on test card and void is not supported"] +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + let authorize_response = CONNECTOR.make_payment(None, None).await.unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let void_response = CONNECTOR + .void_payment(txn_id.unwrap(), None, None) + .await + .unwrap(); + assert_eq!( + void_response.response.unwrap_err().message, + "You cannot cancel this PaymentIntent because it has a status of succeeded." + ); +} + +// Captures a payment using invalid connector payment id. +#[ignore = "Connector triggers 3DS payment on test card and capture is not supported"] +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, None) + .await + .unwrap(); + assert_eq!( + capture_response.response.unwrap_err().message, + String::from("No such payment_intent: '123456789'") + ); +} + +// Refunds a payment with refund amount higher than payment amount. +#[ignore = "Connector triggers 3DS payment on test card"] +#[actix_web::test] +async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let response = CONNECTOR + .make_payment_and_refund( + None, + Some(types::RefundsData { + refund_amount: 150, + ..utils::PaymentRefundType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Refund amount (₹1.50) is greater than charge amount (₹1.00)", + ); +} + +// Connector dependent test cases goes here + +// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 51911bba83..92de95d7e2 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -87,6 +87,7 @@ 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/" +zen.base_url = "https://api.zen-test.com/" [connectors.supported] wallets = ["klarna", "braintree", "applepay"] @@ -118,4 +119,5 @@ cards = [ "trustpay", "worldline", "worldpay", + "zen", ]