From c9fe389b2c04817a843e34de0aab3d024bb31f19 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Thu, 31 Aug 2023 17:09:16 +0530 Subject: [PATCH] feat(connector): [Square] Implement Card Payments for Square (#1902) --- config/config.example.toml | 2 + config/development.toml | 2 + config/docker_compose.toml | 2 + crates/api_models/src/enums.rs | 4 +- crates/router/src/connector/square.rs | 416 ++++++++++++++++-- .../src/connector/square/transformers.rs | 390 ++++++++++++---- crates/router/src/core/admin.rs | 4 + crates/router/src/core/payments/flows.rs | 2 +- .../src/core/payments/flows/authorize_flow.rs | 7 +- .../src/core/payments/flows/verify_flow.rs | 7 +- .../router/src/core/payments/tokenization.rs | 16 +- .../router/src/core/payments/transformers.rs | 1 + crates/router/src/types.rs | 41 +- crates/router/src/types/api.rs | 2 +- .../router/tests/connectors/sample_auth.toml | 1 + crates/router/tests/connectors/square.rs | 358 +++++++++++---- crates/router/tests/connectors/stax.rs | 8 + crates/router/tests/connectors/utils.rs | 2 + crates/test_utils/src/connector_auth.rs | 2 +- loadtest/config/development.toml | 1 + openapi/openapi_spec.json | 1 + 21 files changed, 1043 insertions(+), 226 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index bc3439a105..547aef7afe 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -193,6 +193,7 @@ 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/" +square.secondary_base_url = "https://pci-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/" @@ -306,6 +307,7 @@ stripe = { long_lived_token = false, payment_method = "wallet", payment_method_t checkout = { long_lived_token = false, payment_method = "wallet" } mollie = {long_lived_token = false, payment_method = "card"} stax = { long_lived_token = true, payment_method = "card,bank_debit" } +square = {long_lived_token = false, payment_method = "card"} braintree = { long_lived_token = false, payment_method = "card" } [dummy_connector] diff --git a/config/development.toml b/config/development.toml index b0ff5f5c2b..4f84052253 100644 --- a/config/development.toml +++ b/config/development.toml @@ -168,6 +168,7 @@ 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/" +square.secondary_base_url = "https://pci-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/" @@ -366,6 +367,7 @@ stripe = { long_lived_token = false, payment_method = "wallet", payment_method_t checkout = { long_lived_token = false, payment_method = "wallet" } stax = { long_lived_token = true, payment_method = "card,bank_debit" } mollie = {long_lived_token = false, payment_method = "card"} +square = {long_lived_token = false, payment_method = "card"} braintree = { long_lived_token = false, payment_method = "card" } [connector_customer] diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 10fb354782..66f7e7e8ac 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -114,6 +114,7 @@ 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/" +square.secondary_base_url = "https://pci-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/" @@ -197,6 +198,7 @@ stripe = { long_lived_token = false, payment_method = "wallet", payment_method_t checkout = { long_lived_token = false, payment_method = "wallet" } mollie = {long_lived_token = false, payment_method = "card"} stax = { long_lived_token = true, payment_method = "card,bank_debit" } +square = {long_lived_token = false, payment_method = "card"} braintree = { long_lived_token = false, payment_method = "card" } [dummy_connector] diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 392e8afb67..d36f288943 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -108,7 +108,7 @@ pub enum Connector { Powertranz, Rapyd, Shift4, - // Square, added as template code for future usage, + Square, Stax, Stripe, Trustpay, @@ -223,7 +223,7 @@ pub enum RoutableConnectors { Powertranz, Rapyd, Shift4, - //Square, added as template code for future usage + Square, Stax, Stripe, Trustpay, diff --git a/crates/router/src/connector/square.rs b/crates/router/src/connector/square.rs index ce3d9fbfad..6a35a4b551 100644 --- a/crates/router/src/connector/square.rs +++ b/crates/router/src/connector/square.rs @@ -1,14 +1,20 @@ -mod transformers; +pub mod transformers; use std::fmt::Debug; +use api_models::enums; use error_stack::{IntoReport, ResultExt}; -use masking::ExposeInterface; +use masking::PeekInterface; use transformers as square; +use super::utils::RefundsRequestData; use crate::{ configs::settings, - core::errors::{self, CustomResult}, + consts, + core::{ + errors::{self, CustomResult}, + payments, + }, headers, services::{ self, @@ -39,16 +45,6 @@ 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, @@ -91,7 +87,7 @@ impl ConnectorCommon for Square { .change_context(errors::ConnectorError::FailedToObtainAuthType)?; Ok(vec![( headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), + format!("Bearer {}", auth.api_key.peek()).into_masked(), )]) } @@ -104,16 +100,45 @@ impl ConnectorCommon for Square { .parse_struct("SquareErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let mut reason_list = Vec::new(); + for error_iter in response.errors.iter() { + if let Some(error) = error_iter.detail.clone() { + reason_list.push(error) + } + } + let reason = reason_list.join(" "); + Ok(ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, + code: response + .errors + .first() + .and_then(|error| error.code.clone()) + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: response + .errors + .first() + .and_then(|error| error.category.clone()) + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(reason), }) } } -impl ConnectorValidation for Square {} +impl ConnectorValidation for Square { + fn validate_capture_method( + &self, + capture_method: Option, + ) -> CustomResult<(), errors::ConnectorError> { + let capture_method = capture_method.unwrap_or_default(); + match capture_method { + enums::CaptureMethod::Automatic | enums::CaptureMethod::Manual => Ok(()), + enums::CaptureMethod::ManualMultiple | enums::CaptureMethod::Scheduled => Err( + super::utils::construct_not_implemented_error_report(capture_method, self.id()), + ), + } + } +} impl ConnectorIntegration for Square @@ -131,6 +156,230 @@ impl ConnectorIntegration for Square +{ + async fn execute_pretasks( + &self, + router_data: &mut types::TokenizationRouterData, + app_state: &crate::routes::AppState, + ) -> CustomResult<(), errors::ConnectorError> { + let integ: Box< + &(dyn ConnectorIntegration< + api::AuthorizeSessionToken, + types::AuthorizeSessionTokenData, + types::PaymentsResponseData, + > + Send + + Sync + + 'static), + > = Box::new(&Self); + + let authorize_session_token_data = types::AuthorizeSessionTokenData { + connector_transaction_id: router_data.payment_id.clone(), + amount_to_capture: None, + currency: router_data.request.currency, + amount: router_data.request.amount, + }; + + let authorize_data = &types::PaymentsAuthorizeSessionTokenRouterData::from(( + &router_data.to_owned(), + authorize_session_token_data, + )); + + let resp = services::execute_connector_processing_step( + app_state, + integ, + authorize_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await?; + + router_data.session_token = resp.session_token; + Ok(()) + } + + fn get_headers( + &self, + _req: &types::TokenizationRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + Ok(vec![( + headers::CONTENT_TYPE.to_string(), + types::TokenizationType::get_content_type(self) + .to_string() + .into(), + )]) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::TokenizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}v2/card-nonce", + connectors + .square + .secondary_base_url + .clone() + .ok_or(errors::ConnectorError::FailedToObtainIntegrationUrl)?, + )) + } + + fn get_request_body( + &self, + req: &types::TokenizationRouterData, + ) -> CustomResult, errors::ConnectorError> { + let connector_request = square::SquareTokenRequest::try_from(req)?; + + let square_req = types::RequestBody::log_and_get_request_body( + &connector_request, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(square_req)) + } + + fn build_request( + &self, + req: &types::TokenizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::TokenizationType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::TokenizationType::get_headers(self, req, connectors)?) + .body(types::TokenizationType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::TokenizationRouterData, + res: Response, + ) -> CustomResult + where + types::PaymentsResponseData: Clone, + { + let response: square::SquareTokenResponse = res + .response + .parse_struct("SquareTokenResponse") + .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< + api::AuthorizeSessionToken, + types::AuthorizeSessionTokenData, + types::PaymentsResponseData, + > for Square +{ + fn get_headers( + &self, + _req: &types::PaymentsAuthorizeSessionTokenRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + Ok(vec![( + headers::CONTENT_TYPE.to_string(), + types::PaymentsAuthorizeType::get_content_type(self) + .to_string() + .into(), + )]) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsAuthorizeSessionTokenRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let auth = square::SquareAuthType::try_from(&req.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + + Ok(format!( + "{}payments/hydrate?applicationId={}", + connectors + .square + .secondary_base_url + .clone() + .ok_or(errors::ConnectorError::FailedToObtainIntegrationUrl)?, + auth.key1.peek() + )) + } + + fn build_request( + &self, + req: &types::PaymentsAuthorizeSessionTokenRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::PaymentsPreAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsPreAuthorizeType::get_headers( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsAuthorizeSessionTokenRouterData, + res: Response, + ) -> CustomResult { + let response: square::SquareSessionResponse = res + .response + .parse_struct("SquareSessionResponse") + .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 { @@ -149,9 +398,9 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}v2/payments", self.base_url(connectors))) } fn get_request_body( @@ -159,6 +408,7 @@ impl ConnectorIntegration 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, @@ -195,7 +445,7 @@ impl ConnectorIntegration CustomResult { let response: square::SquarePaymentsResponse = res .response - .parse_struct("Square PaymentsAuthorizeResponse") + .parse_struct("SquarePaymentsAuthorizeResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -229,10 +479,19 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + + Ok(format!( + "{}v2/payments/{connector_payment_id}", + self.base_url(connectors), + )) } fn build_request( @@ -257,7 +516,7 @@ impl ConnectorIntegration CustomResult { let response: square::SquarePaymentsResponse = res .response - .parse_struct("square PaymentsSyncResponse") + .parse_struct("SquarePaymentsSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -291,17 +550,14 @@ impl ConnectorIntegration 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()) + Ok(format!( + "{}v2/payments/{}/complete", + self.base_url(connectors), + req.request.connector_transaction_id, + )) } fn build_request( @@ -317,7 +573,6 @@ impl ConnectorIntegration CustomResult { let response: square::SquarePaymentsResponse = res .response - .parse_struct("Square PaymentsCaptureResponse") + .parse_struct("SquarePaymentsCaptureResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, @@ -349,6 +604,67 @@ impl ConnectorIntegration for Square { + 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 { + Ok(format!( + "{}v2/payments/{}/cancel", + self.base_url(connectors), + req.request.connector_transaction_id, + )) + } + + 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)?) + .attach_default_headers() + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: Response, + ) -> CustomResult { + let response: square::SquarePaymentsResponse = res + .response + .parse_struct("SquarePaymentsVoidResponse") + .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 { @@ -367,9 +683,9 @@ impl ConnectorIntegration, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}v2/refunds", self.base_url(connectors),)) } fn get_request_body( @@ -407,10 +723,10 @@ impl ConnectorIntegration, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: square::RefundResponse = - res.response - .parse_struct("square RefundResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + let response: square::RefundResponse = res + .response + .parse_struct("SquareRefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), @@ -441,10 +757,14 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}v2/refunds/{}", + self.base_url(connectors), + req.request.get_connector_refund_id()?, + )) } fn build_request( @@ -458,7 +778,6 @@ impl ConnectorIntegration CustomResult { let response: square::RefundResponse = res .response - .parse_struct("square RefundSyncResponse") + .parse_struct("SquareRefundSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { response, data: data.clone(), diff --git a/crates/router/src/connector/square/transformers.rs b/crates/router/src/connector/square/transformers.rs index d2fa508f6f..9340bd9fa4 100644 --- a/crates/router/src/connector/square/transformers.rs +++ b/crates/router/src/connector/square/transformers.rs @@ -1,95 +1,328 @@ -use masking::Secret; +use api_models::payments::{BankDebitData, PayLaterData, WalletData}; +use error_stack::{IntoReport, ResultExt}; +use masking::{ExposeInterface, PeekInterface, Secret}; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::PaymentsAuthorizeRequestData, + connector::utils::{CardData, PaymentsAuthorizeRequestData, RouterData}, core::errors, - types::{self, api, storage::enums}, + types::{ + self, api, + storage::{self, enums}, + }, }; -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct SquarePaymentsRequest { - amount: i64, - card: SquareCard, +impl TryFrom<(&types::TokenizationRouterData, BankDebitData)> for SquareTokenRequest { + type Error = error_stack::Report; + fn try_from( + value: (&types::TokenizationRouterData, BankDebitData), + ) -> Result { + let (item, bank_debit_data) = value; + match bank_debit_data { + BankDebitData::AchBankDebit { .. } => Err(errors::ConnectorError::NotImplemented( + "Payment Method".to_string(), + )) + .into_report(), + _ => Err(errors::ConnectorError::NotSupported { + message: format!("{:?}", item.request.payment_method_data), + connector: "Square", + })?, + } + } } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct SquareCard { - name: Secret, +impl TryFrom<(&types::TokenizationRouterData, api_models::payments::Card)> for SquareTokenRequest { + type Error = error_stack::Report; + fn try_from( + value: (&types::TokenizationRouterData, api_models::payments::Card), + ) -> Result { + let (item, card_data) = value; + let auth = SquareAuthType::try_from(&item.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let exp_year = Secret::new( + card_data + .get_expiry_year_4_digit() + .peek() + .parse::() + .into_report() + .change_context(errors::ConnectorError::DateFormattingFailed)?, + ); + let exp_month = Secret::new( + card_data + .card_exp_month + .peek() + .parse::() + .into_report() + .change_context(errors::ConnectorError::DateFormattingFailed)?, + ); + //The below error will never happen because if session-id is not generated it would give error in execute_pretasks itself. + let session_id = Secret::new( + item.session_token + .clone() + .ok_or(errors::ConnectorError::RequestEncodingFailed)?, + ); + Ok(Self::Card(SquareTokenizeData { + client_id: auth.key1, + session_id, + card_data: SquareCardData { + exp_year, + exp_month, + number: card_data.card_number, + cvv: card_data.card_cvc, + }, + })) + } +} + +impl TryFrom<(&types::TokenizationRouterData, PayLaterData)> for SquareTokenRequest { + type Error = error_stack::Report; + fn try_from( + value: (&types::TokenizationRouterData, PayLaterData), + ) -> Result { + let (item, pay_later_data) = value; + match pay_later_data { + PayLaterData::AfterpayClearpayRedirect { .. } => Err( + errors::ConnectorError::NotImplemented("Payment Method".to_string()), + ) + .into_report(), + _ => Err(errors::ConnectorError::NotSupported { + message: format!("{:?}", item.request.payment_method_data), + connector: "Square", + })?, + } + } +} + +impl TryFrom<(&types::TokenizationRouterData, WalletData)> for SquareTokenRequest { + type Error = error_stack::Report; + fn try_from(value: (&types::TokenizationRouterData, WalletData)) -> Result { + let (item, wallet_data) = value; + match wallet_data { + WalletData::ApplePay(_) => Err(errors::ConnectorError::NotImplemented( + "Payment Method".to_string(), + )) + .into_report(), + WalletData::GooglePay(_) => Err(errors::ConnectorError::NotImplemented( + "Payment Method".to_string(), + )) + .into_report(), + _ => Err(errors::ConnectorError::NotSupported { + message: format!("{:?}", item.request.payment_method_data), + connector: "Square", + })?, + } + } +} + +#[derive(Debug, Serialize)] +pub struct SquareCardData { + cvv: Secret, + exp_month: Secret, + exp_year: Secret, number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, +} +#[derive(Debug, Serialize)] +pub struct SquareTokenizeData { + client_id: Secret, + session_id: Secret, + card_data: SquareCardData, +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum SquareTokenRequest { + Card(SquareTokenizeData), +} + +impl TryFrom<&types::TokenizationRouterData> for SquareTokenRequest { + type Error = error_stack::Report; + fn try_from(item: &types::TokenizationRouterData) -> Result { + match item.request.payment_method_data.clone() { + api::PaymentMethodData::BankDebit(bank_debit_data) => { + Self::try_from((item, bank_debit_data)) + } + api::PaymentMethodData::Card(card_data) => Self::try_from((item, card_data)), + api::PaymentMethodData::Wallet(wallet_data) => Self::try_from((item, wallet_data)), + api::PaymentMethodData::PayLater(pay_later_data) => { + Self::try_from((item, pay_later_data)) + } + api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + "Payment Method".to_string(), + )) + .into_report(), + api::PaymentMethodData::BankRedirect(_) + | api::PaymentMethodData::BankTransfer(_) + | api::PaymentMethodData::CardRedirect(_) + | api::PaymentMethodData::Crypto(_) + | api::PaymentMethodData::MandatePayment + | api::PaymentMethodData::Reward + | api::PaymentMethodData::Upi(_) + | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + message: format!("{:?}", item.request.payment_method_data), + connector: "Square", + })?, + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SquareSessionResponse { + session_id: String, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + Ok(Self { + status: storage::enums::AttemptStatus::Pending, + session_token: Some(item.response.session_id.clone()), + response: Ok(types::PaymentsResponseData::SessionTokenResponse { + session_token: item.response.session_id, + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize)] +pub struct SquareTokenResponse { + card_nonce: Secret, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::PaymentsResponseData::TokenizationResponse { + token: item.response.card_nonce.expose(), + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SquarePaymentsAmountData { + amount: i64, + currency: enums::Currency, +} +#[derive(Debug, Serialize)] +pub struct SquarePaymentsRequestExternalDetails { + source: String, + #[serde(rename = "type")] + source_type: String, +} + +#[derive(Debug, Serialize)] +pub struct SquarePaymentsRequest { + amount_money: SquarePaymentsAmountData, + idempotency_key: Secret, + source_id: Secret, + autocomplete: bool, + external_details: SquarePaymentsRequestExternalDetails, } impl TryFrom<&types::PaymentsAuthorizeRouterData> for SquarePaymentsRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + let autocomplete = item.request.is_auto_capture()?; 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 { + api::PaymentMethodData::Card(_) => Ok(Self { + idempotency_key: Secret::new(item.attempt_id.clone()), + source_id: Secret::new(item.get_payment_method_token()?), + amount_money: SquarePaymentsAmountData { amount: item.request.amount, - card, - }) - } - _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), + currency: item.request.currency, + }, + autocomplete, + external_details: SquarePaymentsRequestExternalDetails { + source: "Hyperswitch".to_string(), + source_type: "Card".to_string(), + }, + }), + api::PaymentMethodData::BankDebit(_) + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::PayLater(_) + | api::PaymentMethodData::Wallet(_) => Err(errors::ConnectorError::NotImplemented( + "Payment Method".to_string(), + )) + .into_report(), + api::PaymentMethodData::BankRedirect(_) + | api::PaymentMethodData::BankTransfer(_) + | api::PaymentMethodData::CardRedirect(_) + | api::PaymentMethodData::Crypto(_) + | api::PaymentMethodData::MandatePayment + | api::PaymentMethodData::Reward + | api::PaymentMethodData::Upi(_) + | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + message: format!("{:?}", item.request.payment_method_data), + connector: "Square", + })?, } } } -//TODO: Fill the struct with respective fields // Auth Struct pub struct SquareAuthType { pub(super) api_key: Secret, + pub(super) key1: 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 { + types::ConnectorAuthType::BodyKey { api_key, key1, .. } => Ok(Self { api_key: api_key.to_owned(), + key1: key1.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")] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "UPPERCASE")] pub enum SquarePaymentStatus { - Succeeded, + Completed, Failed, - #[default] - Processing, + Approved, + Canceled, + Pending, } impl From for enums::AttemptStatus { fn from(item: SquarePaymentStatus) -> Self { match item { - SquarePaymentStatus::Succeeded => Self::Charged, + SquarePaymentStatus::Completed => Self::Charged, + SquarePaymentStatus::Approved => Self::Authorized, SquarePaymentStatus::Failed => Self::Failure, - SquarePaymentStatus::Processing => Self::Authorizing, + SquarePaymentStatus::Canceled => Self::Voided, + SquarePaymentStatus::Pending => Self::Pending, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct SquarePaymentsResponse { +#[derive(Debug, Deserialize)] +pub struct SquarePaymentsResponseDetails { status: SquarePaymentStatus, id: String, + amount_money: SquarePaymentsAmountData, +} +#[derive(Debug, Deserialize)] +pub struct SquarePaymentsResponse { + payment: SquarePaymentsResponseDetails, } impl @@ -101,64 +334,71 @@ impl item: types::ResponseRouterData, ) -> Result { Ok(Self { - status: enums::AttemptStatus::from(item.response.status), + status: enums::AttemptStatus::from(item.response.payment.status), response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + resource_id: types::ResponseId::ConnectorTransactionId(item.response.payment.id), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, connector_response_reference_id: None, }), + amount_captured: Some(item.response.payment.amount_money.amount), ..item.data }) } } -//TODO: Fill the struct with respective fields // REFUND : // Type definition for RefundRequest -#[derive(Default, Debug, Serialize)] +#[derive(Debug, Serialize)] pub struct SquareRefundRequest { - pub amount: i64, + amount_money: SquarePaymentsAmountData, + idempotency_key: Secret, + payment_id: Secret, } 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, + amount_money: SquarePaymentsAmountData { + amount: item.request.refund_amount, + currency: item.request.currency, + }, + idempotency_key: Secret::new(item.request.refund_id.clone()), + payment_id: Secret::new(item.request.connector_transaction_id.clone()), }) } } -// Type definition for Refund Response - -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "UPPERCASE")] pub enum RefundStatus { - Succeeded, + Completed, Failed, - #[default] - Processing, + Pending, + Rejected, } 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 + RefundStatus::Completed => Self::Success, + RefundStatus::Failed | RefundStatus::Rejected => Self::Failure, + RefundStatus::Pending => Self::Pending, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse { - id: String, +#[derive(Debug, Deserialize)] +pub struct SquareRefundResponseDetails { status: RefundStatus, + id: String, +} +#[derive(Debug, Deserialize)] +pub struct RefundResponse { + refund: SquareRefundResponseDetails, } impl TryFrom> @@ -170,8 +410,8 @@ impl TryFrom> ) -> Result { Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + connector_refund_id: item.response.refund.id, + refund_status: enums::RefundStatus::from(item.response.refund.status), }), ..item.data }) @@ -187,19 +427,21 @@ impl TryFrom> ) -> Result { Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + connector_refund_id: item.response.refund.id, + refund_status: enums::RefundStatus::from(item.response.refund.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, +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct SquareErrorDetails { + pub category: Option, + pub code: Option, + pub detail: Option, +} +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct SquareErrorResponse { + pub errors: Vec, } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 22ac930f40..2de2c0725e 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1219,6 +1219,10 @@ pub(crate) fn validate_auth_type( shift4::transformers::Shift4AuthType::try_from(val)?; Ok(()) } + api_enums::Connector::Square => { + square::transformers::SquareAuthType::try_from(val)?; + Ok(()) + } api_enums::Connector::Stax => { stax::transformers::StaxAuthType::try_from(val)?; Ok(()) diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 5782887b9a..9d084a79db 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -59,7 +59,7 @@ pub trait Feature { dyn api::Connector: services::ConnectorIntegration; async fn add_payment_method_token<'a>( - &self, + &mut self, _state: &AppState, _connector: &api::ConnectorData, _tokenization_action: &payments::TokenizationAction, diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 3d68ddf1e6..3129648495 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -118,17 +118,18 @@ impl Feature for types::PaymentsAu } async fn add_payment_method_token<'a>( - &self, + &mut self, state: &AppState, connector: &api::ConnectorData, tokenization_action: &payments::TokenizationAction, ) -> RouterResult> { + let request = self.request.clone(); tokenization::add_payment_method_token( state, connector, tokenization_action, self, - types::PaymentMethodTokenizationData::try_from(self.request.to_owned())?, + types::PaymentMethodTokenizationData::try_from(request)?, ) .await } @@ -346,6 +347,8 @@ impl TryFrom for types::PaymentMethodTokenizationD Ok(Self { payment_method_data: data.payment_method_data, browser_info: data.browser_info, + currency: data.currency, + amount: Some(data.amount), }) } } diff --git a/crates/router/src/core/payments/flows/verify_flow.rs b/crates/router/src/core/payments/flows/verify_flow.rs index 275df0792d..00c47bbb59 100644 --- a/crates/router/src/core/payments/flows/verify_flow.rs +++ b/crates/router/src/core/payments/flows/verify_flow.rs @@ -86,17 +86,18 @@ impl Feature for types::VerifyRouterData } async fn add_payment_method_token<'a>( - &self, + &mut self, state: &AppState, connector: &api::ConnectorData, tokenization_action: &payments::TokenizationAction, ) -> RouterResult> { + let request = self.request.clone(); tokenization::add_payment_method_token( state, connector, tokenization_action, self, - types::PaymentMethodTokenizationData::try_from(self.request.to_owned())?, + types::PaymentMethodTokenizationData::try_from(request)?, ) .await } @@ -234,6 +235,8 @@ impl TryFrom for types::PaymentMethodTokenizationData Ok(Self { payment_method_data: data.payment_method_data, browser_info: None, + currency: data.currency, + amount: data.amount, }) } } diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index e860d55870..c38cb4a48d 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -211,11 +211,11 @@ pub fn create_payment_method_metadata( })) } -pub async fn add_payment_method_token( +pub async fn add_payment_method_token( state: &AppState, connector: &api::ConnectorData, tokenization_action: &payments::TokenizationAction, - router_data: &types::RouterData, + router_data: &mut types::RouterData, pm_token_request_data: types::PaymentMethodTokenizationData, ) -> RouterResult> { match tokenization_action { @@ -230,7 +230,7 @@ pub async fn add_payment_method_token( let pm_token_response_data: Result = Err(types::ErrorResponse::default()); - let pm_token_router_data = payments::helpers::router_data_type_conversion::< + let mut pm_token_router_data = payments::helpers::router_data_type_conversion::< _, api::PaymentMethodToken, _, @@ -242,6 +242,16 @@ pub async fn add_payment_method_token( pm_token_request_data, pm_token_response_data, ); + + connector_integration + .execute_pretasks(&mut pm_token_router_data, state) + .await + .to_payment_failed_response()?; + + router_data + .request + .set_session_token(pm_token_router_data.session_token.clone()); + let resp = services::execute_connector_processing_step( state, connector_integration, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 3d2a1781f3..bfab35d24c 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1129,6 +1129,7 @@ impl TryFrom> for types::VerifyRequestDat Ok(Self { currency: payment_data.currency, confirm: true, + amount: Some(payment_data.amount.into()), payment_method_data: payment_data .payment_method_data .get_required_value("payment_method_data")?, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 324f99700a..3668879dee 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -351,7 +351,7 @@ pub struct AuthorizeSessionTokenData { pub amount_to_capture: Option, pub currency: storage_enums::Currency, pub connector_transaction_id: String, - pub amount: i64, + pub amount: Option, } #[derive(Debug, Clone)] @@ -367,6 +367,8 @@ pub struct ConnectorCustomerData { pub struct PaymentMethodTokenizationData { pub payment_method_data: payments::PaymentMethodData, pub browser_info: Option, + pub currency: storage_enums::Currency, + pub amount: Option, } #[derive(Debug, Clone)] @@ -448,6 +450,7 @@ pub struct PaymentsSessionData { pub struct VerifyRequestData { pub currency: storage_enums::Currency, pub payment_method_data: payments::PaymentMethodData, + pub amount: Option, pub confirm: bool, pub statement_descriptor_suffix: Option, pub mandate_id: Option, @@ -874,7 +877,7 @@ impl From<&&mut PaymentsAuthorizeRouterData> for AuthorizeSessionTokenData { amount_to_capture: data.amount_captured, currency: data.request.currency, connector_transaction_id: data.payment_id.clone(), - amount: data.request.amount, + amount: Some(data.request.amount), } } } @@ -891,6 +894,40 @@ impl From<&&mut PaymentsAuthorizeRouterData> for ConnectorCustomerData { } } +impl From<&RouterData> + for PaymentMethodTokenizationData +{ + fn from(data: &RouterData) -> Self { + Self { + payment_method_data: data.request.payment_method_data.clone(), + browser_info: None, + currency: data.request.currency, + amount: Some(data.request.amount), + } + } +} + +pub trait Tokenizable { + fn get_pm_data(&self) -> payments::PaymentMethodData; + fn set_session_token(&mut self, token: Option); +} + +impl Tokenizable for VerifyRequestData { + fn get_pm_data(&self) -> payments::PaymentMethodData { + self.payment_method_data.clone() + } + fn set_session_token(&mut self, _token: Option) {} +} + +impl Tokenizable for PaymentsAuthorizeData { + fn get_pm_data(&self) -> payments::PaymentMethodData { + self.payment_method_data.clone() + } + fn set_session_token(&mut self, token: Option) { + self.session_token = token; + } +} + impl From<&VerifyRouterData> for PaymentsAuthorizeData { fn from(data: &VerifyRouterData) -> Self { Self { diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 52882a5d9b..b2e1617e5e 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -319,7 +319,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::Square => Ok(Box::new(&connector::Square)), 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/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index aeca3d4977..ca5dd99c9a 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -171,6 +171,7 @@ key1 = "transaction key" [square] api_key="API Key" +key1 = "transaction key" [helcim] api_key="API Key" diff --git a/crates/router/tests/connectors/square.rs b/crates/router/tests/connectors/square.rs index 8dbccd1312..2d4797ced9 100644 --- a/crates/router/tests/connectors/square.rs +++ b/crates/router/tests/connectors/square.rs @@ -1,18 +1,24 @@ +use std::{str::FromStr, time::Duration}; + use masking::Secret; -use router::types::{self, api, storage::enums}; +use router::types::{ + self, api, + storage::{self, enums}, + PaymentsResponseData, +}; use test_utils::connector_auth::ConnectorAuthentication; -use crate::utils::{self, ConnectorActions}; +use crate::utils::{self, get_connector_transaction_id, Connector, ConnectorActions}; #[derive(Clone, Copy)] struct SquareTest; impl ConnectorActions for SquareTest {} -impl utils::Connector for SquareTest { +impl 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, + connector_name: types::Connector::Square, get_token: types::api::GetToken::Connector, } } @@ -33,20 +39,60 @@ impl utils::Connector for SquareTest { static CONNECTOR: SquareTest = SquareTest {}; -fn get_default_payment_info() -> Option { - None +fn get_default_payment_info(payment_method_token: Option) -> Option { + Some(utils::PaymentInfo { + address: None, + auth_type: None, + access_token: None, + connector_meta_data: None, + return_url: None, + connector_customer: None, + payment_method_token, + payout_method_data: None, + currency: None, + country: None, + }) } fn payment_method_details() -> Option { None } +fn token_details() -> Option { + Some(types::PaymentMethodTokenizationData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), + card_exp_month: Secret::new("04".to_string()), + card_exp_year: Secret::new("2027".to_string()), + card_cvc: Secret::new("100".to_string()), + ..utils::CCardType::default().0 + }), + browser_info: None, + amount: None, + currency: storage::enums::Currency::USD, + }) +} + +async fn create_token() -> Option { + let token_response = CONNECTOR + .create_connector_pm_token(token_details(), get_default_payment_info(None)) + .await + .expect("Authorize payment response"); + match token_response.response.unwrap() { + PaymentsResponseData::TokenizationResponse { token } => Some(token), + _ => 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()) + .authorize_payment( + payment_method_details(), + get_default_payment_info(create_token().await), + ) .await .expect("Authorize payment response"); assert_eq!(response.status, enums::AttemptStatus::Authorized); @@ -56,7 +102,11 @@ async fn should_only_authorize_payment() { #[actix_web::test] async fn should_capture_authorized_payment() { let response = CONNECTOR - .authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info()) + .authorize_and_capture_payment( + payment_method_details(), + None, + get_default_payment_info(create_token().await), + ) .await .expect("Capture payment response"); assert_eq!(response.status, enums::AttemptStatus::Charged); @@ -72,7 +122,7 @@ async fn should_partially_capture_authorized_payment() { amount_to_capture: 50, ..utils::PaymentCaptureType::default().0 }), - get_default_payment_info(), + get_default_payment_info(create_token().await), ) .await .expect("Capture payment response"); @@ -83,10 +133,13 @@ async fn should_partially_capture_authorized_payment() { #[actix_web::test] async fn should_sync_authorized_payment() { let authorize_response = CONNECTOR - .authorize_payment(payment_method_details(), get_default_payment_info()) + .authorize_payment( + payment_method_details(), + get_default_payment_info(create_token().await), + ) .await .expect("Authorize payment response"); - let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let txn_id = get_connector_transaction_id(authorize_response.response); let response = CONNECTOR .psync_retry_till_status_matches( enums::AttemptStatus::Authorized, @@ -96,7 +149,7 @@ async fn should_sync_authorized_payment() { ), ..Default::default() }), - get_default_payment_info(), + get_default_payment_info(None), ) .await .expect("PSync response"); @@ -114,7 +167,7 @@ async fn should_void_authorized_payment() { cancellation_reason: Some("requested_by_customer".to_string()), ..Default::default() }), - get_default_payment_info(), + get_default_payment_info(create_token().await), ) .await .expect("Void payment response"); @@ -124,12 +177,21 @@ async fn should_void_authorized_payment() { // Refunds a payment using the manual capture flow (Non 3DS). #[actix_web::test] async fn should_refund_manually_captured_payment() { - let response = CONNECTOR + let refund_response = CONNECTOR .capture_payment_and_refund( payment_method_details(), None, None, - get_default_payment_info(), + get_default_payment_info(create_token().await), + ) + .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(None), ) .await .unwrap(); @@ -142,7 +204,7 @@ async fn should_refund_manually_captured_payment() { // 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 + let refund_response = CONNECTOR .capture_payment_and_refund( payment_method_details(), None, @@ -150,7 +212,16 @@ async fn should_partially_refund_manually_captured_payment() { refund_amount: 50, ..utils::PaymentRefundType::default().0 }), - get_default_payment_info(), + get_default_payment_info(create_token().await), + ) + .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(None), ) .await .unwrap(); @@ -168,7 +239,7 @@ async fn should_sync_manually_captured_refund() { payment_method_details(), None, None, - get_default_payment_info(), + get_default_payment_info(create_token().await), ) .await .unwrap(); @@ -177,7 +248,7 @@ async fn should_sync_manually_captured_refund() { enums::RefundStatus::Success, refund_response.response.unwrap().connector_refund_id, None, - get_default_payment_info(), + get_default_payment_info(None), ) .await .unwrap(); @@ -191,7 +262,10 @@ async fn should_sync_manually_captured_refund() { #[actix_web::test] async fn should_make_payment() { let authorize_response = CONNECTOR - .make_payment(payment_method_details(), get_default_payment_info()) + .make_payment( + payment_method_details(), + get_default_payment_info(create_token().await), + ) .await .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); @@ -201,11 +275,14 @@ async fn should_make_payment() { #[actix_web::test] async fn should_sync_auto_captured_payment() { let authorize_response = CONNECTOR - .make_payment(payment_method_details(), get_default_payment_info()) + .make_payment( + payment_method_details(), + get_default_payment_info(create_token().await), + ) .await .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); - let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let txn_id = get_connector_transaction_id(authorize_response.response); assert_ne!(txn_id, None, "Empty connector transaction id"); let response = CONNECTOR .psync_retry_till_status_matches( @@ -217,7 +294,7 @@ async fn should_sync_auto_captured_payment() { capture_method: Some(enums::CaptureMethod::Automatic), ..Default::default() }), - get_default_payment_info(), + get_default_payment_info(None), ) .await .unwrap(); @@ -227,8 +304,21 @@ 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 refund_response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + None, + get_default_payment_info(create_token().await), + ) + .await + .unwrap(); let response = CONNECTOR - .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(None), + ) .await .unwrap(); assert_eq!( @@ -247,44 +337,85 @@ async fn should_partially_refund_succeeded_payment() { refund_amount: 50, ..utils::PaymentRefundType::default().0 }), - get_default_payment_info(), + get_default_payment_info(create_token().await), ) .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(), + get_default_payment_info(None), + ) + .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() { + //make a successful payment + let response = CONNECTOR + .make_payment( + payment_method_details(), + get_default_payment_info(create_token().await), + ) + .await + .unwrap(); + let refund_data = Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }); + //try refund for previous payment + let transaction_id = get_connector_transaction_id(response.response).unwrap(); + for _x in 0..2 { + tokio::time::sleep(Duration::from_secs(CONNECTOR.get_request_interval())).await; // to avoid 404 error + let refund_response = CONNECTOR + .refund_payment( + transaction_id.clone(), + refund_data.clone(), + get_default_payment_info(None), + ) + .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(None), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); + } +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_refund() { + let refund_response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + None, + get_default_payment_info(create_token().await), + ) + .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(None), ) .await .unwrap(); @@ -298,66 +429,93 @@ async fn should_sync_refund() { // 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 { + let token_response = CONNECTOR + .create_connector_pm_token( + Some(types::PaymentMethodTokenizationData { payment_method_data: types::api::PaymentMethodData::Card(api::Card { - card_cvc: Secret::new("12345".to_string()), + card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), + card_exp_month: Secret::new("11".to_string()), + card_exp_year: Secret::new("2027".to_string()), + card_cvc: Secret::new("".to_string()), ..utils::CCardType::default().0 }), - ..utils::PaymentAuthorizeType::default().0 + browser_info: None, + amount: None, + currency: storage::enums::Currency::USD, }), - get_default_payment_info(), + get_default_payment_info(None), ) .await - .unwrap(); + .expect("Authorize payment response"); assert_eq!( - response.response.unwrap_err().message, - "Your card's security code is invalid.".to_string(), + token_response + .response + .unwrap_err() + .reason + .unwrap_or("".to_string()), + "Missing required parameter.".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 { + let token_response = CONNECTOR + .create_connector_pm_token( + Some(types::PaymentMethodTokenizationData { payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), card_exp_month: Secret::new("20".to_string()), + card_exp_year: Secret::new("2027".to_string()), + card_cvc: Secret::new("123".to_string()), ..utils::CCardType::default().0 }), - ..utils::PaymentAuthorizeType::default().0 + browser_info: None, + amount: None, + currency: storage::enums::Currency::USD, }), - get_default_payment_info(), + get_default_payment_info(None), ) .await - .unwrap(); + .expect("Authorize payment response"); assert_eq!( - response.response.unwrap_err().message, - "Your card's expiration month is invalid.".to_string(), + token_response + .response + .unwrap_err() + .reason + .unwrap_or("".to_string()), + "Invalid card expiration 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(types::PaymentsAuthorizeData { + let token_response = CONNECTOR + .create_connector_pm_token( + Some(types::PaymentMethodTokenizationData { payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), + card_exp_month: Secret::new("11".to_string()), card_exp_year: Secret::new("2000".to_string()), + card_cvc: Secret::new("123".to_string()), ..utils::CCardType::default().0 }), - ..utils::PaymentAuthorizeType::default().0 + browser_info: None, + amount: None, + currency: storage::enums::Currency::USD, }), - get_default_payment_info(), + get_default_payment_info(None), ) .await - .unwrap(); + .expect("Authorize payment response"); assert_eq!( - response.response.unwrap_err().message, - "Your card's expiration year is invalid.".to_string(), + token_response + .response + .unwrap_err() + .reason + .unwrap_or("".to_string()), + "Invalid card expiration date.".to_string(), ); } @@ -365,19 +523,27 @@ async fn should_fail_payment_for_incorrect_expiry_year() { #[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()) + .make_payment( + payment_method_details(), + get_default_payment_info(create_token().await), + ) .await .unwrap(); assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); - let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let txn_id = 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()) + .void_payment( + txn_id.clone().unwrap(), + None, + get_default_payment_info(None), + ) .await .unwrap(); + let connector_transaction_id = txn_id.unwrap(); assert_eq!( - void_response.response.unwrap_err().message, - "You cannot cancel this PaymentIntent because it has a status of succeeded." + void_response.response.unwrap_err().reason.unwrap_or("".to_string()), + format!("Payment {connector_transaction_id} is in inflight state COMPLETED, which is invalid for the requested operation") ); } @@ -385,12 +551,20 @@ async fn should_fail_void_payment_for_auto_capture() { #[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()) + .capture_payment( + "123456789".to_string(), + None, + get_default_payment_info(create_token().await), + ) .await .unwrap(); assert_eq!( - capture_response.response.unwrap_err().message, - String::from("No such payment_intent: '123456789'") + capture_response + .response + .unwrap_err() + .reason + .unwrap_or("".to_string()), + String::from("Could not find payment with id: 123456789") ); } @@ -404,13 +578,17 @@ async fn should_fail_for_refund_amount_higher_than_payment_amount() { refund_amount: 150, ..utils::PaymentRefundType::default().0 }), - get_default_payment_info(), + get_default_payment_info(create_token().await), ) .await .unwrap(); assert_eq!( - response.response.unwrap_err().message, - "Refund amount (₹1.50) is greater than charge amount (₹1.00)", + response + .response + .unwrap_err() + .reason + .unwrap_or("".to_string()), + "The requested refund amount exceeds the amount available to refund.", ); } diff --git a/crates/router/tests/connectors/stax.rs b/crates/router/tests/connectors/stax.rs index 98527d91f3..c4eb2c5132 100644 --- a/crates/router/tests/connectors/stax.rs +++ b/crates/router/tests/connectors/stax.rs @@ -69,6 +69,8 @@ fn token_details() -> Option { ..utils::CCardType::default().0 }), browser_info: None, + amount: None, + currency: enums::Currency::USD, }) } @@ -477,6 +479,8 @@ async fn should_fail_payment_for_incorrect_cvc() { ..utils::CCardType::default().0 }), browser_info: None, + amount: None, + currency: enums::Currency::USD, }), get_default_payment_info(connector_customer_id, None), ) @@ -513,6 +517,8 @@ async fn should_fail_payment_for_invalid_exp_month() { ..utils::CCardType::default().0 }), browser_info: None, + amount: None, + currency: enums::Currency::USD, }), get_default_payment_info(connector_customer_id, None), ) @@ -549,6 +555,8 @@ async fn should_fail_payment_for_incorrect_expiry_year() { ..utils::CCardType::default().0 }), browser_info: None, + amount: None, + currency: enums::Currency::USD, }), get_default_payment_info(connector_customer_id, None), ) diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index e5cb8132c3..ea46502e10 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -963,6 +963,8 @@ impl Default for TokenType { let data = types::PaymentMethodTokenizationData { payment_method_data: types::api::PaymentMethodData::Card(CCardType::default().0), browser_info: None, + amount: Some(100), + currency: enums::Currency::USD, }; Self(data) } diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index fbbd5d1c40..46eca302b2 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -49,7 +49,7 @@ pub struct ConnectorAuthentication { pub powertranz: Option, pub rapyd: Option, pub shift4: Option, - pub square: 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 8fdcad9946..62a3781ba4 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -100,6 +100,7 @@ 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/" +square.secondary_base_url = "https://pci-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/openapi/openapi_spec.json b/openapi/openapi_spec.json index 15eecf072d..731151b480 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -3858,6 +3858,7 @@ "powertranz", "rapyd", "shift4", + "square", "stax", "stripe", "trustpay",