diff --git a/Cargo.lock b/Cargo.lock index b2225445c7..ff9c280597 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2758,7 +2758,7 @@ dependencies = [ [[package]] name = "opentelemetry" version = "0.18.0" -source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658" +source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658" dependencies = [ "opentelemetry_api", "opentelemetry_sdk", @@ -2767,7 +2767,7 @@ dependencies = [ [[package]] name = "opentelemetry-otlp" version = "0.11.0" -source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658" +source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658" dependencies = [ "async-trait", "futures", @@ -2784,7 +2784,7 @@ dependencies = [ [[package]] name = "opentelemetry-proto" version = "0.1.0" -source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658" +source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658" dependencies = [ "futures", "futures-util", @@ -2796,7 +2796,7 @@ dependencies = [ [[package]] name = "opentelemetry_api" version = "0.18.0" -source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658" +source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658" dependencies = [ "fnv", "futures-channel", @@ -2811,7 +2811,7 @@ dependencies = [ [[package]] name = "opentelemetry_sdk" version = "0.18.0" -source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658" +source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658" dependencies = [ "async-trait", "crossbeam-channel", diff --git a/config/Development.toml b/config/Development.toml index fd2127bb03..259aedc2cb 100644 --- a/config/Development.toml +++ b/config/Development.toml @@ -69,6 +69,7 @@ cards = [ "multisafepay", "nuvei", "opennode", + "payeezy", "paypal", "payu", "shift4", @@ -111,6 +112,7 @@ mollie.base_url = "https://api.mollie.com/v2/" multisafepay.base_url = "https://testapi.multisafepay.com/" nuvei.base_url = "https://ppp-test.nuvei.com/" opennode.base_url = "https://dev-api.opennode.com" +payeezy.base_url = "https://api-cert.payeezy.com/" paypal.base_url = "https://www.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" rapyd.base_url = "https://sandboxapi.rapyd.net" diff --git a/config/config.example.toml b/config/config.example.toml index 5dd5fc0761..237adf4543 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -159,6 +159,7 @@ mollie.base_url = "https://api.mollie.com/v2/" multisafepay.base_url = "https://testapi.multisafepay.com/" nuvei.base_url = "https://ppp-test.nuvei.com/" opennode.base_url = "https://dev-api.opennode.com" +payeezy.base_url = "https://api-cert.payeezy.com/" paypal.base_url = "https://www.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" rapyd.base_url = "https://sandboxapi.rapyd.net" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 0c352b3424..5d2844ba99 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -88,6 +88,7 @@ mollie.base_url = "https://api.mollie.com/v2/" multisafepay.base_url = "https://testapi.multisafepay.com/" nuvei.base_url = "https://ppp-test.nuvei.com/" opennode.base_url = "https://dev-api.opennode.com" +payeezy.base_url = "https://api-cert.payeezy.com/" paypal.base_url = "https://www.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" rapyd.base_url = "https://sandboxapi.rapyd.net" @@ -119,6 +120,7 @@ cards = [ "multisafepay", "nuvei", "opennode", + "payeezy", "paypal", "payu", "shift4", diff --git a/connector-template/test.rs b/connector-template/test.rs index 5b90d17ce5..ce75612143 100644 --- a/connector-template/test.rs +++ b/connector-template/test.rs @@ -70,7 +70,7 @@ async fn should_partially_capture_authorized_payment() { .authorize_and_capture_payment( payment_method_details(), Some(types::PaymentsCaptureData { - amount_to_capture: Some(50), + amount_to_capture: 50, ..utils::PaymentCaptureType::default().0 }), get_default_payment_info(), diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 23043d0e0a..ced971d415 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -581,6 +581,7 @@ pub enum Connector { Mollie, Multisafepay, Nuvei, + // Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage Paypal, Payu, Rapyd, @@ -637,6 +638,7 @@ pub enum RoutableConnectors { Multisafepay, Nuvei, Opennode, + // Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage Paypal, Payu, Rapyd, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index f747bd30b9..4df55460d2 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -292,6 +292,7 @@ pub struct Connectors { pub multisafepay: ConnectorParams, pub nuvei: ConnectorParams, pub opennode: ConnectorParams, + pub payeezy: ConnectorParams, pub paypal: ConnectorParams, pub payu: ConnectorParams, pub rapyd: ConnectorParams, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 9f00f49861..e9cbe411dc 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -16,6 +16,7 @@ pub mod klarna; pub mod multisafepay; pub mod nuvei; pub mod opennode; +pub mod payeezy; pub mod paypal; pub mod payu; pub mod rapyd; @@ -33,7 +34,7 @@ pub use self::{ authorizedotnet::Authorizedotnet, bambora::Bambora, bluesnap::Bluesnap, braintree::Braintree, checkout::Checkout, coinbase::Coinbase, cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv, globalpay::Globalpay, klarna::Klarna, mollie::Mollie, - multisafepay::Multisafepay, nuvei::Nuvei, opennode::Opennode, paypal::Paypal, payu::Payu, - rapyd::Rapyd, shift4::Shift4, stripe::Stripe, trustpay::Trustpay, worldline::Worldline, - worldpay::Worldpay, + multisafepay::Multisafepay, nuvei::Nuvei, opennode::Opennode, payeezy::Payeezy, paypal::Paypal, + payu::Payu, rapyd::Rapyd, shift4::Shift4, stripe::Stripe, trustpay::Trustpay, + worldline::Worldline, worldpay::Worldpay, }; diff --git a/crates/router/src/connector/payeezy.rs b/crates/router/src/connector/payeezy.rs new file mode 100644 index 0000000000..d515bd6a8a --- /dev/null +++ b/crates/router/src/connector/payeezy.rs @@ -0,0 +1,530 @@ +mod transformers; + +use std::fmt::Debug; + +use base64::Engine; +use error_stack::{IntoReport, ResultExt}; +use rand::distributions::DistString; +use ring::hmac; +use transformers as payeezy; + +use crate::{ + configs::settings, + consts, + core::errors::{self, CustomResult}, + headers, + services::{self, ConnectorIntegration}, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Payeezy; + +impl ConnectorCommonExt for Payeezy +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let auth = payeezy::PayeezyAuthType::try_from(&req.connector_auth_type)?; + let option_request_payload = self.get_request_body(req)?; + let request_payload = option_request_payload.map_or("{}".to_string(), |s| s); + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .ok() + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + .as_millis() + .to_string(); + let nonce = rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 19); + let signature_string = format!( + "{}{}{}{}{}", + auth.api_key, nonce, timestamp, auth.merchant_token, request_payload + ); + let key = hmac::Key::new(hmac::HMAC_SHA256, auth.api_secret.as_bytes()); + let tag = hmac::sign(&key, signature_string.as_bytes()); + let hmac_sign = hex::encode(tag); + let signature_value = consts::BASE64_ENGINE_URL_SAFE.encode(hmac_sign); + Ok(vec![ + ( + headers::CONTENT_TYPE.to_string(), + Self.get_content_type().to_string(), + ), + (headers::APIKEY.to_string(), auth.api_key), + (headers::TOKEN.to_string(), auth.merchant_token), + (headers::AUTHORIZATION.to_string(), signature_value), + (headers::NONCE.to_string(), nonce), + (headers::TIMESTAMP.to_string(), timestamp), + ]) + } +} + +impl ConnectorCommon for Payeezy { + fn id(&self) -> &'static str { + "payeezy" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.payeezy.base_url.as_ref() + } + + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: payeezy::PayeezyErrorResponse = res + .response + .parse_struct("payeezy ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + let error_messages: Vec = response + .error + .messages + .iter() + .map(|m| m.description.clone()) + .collect(); + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.transaction_status, + message: error_messages.join(", "), + reason: None, + }) + } +} + +impl api::Payment for Payeezy {} + +impl api::PreVerify for Payeezy {} +impl ConnectorIntegration + for Payeezy +{ +} + +impl api::PaymentToken for Payeezy {} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Payeezy +{ + // Not Implemented (R) +} + +impl api::PaymentVoid for Payeezy {} + +impl ConnectorIntegration + for Payeezy +{ + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + 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::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}v1/transactions/{}", + self.base_url(connectors), + connector_payment_id + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsCancelRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = payeezy::PayeezyCaptureOrVoidRequest::try_from(req)?; + let payeezy_req = + utils::Encode::::encode_to_string_of_json( + &connector_req, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(payeezy_req)) + } + + fn build_request( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .body(types::PaymentsVoidType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: Response, + ) -> CustomResult { + let response: payeezy::PayeezyPaymentsResponse = res + .response + .parse_struct("Payeezy 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::ConnectorAccessToken for Payeezy {} + +impl ConnectorIntegration + for Payeezy +{ +} + +impl api::PaymentSync for Payeezy {} +impl ConnectorIntegration + for Payeezy +{ + fn build_request( + &self, + _req: &types::PaymentsSyncRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::FlowNotSupported { + flow: "Psync".to_owned(), + connector: "payeezy".to_owned(), + } + .into()) + } +} + +impl api::PaymentCapture for Payeezy {} +impl ConnectorIntegration + for Payeezy +{ + fn get_headers( + &self, + req: &types::PaymentsCaptureRouterData, + 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::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}v1/transactions/{}", + self.base_url(connectors), + connector_payment_id + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsCaptureRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = payeezy::PayeezyCaptureOrVoidRequest::try_from(req)?; + let payeezy_req = + utils::Encode::::encode_to_string_of_json( + &connector_req, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(payeezy_req)) + } + + fn build_request( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsCaptureType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCaptureRouterData, + res: Response, + ) -> CustomResult { + let response: payeezy::PayeezyPaymentsResponse = res + .response + .parse_struct("Payeezy 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::PaymentSession for Payeezy {} + +impl ConnectorIntegration + for Payeezy +{ + //TODO: implement sessions flow +} + +impl api::PaymentAuthorize for Payeezy {} + +impl ConnectorIntegration + for Payeezy +{ + 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 connector_req = payeezy::PayeezyPaymentsRequest::try_from(req)?; + let payeezy_req = + utils::Encode::::encode_to_string_of_json( + &connector_req, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(payeezy_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, + )?) + .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: payeezy::PayeezyPaymentsResponse = res + .response + .parse_struct("payeezy Response") + .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 Payeezy {} +impl api::RefundExecute for Payeezy {} +impl api::RefundSync for Payeezy {} + +impl ConnectorIntegration + for Payeezy +{ + 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 { + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}v1/transactions/{}", + self.base_url(connectors), + connector_payment_id + )) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_req = payeezy::PayeezyRefundRequest::try_from(req)?; + let payeezy_req = + utils::Encode::::encode_to_string_of_json( + &connector_req, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(payeezy_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)?) + .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: payeezy::RefundResponse = res + .response + .parse_struct("payeezy RefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RefundsRouterData::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 Payeezy { + fn build_request( + &self, + _req: &types::RefundSyncRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::FlowNotSupported { + flow: "Rsync".to_owned(), + connector: "payeezy".to_owned(), + } + .into()) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Payeezy { + 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() + } +} diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs new file mode 100644 index 0000000000..dd4d9b5aa3 --- /dev/null +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -0,0 +1,486 @@ +use common_utils::ext_traits::Encode; +use error_stack::ResultExt; +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{ + connector::utils::{self, CardData}, + core::errors, + pii::{self}, + types::{self, api, storage::enums, transformers::ForeignFrom}, +}; + +#[derive(Serialize, Debug)] +pub struct PayeezyCard { + #[serde(rename = "type")] + pub card_type: PayeezyCardType, + pub cardholder_name: Secret, + pub card_number: Secret, + pub exp_date: Secret, + pub cvv: Secret, +} + +#[derive(Serialize, Debug)] +pub enum PayeezyCardType { + #[serde(rename = "American Express")] + AmericanExpress, + Visa, + Mastercard, + Discover, +} + +impl TryFrom for PayeezyCardType { + type Error = error_stack::Report; + fn try_from(issuer: utils::CardIssuer) -> Result { + match issuer { + utils::CardIssuer::AmericanExpress => Ok(Self::AmericanExpress), + utils::CardIssuer::Master => Ok(Self::Mastercard), + utils::CardIssuer::Discover => Ok(Self::Discover), + utils::CardIssuer::Visa => Ok(Self::Visa), + _ => Err(errors::ConnectorError::NotSupported { + payment_method: api::enums::PaymentMethod::Card.to_string(), + connector: "Payeezy", + payment_experience: api::enums::PaymentExperience::RedirectToUrl.to_string(), + } + .into()), + } + } +} + +#[derive(Serialize, Debug)] +#[serde(untagged)] +pub enum PayeezyPaymentMethod { + PayeezyCard(PayeezyCard), +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum PayeezyPaymentMethodType { + CreditCard, +} + +#[derive(Serialize, Debug)] +pub struct PayeezyPaymentsRequest { + pub merchant_ref: String, + pub transaction_type: PayeezyTransactionType, + pub method: PayeezyPaymentMethodType, + pub amount: i64, + pub currency_code: String, + pub credit_card: PayeezyPaymentMethod, + pub stored_credentials: Option, +} + +#[derive(Serialize, Debug)] +pub struct StoredCredentials { + pub sequence: Sequence, + pub initiator: Initiator, + pub is_scheduled: bool, + pub cardbrand_original_transaction_id: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum Sequence { + First, + Subsequent, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum Initiator { + Merchant, + CardHolder, +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for PayeezyPaymentsRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + match item.payment_method { + storage_models::enums::PaymentMethod::Card => get_card_specific_payment_data(item), + _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + } + } +} + +fn get_card_specific_payment_data( + item: &types::PaymentsAuthorizeRouterData, +) -> Result> { + let merchant_ref = item.attempt_id.to_string(); + let method = PayeezyPaymentMethodType::CreditCard; + let amount = item.request.amount; + let currency_code = item.request.currency.to_string(); + let credit_card = get_payment_method_data(item)?; + let (transaction_type, stored_credentials) = get_transaction_type_and_stored_creds(item)?; + Ok(PayeezyPaymentsRequest { + merchant_ref, + transaction_type, + method, + amount, + currency_code, + credit_card, + stored_credentials, + }) +} +fn get_transaction_type_and_stored_creds( + item: &types::PaymentsAuthorizeRouterData, +) -> Result< + (PayeezyTransactionType, Option), + error_stack::Report, +> { + let connector_mandate_id = item + .request + .mandate_id + .as_ref() + .and_then(|mandate_ids| mandate_ids.connector_mandate_id.clone()); + let (transaction_type, stored_credentials) = + if is_mandate_payment(item, connector_mandate_id.as_ref()) { + // Mandate payment + ( + PayeezyTransactionType::Recurring, + Some(StoredCredentials { + // connector_mandate_id is not present then it is a First payment, else it is a Subsequent mandate payment + sequence: match connector_mandate_id.is_some() { + true => Sequence::Subsequent, + false => Sequence::First, + }, + // off_session true denotes the customer not present during the checkout process. In other cases customer present at the checkout. + initiator: match item.request.off_session { + Some(true) => Initiator::Merchant, + _ => Initiator::CardHolder, + }, + is_scheduled: true, + // In case of first mandate payment connector_mandate_id would be None, otherwise holds some value + cardbrand_original_transaction_id: connector_mandate_id, + }), + ) + } else { + match item.request.capture_method { + Some(storage_models::enums::CaptureMethod::Manual) => { + Ok((PayeezyTransactionType::Authorize, None)) + } + Some(storage_models::enums::CaptureMethod::Automatic) => { + Ok((PayeezyTransactionType::Purchase, None)) + } + _ => Err(errors::ConnectorError::FlowNotSupported { + flow: item.request.capture_method.unwrap_or_default().to_string(), + connector: "Payeezy".to_string(), + }), + }? + }; + Ok((transaction_type, stored_credentials)) +} + +fn is_mandate_payment( + item: &types::PaymentsAuthorizeRouterData, + connector_mandate_id: Option<&String>, +) -> bool { + item.request.setup_mandate_details.is_some() || connector_mandate_id.is_some() +} + +fn get_payment_method_data( + item: &types::PaymentsAuthorizeRouterData, +) -> Result> { + match item.request.payment_method_data { + api::PaymentMethodData::Card(ref card) => { + let card_type = PayeezyCardType::try_from(card.get_card_issuer()?)?; + let payeezy_card = PayeezyCard { + card_type, + cardholder_name: card.card_holder_name.clone(), + card_number: card.card_number.clone(), + exp_date: card.get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), + cvv: card.card_cvc.clone(), + }; + Ok(PayeezyPaymentMethod::PayeezyCard(payeezy_card)) + } + _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + } +} + +// Auth Struct +pub struct PayeezyAuthType { + pub(super) api_key: String, + pub(super) api_secret: String, + pub(super) merchant_token: String, +} + +impl TryFrom<&types::ConnectorAuthType> for PayeezyAuthType { + type Error = error_stack::Report; + fn try_from(item: &types::ConnectorAuthType) -> Result { + if let types::ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } = item + { + Ok(Self { + api_key: api_key.to_string(), + api_secret: api_secret.to_string(), + merchant_token: key1.to_string(), + }) + } else { + Err(errors::ConnectorError::FailedToObtainAuthType.into()) + } + } +} +// PaymentsResponse + +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum PayeezyPaymentStatus { + Approved, + Declined, + #[default] + #[serde(rename = "Not Processed")] + NotProcessed, +} + +#[derive(Deserialize)] +pub struct PayeezyPaymentsResponse { + pub correlation_id: String, + pub transaction_status: PayeezyPaymentStatus, + pub validation_status: String, + pub transaction_type: PayeezyTransactionType, + pub transaction_id: String, + pub transaction_tag: Option, + pub method: Option, + pub amount: String, + pub currency: String, + pub bank_resp_code: String, + pub bank_message: String, + pub gateway_resp_code: String, + pub gateway_message: String, + pub stored_credentials: Option, +} + +#[derive(Debug, Deserialize)] +pub struct PaymentsStoredCredentials { + cardbrand_original_transaction_id: String, +} + +#[derive(Debug, Serialize)] +pub struct PayeezyCaptureOrVoidRequest { + transaction_tag: String, + transaction_type: PayeezyTransactionType, + amount: String, + currency_code: String, +} + +impl TryFrom<&types::PaymentsCaptureRouterData> for PayeezyCaptureOrVoidRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + let metadata: PayeezyPaymentsMetadata = + utils::to_connector_meta(item.request.connector_meta.clone()) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Self { + transaction_type: PayeezyTransactionType::Capture, + amount: item.request.amount_to_capture.to_string(), + currency_code: item.request.currency.to_string(), + transaction_tag: metadata.transaction_tag, + }) + } +} + +impl TryFrom<&types::PaymentsCancelRouterData> for PayeezyCaptureOrVoidRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCancelRouterData) -> Result { + let metadata: PayeezyPaymentsMetadata = + utils::to_connector_meta(item.request.connector_meta.clone()) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Self { + transaction_type: PayeezyTransactionType::Void, + amount: item + .request + .amount + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + .to_string(), + currency_code: item.request.currency.unwrap_or_default().to_string(), + transaction_tag: metadata.transaction_tag, + }) + } +} +#[derive(Debug, Deserialize, Serialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum PayeezyTransactionType { + Authorize, + Capture, + Purchase, + Recurring, + Void, + Refund, + #[default] + Pending, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PayeezyPaymentsMetadata { + transaction_tag: String, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + let metadata = item + .response + .transaction_tag + .map(|txn_tag| { + Encode::<'_, PayeezyPaymentsMetadata>::encode_to_value( + &construct_payeezy_payments_metadata(txn_tag), + ) + }) + .transpose() + .change_context(errors::ConnectorError::ResponseHandlingFailed)?; + + let mandate_reference = item + .response + .stored_credentials + .map(|credentials| credentials.cardbrand_original_transaction_id); + let status = enums::AttemptStatus::foreign_from(( + item.response.transaction_status, + item.response.transaction_type, + )); + + Ok(Self { + status, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + item.response.transaction_id, + ), + redirection_data: None, + mandate_reference, + connector_metadata: metadata, + }), + ..item.data + }) + } +} + +impl ForeignFrom<(PayeezyPaymentStatus, PayeezyTransactionType)> for enums::AttemptStatus { + fn foreign_from((status, method): (PayeezyPaymentStatus, PayeezyTransactionType)) -> Self { + match status { + PayeezyPaymentStatus::Approved => match method { + PayeezyTransactionType::Authorize => Self::Authorized, + PayeezyTransactionType::Capture + | PayeezyTransactionType::Purchase + | PayeezyTransactionType::Recurring => Self::Charged, + PayeezyTransactionType::Void => Self::Voided, + _ => Self::Pending, + }, + PayeezyPaymentStatus::Declined | PayeezyPaymentStatus::NotProcessed => match method { + PayeezyTransactionType::Capture => Self::CaptureFailed, + PayeezyTransactionType::Authorize + | PayeezyTransactionType::Purchase + | PayeezyTransactionType::Recurring => Self::AuthorizationFailed, + PayeezyTransactionType::Void => Self::VoidFailed, + _ => Self::Pending, + }, + } + } +} + +// REFUND : +// Type definition for RefundRequest +#[derive(Debug, Serialize)] +pub struct PayeezyRefundRequest { + transaction_tag: String, + transaction_type: PayeezyTransactionType, + amount: String, + currency_code: String, +} + +impl TryFrom<&types::RefundsRouterData> for PayeezyRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundsRouterData) -> Result { + let metadata: PayeezyPaymentsMetadata = + utils::to_connector_meta(item.request.connector_metadata.clone()) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Self { + transaction_type: PayeezyTransactionType::Refund, + amount: item.request.refund_amount.to_string(), + currency_code: item.request.currency.to_string(), + transaction_tag: metadata.transaction_tag, + }) + } +} + +// Type definition for Refund Response + +#[derive(Debug, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum RefundStatus { + Approved, + Declined, + #[default] + #[serde(rename = "Not Processed")] + NotProcessed, +} + +impl From for enums::RefundStatus { + fn from(item: RefundStatus) -> Self { + match item { + RefundStatus::Approved => Self::Success, + RefundStatus::Declined => Self::Failure, + RefundStatus::NotProcessed => Self::Pending, + } + } +} + +#[derive(Deserialize)] +pub struct RefundResponse { + pub correlation_id: String, + pub transaction_status: RefundStatus, + pub validation_status: String, + pub transaction_type: String, + pub transaction_id: String, + pub transaction_tag: Option, + pub method: Option, + pub amount: String, + pub currency: String, + pub bank_resp_code: String, + pub bank_message: String, + pub gateway_resp_code: String, + pub gateway_message: String, +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.transaction_id, + refund_status: enums::RefundStatus::from(item.response.transaction_status), + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize)] +pub struct Message { + pub code: String, + pub description: String, +} + +#[derive(Debug, Deserialize)] +pub struct PayeezyError { + pub messages: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct PayeezyErrorResponse { + pub transaction_status: String, + #[serde(rename = "Error")] + pub error: PayeezyError, +} + +fn construct_payeezy_payments_metadata(transaction_tag: String) -> PayeezyPaymentsMetadata { + PayeezyPaymentsMetadata { transaction_tag } +} diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 499e7121cc..124b5f91c3 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -577,6 +577,7 @@ pub async fn mock_add_card_hs( card_cvc, payment_method_id, customer_id: customer_id.map(str::to_string), + name_on_card: card.card_holder_name.to_owned().expose_option(), }; let response = db @@ -616,6 +617,7 @@ pub async fn mock_add_card( card_cvc, payment_method_id, customer_id: customer_id.map(str::to_string), + name_on_card: card.card_holder_name.to_owned().expose_option(), }; let response = db .insert_locker_mock_up(locker_mock_up) @@ -630,7 +632,7 @@ pub async fn mock_add_card( card_number: Some(response.card_number.into()), card_exp_year: Some(response.card_exp_year.into()), card_exp_month: Some(response.card_exp_month.into()), - name_on_card: None, + name_on_card: response.name_on_card.map(|c| c.into()), nickname: response.nickname, customer_id: response.customer_id, duplicate: response.duplicate, @@ -657,7 +659,7 @@ pub async fn mock_get_card<'a>( card_number: Some(locker_mock_up.card_number.into()), card_exp_year: Some(locker_mock_up.card_exp_year.into()), card_exp_month: Some(locker_mock_up.card_exp_month.into()), - name_on_card: None, + name_on_card: locker_mock_up.name_on_card.map(|card| card.into()), nickname: locker_mock_up.nickname, customer_id: locker_mock_up.customer_id, duplicate: locker_mock_up.duplicate, diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index ffb0514b5e..2fbb1cde86 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -360,7 +360,7 @@ pub fn mk_add_card_response( expiry_year: Some(card.card_exp_year), card_token: Some(response.external_id.into()), // [#256] card_fingerprint: Some(response.card_fingerprint), - card_holder_name: None, + card_holder_name: card.card_holder_name, }; api::PaymentMethodResponse { merchant_id: merchant_id.to_owned(), @@ -602,7 +602,7 @@ pub fn get_card_detail( expiry_year: Some(response.card_exp_year), card_token: None, card_fingerprint: None, - card_holder_name: None, + card_holder_name: response.name_on_card, }; Ok(card_detail) } diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index d083e55ec0..06aede4f94 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -101,6 +101,7 @@ default_imp_for_complete_authorize!( connector::Klarna, connector::Multisafepay, connector::Opennode, + connector::Payeezy, connector::Payu, connector::Rapyd, connector::Shift4, @@ -142,6 +143,7 @@ default_imp_for_connector_redirect_response!( connector::Klarna, connector::Multisafepay, connector::Opennode, + connector::Payeezy, connector::Payu, connector::Rapyd, connector::Shift4, @@ -177,6 +179,7 @@ default_imp_for_connector_request_id!( connector::Multisafepay, connector::Nuvei, connector::Opennode, + connector::Payeezy, connector::Payu, connector::Rapyd, connector::Shift4, diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 4c61d25b99..f39c25cd75 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -45,11 +45,14 @@ static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; pub mod headers { pub const ACCEPT: &str = "Accept"; pub const API_KEY: &str = "API-KEY"; + pub const APIKEY: &str = "apikey"; pub const X_CC_API_KEY: &str = "X-CC-Api-Key"; pub const AUTHORIZATION: &str = "Authorization"; pub const CONTENT_TYPE: &str = "Content-Type"; pub const DATE: &str = "Date"; + pub const NONCE: &str = "nonce"; pub const TIMESTAMP: &str = "Timestamp"; + pub const TOKEN: &str = "token"; pub const X_API_KEY: &str = "X-API-KEY"; pub const X_API_VERSION: &str = "X-ApiVersion"; pub const X_MERCHANT_ID: &str = "X-Merchant-Id"; diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 1cfb5e33de..b719986ba9 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -204,6 +204,7 @@ impl ConnectorData { "mollie" => Ok(Box::new(&connector::Mollie)), "nuvei" => Ok(Box::new(&connector::Nuvei)), "opennode" => Ok(Box::new(&connector::Opennode)), + // "payeezy" => Ok(Box::new(&connector::Payeezy)), As psync and rsync are not supported by this connector, it is added as template code for future usage "payu" => Ok(Box::new(&connector::Payu)), "rapyd" => Ok(Box::new(&connector::Rapyd)), "shift4" => Ok(Box::new(&connector::Shift4)), diff --git a/crates/router/tests/connectors/connector_auth.rs b/crates/router/tests/connectors/connector_auth.rs index b636de0a65..39e2d8a4ed 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -21,6 +21,7 @@ pub(crate) struct ConnectorAuthentication { pub multisafepay: Option, pub nuvei: Option, pub opennode: Option, + pub payeezy: Option, pub paypal: Option, pub payu: Option, pub rapyd: Option, diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index 91306502ec..6bf25e42f4 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -17,6 +17,7 @@ mod mollie; mod multisafepay; mod nuvei; mod opennode; +mod payeezy; mod paypal; mod payu; mod rapyd; diff --git a/crates/router/tests/connectors/payeezy.rs b/crates/router/tests/connectors/payeezy.rs new file mode 100644 index 0000000000..0f2ff7bb23 --- /dev/null +++ b/crates/router/tests/connectors/payeezy.rs @@ -0,0 +1,526 @@ +use api_models::payments::{Address, AddressDetails}; +use masking::Secret; +use router::{ + core::errors, + types::{self, api, storage::enums, PaymentsAuthorizeData}, +}; + +use crate::{ + connector_auth::{self}, + utils::{self, ConnectorActions, PaymentInfo}, +}; + +#[derive(Clone, Copy)] +struct PayeezyTest; +impl ConnectorActions for PayeezyTest {} +static CONNECTOR: PayeezyTest = PayeezyTest {}; +impl utils::Connector for PayeezyTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Payeezy; + types::api::ConnectorData { + connector: Box::new(&Payeezy), + connector_name: types::Connector::Dummy, + get_token: types::api::GetToken::Connector, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + types::ConnectorAuthType::from( + connector_auth::ConnectorAuthentication::new() + .payeezy + .expect("Missing connector authentication configuration"), + ) + } + + fn get_name(&self) -> String { + "payeezy".to_string() + } +} + +impl PayeezyTest { + fn get_payment_data() -> Option { + Some(PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: Secret::new(String::from("4012000033330026")), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }) + } + + fn get_payment_info() -> Option { + Some(PaymentInfo { + address: Some(types::PaymentAddress { + billing: Some(Address { + address: Some(AddressDetails { + ..Default::default() + }), + phone: None, + }), + ..Default::default() + }), + ..Default::default() + }) + } + fn get_request_interval(&self) -> u64 { + 20 + } +} + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(PayeezyTest::get_payment_data(), None) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} + +// Captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_capture_authorized_payment() { + let response = CONNECTOR + .authorize_payment(PayeezyTest::get_payment_data(), None) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Authorized); + let connector_payment_id = + utils::get_connector_transaction_id(response.response.clone()).unwrap_or_default(); + let connector_meta = utils::get_connector_metadata(response.response); + let capture_data = types::PaymentsCaptureData { + connector_meta, + ..utils::PaymentCaptureType::default().0 + }; + let capture_response = CONNECTOR + .capture_payment(connector_payment_id, Some(capture_data), None) + .await + .unwrap(); + assert_eq!(capture_response.status, enums::AttemptStatus::Charged); +} + +// Partially captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_capture_authorized_payment() { + let response = CONNECTOR + .authorize_payment(PayeezyTest::get_payment_data(), None) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Authorized); + let connector_payment_id = + utils::get_connector_transaction_id(response.response.clone()).unwrap_or_default(); + let connector_meta = utils::get_connector_metadata(response.response); + let capture_data = types::PaymentsCaptureData { + connector_meta, + amount_to_capture: 50, + ..utils::PaymentCaptureType::default().0 + }; + let capture_response = CONNECTOR + .capture_payment(connector_payment_id, Some(capture_data), None) + .await + .unwrap(); + assert_eq!(capture_response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +#[ignore] +async fn should_sync_authorized_payment() {} + +// Voids a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_void_authorized_payment() { + let response = CONNECTOR + .authorize_payment(PayeezyTest::get_payment_data(), None) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Authorized); + let connector_payment_id = + utils::get_connector_transaction_id(response.response.clone()).unwrap_or_default(); + let connector_meta = utils::get_connector_metadata(response.response); + tokio::time::sleep(std::time::Duration::from_secs( + CONNECTOR.get_request_interval(), + )) + .await; // to avoid 404 error + let response = CONNECTOR + .void_payment( + connector_payment_id, + Some(types::PaymentsCancelData { + connector_meta, + amount: Some(100), + currency: Some(storage_models::enums::Currency::USD), + ..utils::PaymentCancelType::default().0 + }), + None, + ) + .await + .unwrap(); + + assert_eq!(response.status, enums::AttemptStatus::Voided); +} + +// Refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_manually_captured_payment() { + let authorize_response = CONNECTOR + .authorize_payment( + PayeezyTest::get_payment_data(), + PayeezyTest::get_payment_info(), + ) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap(); + let capture_connector_meta = utils::get_connector_metadata(authorize_response.response); + let capture_response = CONNECTOR + .capture_payment( + txn_id.clone(), + Some(types::PaymentsCaptureData { + connector_meta: capture_connector_meta, + ..utils::PaymentCaptureType::default().0 + }), + PayeezyTest::get_payment_info(), + ) + .await + .expect("Capture payment response"); + let capture_txn_id = + utils::get_connector_transaction_id(capture_response.response.clone()).unwrap(); + let refund_connector_metadata = utils::get_connector_metadata(capture_response.response); + let response = CONNECTOR + .refund_payment( + capture_txn_id.clone(), + Some(types::RefundsData { + connector_transaction_id: capture_txn_id, + connector_metadata: refund_connector_metadata, + ..utils::PaymentRefundType::default().0 + }), + PayeezyTest::get_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_manually_captured_payment() { + let authorize_response = CONNECTOR + .authorize_payment( + PayeezyTest::get_payment_data(), + PayeezyTest::get_payment_info(), + ) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response.clone()).unwrap(); + let capture_connector_meta = utils::get_connector_metadata(authorize_response.response); + let capture_response = CONNECTOR + .capture_payment( + txn_id.clone(), + Some(types::PaymentsCaptureData { + connector_meta: capture_connector_meta, + ..utils::PaymentCaptureType::default().0 + }), + PayeezyTest::get_payment_info(), + ) + .await + .expect("Capture payment response"); + let capture_txn_id = + utils::get_connector_transaction_id(capture_response.response.clone()).unwrap(); + let refund_connector_metadata = utils::get_connector_metadata(capture_response.response); + let response = CONNECTOR + .refund_payment( + capture_txn_id.clone(), + Some(types::RefundsData { + refund_amount: 50, + connector_transaction_id: capture_txn_id, + connector_metadata: refund_connector_metadata, + ..utils::PaymentRefundType::default().0 + }), + PayeezyTest::get_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Synchronizes a refund using the manual capture flow (Non 3DS). +#[actix_web::test] +#[ignore] +async fn should_sync_manually_captured_refund() {} + +// Creates a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR + .make_payment( + PayeezyTest::get_payment_data(), + PayeezyTest::get_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] +#[ignore] +async fn should_sync_auto_captured_payment() {} + +// Refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_auto_captured_payment() { + let captured_response = CONNECTOR.make_payment(None, None).await.unwrap(); + assert_eq!(captured_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(captured_response.response.clone()); + let connector_meta = utils::get_connector_metadata(captured_response.response); + let response = CONNECTOR + .refund_payment( + txn_id.clone().unwrap(), + Some(types::RefundsData { + refund_amount: 100, + connector_transaction_id: txn_id.unwrap(), + connector_metadata: connector_meta, + ..utils::PaymentRefundType::default().0 + }), + PayeezyTest::get_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_succeeded_payment() { + let captured_response = CONNECTOR.make_payment(None, None).await.unwrap(); + assert_eq!(captured_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(captured_response.response.clone()); + let connector_meta = utils::get_connector_metadata(captured_response.response); + let response = CONNECTOR + .refund_payment( + txn_id.clone().unwrap(), + Some(types::RefundsData { + refund_amount: 50, + connector_transaction_id: txn_id.unwrap(), + connector_metadata: connector_meta, + ..utils::PaymentRefundType::default().0 + }), + PayeezyTest::get_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_succeeded_payment_multiple_times() { + let captured_response = CONNECTOR.make_payment(None, None).await.unwrap(); + assert_eq!(captured_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(captured_response.response.clone()); + let connector_meta = utils::get_connector_metadata(captured_response.response); + for _x in 0..2 { + let refund_response = CONNECTOR + .refund_payment( + txn_id.clone().unwrap(), + Some(types::RefundsData { + connector_metadata: connector_meta.clone(), + connector_transaction_id: txn_id.clone().unwrap(), + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + PayeezyTest::get_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); + } +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). +#[actix_web::test] +#[ignore] +async fn should_sync_refund() {} + +// Cards Negative scenerios +// Creates a payment with incorrect card issuer. + +#[actix_web::test] +async fn should_throw_not_implemented_for_unsupported_issuer() { + let authorize_data = Some(PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: Secret::new(String::from("630495060000000000")), + ..utils::CCardType::default().0 + }), + capture_method: Some(enums::CaptureMethod::Automatic), + ..utils::PaymentAuthorizeType::default().0 + }); + let response = CONNECTOR + .make_payment(authorize_data, PayeezyTest::get_payment_info()) + .await; + assert_eq!( + *response.unwrap_err().current_context(), + errors::ConnectorError::NotSupported { + payment_method: "card".to_string(), + connector: "Payeezy", + payment_experience: "RedirectToUrl".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(PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: Secret::new(String::from("")), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await; + assert_eq!( + *response.unwrap_err().current_context(), + errors::ConnectorError::NotImplemented("Card Type".to_string()) + ) +} + +// Creates a payment with incorrect CVC. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_cvc() { + let response = CONNECTOR + .make_payment( + Some(PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_cvc: Secret::new("12345d".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + *response.response.unwrap_err().message, + "The cvv provided must be numeric".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(PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_exp_month: Secret::new("20".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + *response.response.unwrap_err().message, + "Bad Request (25) - Invalid Expiry Date".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(PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_exp_year: Secret::new("2000".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Expiry Date is invalid".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[actix_web::test] +#[ignore] +async fn should_fail_void_payment_for_auto_capture() {} + +// Captures a payment using invalid connector payment id. +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let connector_payment_id = "12345678".to_string(); + let capture_response = CONNECTOR + .capture_payment( + connector_payment_id, + Some(types::PaymentsCaptureData { + connector_meta: Some( + serde_json::json!({"transaction_tag" : "10069306640".to_string()}), + ), + amount_to_capture: 50, + ..utils::PaymentCaptureType::default().0 + }), + None, + ) + .await + .unwrap(); + assert_eq!( + capture_response.response.unwrap_err().message, + String::from("Bad Request (69) - Invalid Transaction Tag") + ); +} + +// Refunds a payment with refund amount higher than payment amount. +#[actix_web::test] +async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let captured_response = CONNECTOR.make_payment(None, None).await.unwrap(); + assert_eq!(captured_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(captured_response.response.clone()); + let connector_meta = utils::get_connector_metadata(captured_response.response); + let response = CONNECTOR + .refund_payment( + txn_id.clone().unwrap(), + Some(types::RefundsData { + refund_amount: 1500, + connector_transaction_id: txn_id.unwrap(), + connector_metadata: connector_meta, + ..utils::PaymentRefundType::default().0 + }), + PayeezyTest::get_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + String::from("Bad Request (64) - Invalid Refund"), + ); +} diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 214b6ba246..21a7b1d14c 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -79,3 +79,9 @@ api_key="API Key" [opennode] api_key="API Key" + + +[payeezy] +api_key = "api_key" +key1 = "key1" +api_secret = "secret" \ No newline at end of file diff --git a/crates/storage_models/src/locker_mock_up.rs b/crates/storage_models/src/locker_mock_up.rs index 19553f1cb9..e2e11b334e 100644 --- a/crates/storage_models/src/locker_mock_up.rs +++ b/crates/storage_models/src/locker_mock_up.rs @@ -33,6 +33,7 @@ pub struct LockerMockUpNew { pub card_number: String, pub card_exp_year: String, pub card_exp_month: String, + pub name_on_card: Option, pub card_cvc: Option, pub payment_method_id: Option, pub customer_id: Option, diff --git a/loadtest/config/Development.toml b/loadtest/config/Development.toml index 96a1563e23..43dc503403 100644 --- a/loadtest/config/Development.toml +++ b/loadtest/config/Development.toml @@ -74,6 +74,7 @@ mollie.base_url = "https://api.mollie.com/v2/" multisafepay.base_url = "https://testapi.multisafepay.com/" nuvei.base_url = "https://ppp-test.nuvei.com/" opennode.base_url = "https://dev-api.opennode.com" +payeezy.base_url = "https://api-cert.payeezy.com/" paypal.base_url = "https://www.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" rapyd.base_url = "https://sandboxapi.rapyd.net" @@ -104,6 +105,7 @@ cards = [ "multisafepay", "nuvei", "opennode", + "payeezy", "paypal", "payu", "shift4", diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index feeac2b857..d37e040e3f 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -4,7 +4,7 @@ function find_prev_connector() { git checkout $self cp $self $self.tmp # add new connector to existing list and sort it - connectors=(aci adyen airwallex applepay authorizedotnet bambora bluesnap braintree checkout coinbase cybersource dlocal fiserv globalpay klarna mollie multisafepay nuvei opennode payu rapyd shift4 stripe trustpay worldline worldpay "$1") + connectors=(aci adyen airwallex applepay authorizedotnet bambora bluesnap braintree checkout coinbase cybersource dlocal fiserv globalpay klarna mollie multisafepay nuvei opennode payeezy payu rapyd shift4 stripe trustpay worldline worldpay "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res=`echo ${sorted[@]}` sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp