From 80b74e096d56e08685ad52fb3049f6b611d587b3 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Thu, 3 Aug 2023 17:51:44 +0530 Subject: [PATCH] feat(connector): [Square] Add template code for connector Square (#1834) --- config/config.example.toml | 2 + config/development.toml | 2 + config/docker_compose.toml | 2 + crates/api_models/src/enums.rs | 4 +- crates/router/src/configs/settings.rs | 1 + crates/router/src/connector.rs | 6 +- crates/router/src/connector/square.rs | 517 ++++++++++++++++++ .../src/connector/square/transformers.rs | 205 +++++++ crates/router/src/core/payments/flows.rs | 16 + crates/router/src/types/api.rs | 1 + crates/router/tests/connectors/main.rs | 1 + .../router/tests/connectors/sample_auth.toml | 5 +- crates/router/tests/connectors/square.rs | 419 ++++++++++++++ crates/test_utils/src/connector_auth.rs | 1 + loadtest/config/development.toml | 2 + scripts/add_connector.sh | 2 +- 16 files changed, 1181 insertions(+), 5 deletions(-) create mode 100644 crates/router/src/connector/square.rs create mode 100644 crates/router/src/connector/square/transformers.rs create mode 100644 crates/router/tests/connectors/square.rs diff --git a/config/config.example.toml b/config/config.example.toml index 14a306f1bb..7f3177d0aa 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -188,6 +188,7 @@ payu.base_url = "https://secure.snd.payu.com/" powertranz.base_url = "https://staging.ptranz.com/api/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" +square.base_url = "https://connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" stripe.base_url = "https://api.stripe.com/" stripe.base_url_file_upload = "https://files.stripe.com/" @@ -237,6 +238,7 @@ cards = [ "mollie", "paypal", "shift4", + "square", "stax", "stripe", "worldpay", diff --git a/config/development.toml b/config/development.toml index 457627e067..0f55665013 100644 --- a/config/development.toml +++ b/config/development.toml @@ -94,6 +94,7 @@ cards = [ "payu", "powertranz", "shift4", + "square", "stax", "stripe", "trustpay", @@ -158,6 +159,7 @@ payu.base_url = "https://secure.snd.payu.com/" powertranz.base_url = "https://staging.ptranz.com/api/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" +square.base_url = "https://connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" stripe.base_url = "https://api.stripe.com/" stripe.base_url_file_upload = "https://files.stripe.com/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 97cbf89639..9e6dc440aa 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -111,6 +111,7 @@ payu.base_url = "https://secure.snd.payu.com/" powertranz.base_url = "https://staging.ptranz.com/api/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" +square.base_url = "https://connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" stripe.base_url = "https://api.stripe.com/" stripe.base_url_file_upload = "https://files.stripe.com/" @@ -165,6 +166,7 @@ cards = [ "payu", "powertranz", "shift4", + "square", "stax", "stripe", "trustpay", diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 53303a2230..d3d83d18bf 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -107,6 +107,7 @@ pub enum Connector { Powertranz, Rapyd, Shift4, + // Square, added as template code for future usage, Stax, Stripe, Trustpay, @@ -156,7 +157,6 @@ impl Connector { #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum RoutableConnectors { - Stax, #[cfg(feature = "dummy_connector")] #[serde(rename = "phonypay")] #[strum(serialize = "phonypay")] @@ -221,6 +221,8 @@ pub enum RoutableConnectors { Powertranz, Rapyd, Shift4, + //Square, added as template code for future usage + Stax, Stripe, Trustpay, // Tsys, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 32d126b514..57766ff10f 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -477,6 +477,7 @@ pub struct Connectors { pub powertranz: ConnectorParams, pub rapyd: ConnectorParams, pub shift4: ConnectorParams, + pub square: ConnectorParams, pub stax: ConnectorParams, pub stripe: ConnectorParamsWithFileUploadUrl, pub trustpay: ConnectorParamsWithMoreUrls, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 5c8179a71d..e6fd352f37 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -36,6 +36,7 @@ pub mod payu; pub mod powertranz; pub mod rapyd; pub mod shift4; +pub mod square; pub mod stax; pub mod stripe; pub mod trustpay; @@ -56,6 +57,7 @@ pub use self::{ globepay::Globepay, iatapay::Iatapay, klarna::Klarna, mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, nmi::Nmi, noon::Noon, nuvei::Nuvei, opayo::Opayo, opennode::Opennode, payeezy::Payeezy, payme::Payme, paypal::Paypal, payu::Payu, - powertranz::Powertranz, rapyd::Rapyd, shift4::Shift4, stax::Stax, stripe::Stripe, - trustpay::Trustpay, tsys::Tsys, wise::Wise, worldline::Worldline, worldpay::Worldpay, zen::Zen, + powertranz::Powertranz, rapyd::Rapyd, shift4::Shift4, square::Square, stax::Stax, + stripe::Stripe, trustpay::Trustpay, tsys::Tsys, wise::Wise, worldline::Worldline, + worldpay::Worldpay, zen::Zen, }; diff --git a/crates/router/src/connector/square.rs b/crates/router/src/connector/square.rs new file mode 100644 index 0000000000..0bc41decc0 --- /dev/null +++ b/crates/router/src/connector/square.rs @@ -0,0 +1,517 @@ +mod transformers; + +use std::fmt::Debug; + +use error_stack::{IntoReport, ResultExt}; +use masking::ExposeInterface; +use transformers as square; + +use crate::{ + configs::settings, + core::errors::{self, CustomResult}, + headers, + services::{ + self, + request::{self, Mask}, + ConnectorIntegration, + }, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Square; + +impl api::Payment for Square {} +impl api::PaymentSession for Square {} +impl api::ConnectorAccessToken for Square {} +impl api::PreVerify for Square {} +impl api::PaymentAuthorize for Square {} +impl api::PaymentSync for Square {} +impl api::PaymentCapture for Square {} +impl api::PaymentVoid for Square {} +impl api::Refund for Square {} +impl api::RefundExecute for Square {} +impl api::RefundSync for Square {} +impl api::PaymentToken for Square {} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Square +{ + // Not Implemented (R) +} + +impl ConnectorCommonExt for Square +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + types::PaymentsAuthorizeType::get_content_type(self) + .to_string() + .into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + +impl ConnectorCommon for Square { + fn id(&self) -> &'static str { + "square" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.square.base_url.as_ref() + } + + fn validate_auth_type( + &self, + val: &types::ConnectorAuthType, + ) -> Result<(), error_stack::Report> { + square::SquareAuthType::try_from(val)?; + Ok(()) + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = square::SquareAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![( + headers::AUTHORIZATION.to_string(), + auth.api_key.expose().into_masked(), + )]) + } + + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: square::SquareErrorResponse = res + .response + .parse_struct("SquareErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.code, + message: response.message, + reason: response.reason, + }) + } +} + +impl ConnectorIntegration + for Square +{ + //TODO: implement sessions flow +} + +impl ConnectorIntegration + for Square +{ +} + +impl ConnectorIntegration + for Square +{ +} + +impl ConnectorIntegration + for Square +{ + 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 { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = square::SquarePaymentsRequest::try_from(req)?; + let square_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(square_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: square::SquarePaymentsResponse = res + .response + .parse_struct("Square 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 Square +{ + 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 { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + 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: square::SquarePaymentsResponse = res + .response + .parse_struct("square 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 Square +{ + 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 { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + _req: &types::PaymentsCaptureRouterData, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + } + + 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)?) + .attach_default_headers() + .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: square::SquarePaymentsResponse = res + .response + .parse_struct("Square PaymentsCaptureResponse") + .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 Square +{ +} + +impl ConnectorIntegration for Square { + 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 { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = square::SquareRefundRequest::try_from(req)?; + let square_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(square_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: square::RefundResponse = + res.response + .parse_struct("square RefundResponse") + .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 Square { + 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 { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + 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: square::RefundResponse = res + .response + .parse_struct("square 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 Square { + 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/square/transformers.rs b/crates/router/src/connector/square/transformers.rs new file mode 100644 index 0000000000..d2fa508f6f --- /dev/null +++ b/crates/router/src/connector/square/transformers.rs @@ -0,0 +1,205 @@ +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{ + connector::utils::PaymentsAuthorizeRequestData, + core::errors, + types::{self, api, storage::enums}, +}; + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct SquarePaymentsRequest { + amount: i64, + card: SquareCard, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct SquareCard { + name: Secret, + number: cards::CardNumber, + expiry_month: Secret, + expiry_year: Secret, + cvc: Secret, + complete: bool, +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for SquarePaymentsRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + match item.request.payment_method_data.clone() { + api::PaymentMethodData::Card(req_card) => { + let card = SquareCard { + name: req_card.card_holder_name, + number: req_card.card_number, + expiry_month: req_card.card_exp_month, + expiry_year: req_card.card_exp_year, + cvc: req_card.card_cvc, + complete: item.request.is_auto_capture()?, + }; + Ok(Self { + amount: item.request.amount, + card, + }) + } + _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + } + } +} + +//TODO: Fill the struct with respective fields +// Auth Struct +pub struct SquareAuthType { + pub(super) api_key: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for SquareAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { + api_key: api_key.to_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} +// PaymentsResponse +//TODO: Append the remaining status flags +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum SquarePaymentStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for enums::AttemptStatus { + fn from(item: SquarePaymentStatus) -> Self { + match item { + SquarePaymentStatus::Succeeded => Self::Charged, + SquarePaymentStatus::Failed => Self::Failure, + SquarePaymentStatus::Processing => Self::Authorizing, + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct SquarePaymentsResponse { + status: SquarePaymentStatus, + id: String, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::from(item.response.status), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +// REFUND : +// Type definition for RefundRequest +#[derive(Default, Debug, Serialize)] +pub struct SquareRefundRequest { + pub amount: i64, +} + +impl TryFrom<&types::RefundsRouterData> for SquareRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &types::RefundsRouterData) -> Result { + Ok(Self { + amount: item.request.refund_amount, + }) + } +} + +// Type definition for Refund Response + +#[allow(dead_code)] +#[derive(Debug, Serialize, Default, Deserialize, Clone)] +pub enum RefundStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for enums::RefundStatus { + fn from(item: RefundStatus) -> Self { + match item { + RefundStatus::Succeeded => Self::Success, + RefundStatus::Failed => Self::Failure, + RefundStatus::Processing => Self::Pending, + //TODO: Review mapping + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, 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 { + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +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.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct SquareErrorResponse { + pub status_code: u16, + pub code: String, + pub message: String, + pub reason: Option, +} diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index feaba9a708..51992b9f7a 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -163,6 +163,7 @@ default_imp_for_complete_authorize!( connector::Payme, connector::Payu, connector::Rapyd, + connector::Square, connector::Stax, connector::Stripe, connector::Trustpay, @@ -236,6 +237,7 @@ default_imp_for_create_customer!( connector::Powertranz, connector::Rapyd, connector::Shift4, + connector::Square, connector::Trustpay, connector::Tsys, connector::Wise, @@ -300,6 +302,7 @@ default_imp_for_connector_redirect_response!( connector::Powertranz, connector::Rapyd, connector::Shift4, + connector::Square, connector::Stax, connector::Tsys, connector::Wise, @@ -353,6 +356,7 @@ default_imp_for_connector_request_id!( connector::Powertranz, connector::Rapyd, connector::Shift4, + connector::Square, connector::Stax, connector::Stripe, connector::Trustpay, @@ -428,6 +432,7 @@ default_imp_for_accept_dispute!( connector::Powertranz, connector::Rapyd, connector::Shift4, + connector::Square, connector::Stax, connector::Stripe, connector::Trustpay, @@ -523,6 +528,7 @@ default_imp_for_file_upload!( connector::Powertranz, connector::Rapyd, connector::Shift4, + connector::Square, connector::Stax, connector::Trustpay, connector::Tsys, @@ -595,6 +601,7 @@ default_imp_for_submit_evidence!( connector::Powertranz, connector::Rapyd, connector::Shift4, + connector::Square, connector::Stax, connector::Trustpay, connector::Tsys, @@ -667,6 +674,7 @@ default_imp_for_defend_dispute!( connector::Powertranz, connector::Rapyd, connector::Shift4, + connector::Square, connector::Stax, connector::Stripe, connector::Trustpay, @@ -742,6 +750,7 @@ default_imp_for_pre_processing_steps!( connector::Powertranz, connector::Rapyd, connector::Shift4, + connector::Square, connector::Stax, connector::Tsys, connector::Wise, @@ -796,6 +805,7 @@ default_imp_for_payouts!( connector::Payu, connector::Powertranz, connector::Rapyd, + connector::Square, connector::Stax, connector::Stripe, connector::Shift4, @@ -869,6 +879,7 @@ default_imp_for_payouts_create!( connector::Payu, connector::Powertranz, connector::Rapyd, + connector::Square, connector::Stax, connector::Stripe, connector::Shift4, @@ -945,6 +956,7 @@ default_imp_for_payouts_eligibility!( connector::Payu, connector::Powertranz, connector::Rapyd, + connector::Square, connector::Stax, connector::Stripe, connector::Shift4, @@ -1018,6 +1030,7 @@ default_imp_for_payouts_fulfill!( connector::Payu, connector::Powertranz, connector::Rapyd, + connector::Square, connector::Stax, connector::Stripe, connector::Shift4, @@ -1091,6 +1104,7 @@ default_imp_for_payouts_cancel!( connector::Payu, connector::Powertranz, connector::Rapyd, + connector::Square, connector::Stax, connector::Stripe, connector::Shift4, @@ -1165,6 +1179,7 @@ default_imp_for_payouts_quote!( connector::Payu, connector::Powertranz, connector::Rapyd, + connector::Square, connector::Stax, connector::Stripe, connector::Shift4, @@ -1239,6 +1254,7 @@ default_imp_for_payouts_recipient!( connector::Payu, connector::Powertranz, connector::Rapyd, + connector::Square, connector::Stax, connector::Stripe, connector::Shift4, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index d7d8897eeb..4882d7a129 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -323,6 +323,7 @@ impl ConnectorData { enums::Connector::Powertranz => Ok(Box::new(&connector::Powertranz)), enums::Connector::Rapyd => Ok(Box::new(&connector::Rapyd)), enums::Connector::Shift4 => Ok(Box::new(&connector::Shift4)), + //enums::Connector::Square => Ok(Box::new(&connector::Square)), it is added as template code for future usage enums::Connector::Stax => Ok(Box::new(&connector::Stax)), enums::Connector::Stripe => Ok(Box::new(&connector::Stripe)), enums::Connector::Wise => Ok(Box::new(&connector::Wise)), diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index 5fa7ebdb2c..bbb1b14bd1 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -42,6 +42,7 @@ mod payu; mod powertranz; mod rapyd; mod shift4; +mod square; mod stax; mod stripe; mod trustpay; diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 22223275af..981d5b4a5c 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -167,4 +167,7 @@ api_key="API Key" [boku] api_key="API Key" -key1 = "MERCHANT ID" \ No newline at end of file +key1 = "transaction key" + +[square] +api_key="API Key" diff --git a/crates/router/tests/connectors/square.rs b/crates/router/tests/connectors/square.rs new file mode 100644 index 0000000000..8dbccd1312 --- /dev/null +++ b/crates/router/tests/connectors/square.rs @@ -0,0 +1,419 @@ +use masking::Secret; +use router::types::{self, api, storage::enums}; +use test_utils::connector_auth::ConnectorAuthentication; + +use crate::utils::{self, ConnectorActions}; + +#[derive(Clone, Copy)] +struct SquareTest; +impl ConnectorActions for SquareTest {} +impl utils::Connector for SquareTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Square; + types::api::ConnectorData { + connector: Box::new(&Square), + connector_name: types::Connector::DummyConnector1, + get_token: types::api::GetToken::Connector, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + utils::to_connector_auth_type( + ConnectorAuthentication::new() + .square + .expect("Missing connector authentication configuration") + .into(), + ) + } + + fn get_name(&self) -> String { + "square".to_string() + } +} + +static CONNECTOR: SquareTest = SquareTest {}; + +fn get_default_payment_info() -> Option { + None +} + +fn payment_method_details() -> Option { + None +} + +// 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(payment_method_details(), get_default_payment_info()) + .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_and_capture_payment(payment_method_details(), None, get_default_payment_info()) + .await + .expect("Capture payment response"); + assert_eq!(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_and_capture_payment( + payment_method_details(), + Some(types::PaymentsCaptureData { + amount_to_capture: 50, + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(), + ) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .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(), + ), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized,); +} + +// Voids a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_void_authorized_payment() { + let response = CONNECTOR + .authorize_and_void_payment( + payment_method_details(), + Some(types::PaymentsCancelData { + connector_transaction_id: String::from(""), + cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("Void payment response"); + 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 response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_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 response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_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] +async fn should_sync_manually_captured_refund() { + let refund_response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_auto_captured_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: router::types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + capture_method: Some(enums::CaptureMethod::Automatic), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_auto_captured_payment() { + let response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_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 refund_response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .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). +#[actix_web::test] +async fn should_refund_succeeded_payment_multiple_times() { + CONNECTOR + .make_payment_and_multiple_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await; +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_refund() { + let refund_response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Cards Negative scenerios +// 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 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's security code is invalid.".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 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration month is invalid.".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 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration year is invalid.".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let void_response = CONNECTOR + .void_payment(txn_id.unwrap(), None, get_default_payment_info()) + .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. +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, get_default_payment_info()) + .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. +#[actix_web::test] +async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 150, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .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/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index c19c1ffea8..7599259377 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -48,6 +48,7 @@ pub struct ConnectorAuthentication { pub powertranz: Option, pub rapyd: Option, pub shift4: Option, + pub square: Option, pub stax: Option, pub stripe: Option, pub stripe_au: Option, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index b37be72d6c..8bf0f1d985 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -97,6 +97,7 @@ payu.base_url = "https://secure.snd.payu.com/" powertranz.base_url = "https://staging.ptranz.com/api/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" +square.base_url = "https://connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" stripe.base_url = "https://api.stripe.com/" stripe.base_url_file_upload = "https://files.stripe.com/" @@ -150,6 +151,7 @@ cards = [ "payu", "powertranz", "shift4", + "square", "stax", "stripe", "trustpay", diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 15f27f8c95..4302e535ce 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -6,7 +6,7 @@ function find_prev_connector() { git checkout $self cp $self $self.tmp # Add new connector to existing list and sort it - connectors=(aci adyen airwallex applepay authorizedotnet bambora bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector fiserv forte globalpay globepay iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu powertranz rapyd shift4 stax stripe trustpay tsys wise worldline worldpay "$1") + connectors=(aci adyen airwallex applepay authorizedotnet bambora bitpay bluesnap boku braintree cashtocode checkout coinbase cryptopay cybersource dlocal dummyconnector fiserv forte globalpay globepay iatapay klarna mollie multisafepay nexinets noon nuvei opayo opennode payeezy payme paypal payu powertranz rapyd shift4 square stax stripe trustpay tsys wise worldline worldpay "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res=`echo ${sorted[@]}` sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp