feat(connector): [Square] Implement Card Payments for Square (#1902)

This commit is contained in:
DEEPANSHU BANSAL
2023-08-31 17:09:16 +05:30
committed by GitHub
parent ab85617935
commit c9fe389b2c
21 changed files with 1043 additions and 226 deletions

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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,

View File

@ -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<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Square
where
Self: ConnectorIntegration<Flow, Request, Response>,
@ -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<enums::CaptureMethod>,
) -> 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<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
for Square
@ -131,6 +156,230 @@ impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::Payments
{
}
#[async_trait::async_trait]
impl
ConnectorIntegration<
api::PaymentMethodToken,
types::PaymentMethodTokenizationData,
types::PaymentsResponseData,
> 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<Vec<(String, request::Maskable<String>)>, 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<String, errors::ConnectorError> {
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<Option<types::RequestBody>, errors::ConnectorError> {
let connector_request = square::SquareTokenRequest::try_from(req)?;
let square_req = types::RequestBody::log_and_get_request_body(
&connector_request,
utils::Encode::<square::SquareTokenRequest>::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<Option<services::Request>, 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<types::TokenizationRouterData, errors::ConnectorError>
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<ErrorResponse, errors::ConnectorError> {
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<Vec<(String, request::Maskable<String>)>, 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<String, errors::ConnectorError> {
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<Option<services::Request>, 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<types::PaymentsAuthorizeSessionTokenRouterData, errors::ConnectorError> {
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<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
for Square
{
@ -149,9 +398,9 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
fn get_url(
&self,
_req: &types::PaymentsAuthorizeRouterData,
_connectors: &settings::Connectors,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
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<api::Authorize, types::PaymentsAuthorizeData, types::P
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
let req_obj = square::SquarePaymentsRequest::try_from(req)?;
let square_req = types::RequestBody::log_and_get_request_body(
&req_obj,
utils::Encode::<square::SquarePaymentsRequest>::encode_to_string_of_json,
@ -195,7 +445,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
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<api::PSync, types::PaymentsSyncData, types::PaymentsRe
fn get_url(
&self,
_req: &types::PaymentsSyncRouterData,
_connectors: &settings::Connectors,
req: &types::PaymentsSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
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<api::PSync, types::PaymentsSyncData, types::PaymentsRe
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
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<api::Capture, types::PaymentsCaptureData, types::Payme
fn get_url(
&self,
_req: &types::PaymentsCaptureRouterData,
_connectors: &settings::Connectors,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
}
fn get_request_body(
&self,
_req: &types::PaymentsCaptureRouterData,
) -> CustomResult<Option<types::RequestBody>, 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<api::Capture, types::PaymentsCaptureData, types::Payme
.headers(types::PaymentsCaptureType::get_headers(
self, req, connectors,
)?)
.body(types::PaymentsCaptureType::get_request_body(self, req)?)
.build(),
))
}
@ -329,7 +584,7 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
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<api::Capture, types::PaymentsCaptureData, types::Payme
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
for Square
{
fn get_headers(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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<String, errors::ConnectorError> {
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<Option<services::Request>, 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<types::PaymentsCancelRouterData, errors::ConnectorError> {
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<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData> for Square {
@ -367,9 +683,9 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
fn get_url(
&self,
_req: &types::RefundsRouterData<api::Execute>,
_connectors: &settings::Connectors,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
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<api::Execute, types::RefundsData, types::RefundsRespon
data: &types::RefundsRouterData<api::Execute>,
res: Response,
) -> CustomResult<types::RefundsRouterData<api::Execute>, 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<api::RSync, types::RefundsData, types::RefundsResponse
fn get_url(
&self,
_req: &types::RefundSyncRouterData,
_connectors: &settings::Connectors,
req: &types::RefundSyncRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
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<api::RSync, types::RefundsData, types::RefundsResponse
.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(),
))
}
@ -470,8 +789,9 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
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(),

View File

@ -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<errors::ConnectorError>;
fn try_from(
value: (&types::TokenizationRouterData, BankDebitData),
) -> Result<Self, Self::Error> {
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<String>,
impl TryFrom<(&types::TokenizationRouterData, api_models::payments::Card)> for SquareTokenRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
value: (&types::TokenizationRouterData, api_models::payments::Card),
) -> Result<Self, Self::Error> {
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::<u16>()
.into_report()
.change_context(errors::ConnectorError::DateFormattingFailed)?,
);
let exp_month = Secret::new(
card_data
.card_exp_month
.peek()
.parse::<u16>()
.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<errors::ConnectorError>;
fn try_from(
value: (&types::TokenizationRouterData, PayLaterData),
) -> Result<Self, Self::Error> {
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<errors::ConnectorError>;
fn try_from(value: (&types::TokenizationRouterData, WalletData)) -> Result<Self, Self::Error> {
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<String>,
exp_month: Secret<u16>,
exp_year: Secret<u16>,
number: cards::CardNumber,
expiry_month: Secret<String>,
expiry_year: Secret<String>,
cvc: Secret<String>,
complete: bool,
}
#[derive(Debug, Serialize)]
pub struct SquareTokenizeData {
client_id: Secret<String>,
session_id: Secret<String>,
card_data: SquareCardData,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum SquareTokenRequest {
Card(SquareTokenizeData),
}
impl TryFrom<&types::TokenizationRouterData> for SquareTokenRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::TokenizationRouterData) -> Result<Self, Self::Error> {
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<F, T>
TryFrom<types::ResponseRouterData<F, SquareSessionResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, SquareSessionResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
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<String>,
}
impl<F, T>
TryFrom<types::ResponseRouterData<F, SquareTokenResponse, T, types::PaymentsResponseData>>
for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<F, SquareTokenResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
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<String>,
source_id: Secret<String>,
autocomplete: bool,
external_details: SquarePaymentsRequestExternalDetails,
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for SquarePaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
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<String>,
pub(super) key1: Secret<String>,
}
impl TryFrom<&types::ConnectorAuthType> for SquareAuthType {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
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<SquarePaymentStatus> 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<F, T>
@ -101,64 +334,71 @@ impl<F, T>
item: types::ResponseRouterData<F, SquarePaymentsResponse, T, types::PaymentsResponseData>,
) -> Result<Self, Self::Error> {
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<String>,
payment_id: Secret<String>,
}
impl<F> TryFrom<&types::RefundsRouterData<F>> for SquareRefundRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
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<RefundStatus> 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<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
@ -170,8 +410,8 @@ impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
) -> Result<Self, Self::Error> {
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<types::RefundsResponseRouterData<api::RSync, RefundResponse>>
) -> Result<Self, Self::Error> {
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<String>,
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
pub struct SquareErrorDetails {
pub category: Option<String>,
pub code: Option<String>,
pub detail: Option<String>,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
pub struct SquareErrorResponse {
pub errors: Vec<SquareErrorDetails>,
}

View File

@ -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(())

View File

@ -59,7 +59,7 @@ pub trait Feature<F, T> {
dyn api::Connector: services::ConnectorIntegration<F, T, types::PaymentsResponseData>;
async fn add_payment_method_token<'a>(
&self,
&mut self,
_state: &AppState,
_connector: &api::ConnectorData,
_tokenization_action: &payments::TokenizationAction,

View File

@ -118,17 +118,18 @@ impl Feature<api::Authorize, types::PaymentsAuthorizeData> for types::PaymentsAu
}
async fn add_payment_method_token<'a>(
&self,
&mut self,
state: &AppState,
connector: &api::ConnectorData,
tokenization_action: &payments::TokenizationAction,
) -> RouterResult<Option<String>> {
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<types::PaymentsAuthorizeData> for types::PaymentMethodTokenizationD
Ok(Self {
payment_method_data: data.payment_method_data,
browser_info: data.browser_info,
currency: data.currency,
amount: Some(data.amount),
})
}
}

View File

@ -86,17 +86,18 @@ impl Feature<api::Verify, types::VerifyRequestData> for types::VerifyRouterData
}
async fn add_payment_method_token<'a>(
&self,
&mut self,
state: &AppState,
connector: &api::ConnectorData,
tokenization_action: &payments::TokenizationAction,
) -> RouterResult<Option<String>> {
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<types::VerifyRequestData> for types::PaymentMethodTokenizationData
Ok(Self {
payment_method_data: data.payment_method_data,
browser_info: None,
currency: data.currency,
amount: data.amount,
})
}
}

View File

@ -211,11 +211,11 @@ pub fn create_payment_method_metadata(
}))
}
pub async fn add_payment_method_token<F: Clone, T: Clone>(
pub async fn add_payment_method_token<F: Clone, T: types::Tokenizable + Clone>(
state: &AppState,
connector: &api::ConnectorData,
tokenization_action: &payments::TokenizationAction,
router_data: &types::RouterData<F, T, types::PaymentsResponseData>,
router_data: &mut types::RouterData<F, T, types::PaymentsResponseData>,
pm_token_request_data: types::PaymentMethodTokenizationData,
) -> RouterResult<Option<String>> {
match tokenization_action {
@ -230,7 +230,7 @@ pub async fn add_payment_method_token<F: Clone, T: Clone>(
let pm_token_response_data: Result<types::PaymentsResponseData, types::ErrorResponse> =
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<F: Clone, T: Clone>(
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,

View File

@ -1129,6 +1129,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> 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")?,

View File

@ -351,7 +351,7 @@ pub struct AuthorizeSessionTokenData {
pub amount_to_capture: Option<i64>,
pub currency: storage_enums::Currency,
pub connector_transaction_id: String,
pub amount: i64,
pub amount: Option<i64>,
}
#[derive(Debug, Clone)]
@ -367,6 +367,8 @@ pub struct ConnectorCustomerData {
pub struct PaymentMethodTokenizationData {
pub payment_method_data: payments::PaymentMethodData,
pub browser_info: Option<BrowserInformation>,
pub currency: storage_enums::Currency,
pub amount: Option<i64>,
}
#[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<i64>,
pub confirm: bool,
pub statement_descriptor_suffix: Option<String>,
pub mandate_id: Option<api_models::payments::MandateIds>,
@ -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<F> From<&RouterData<F, PaymentsAuthorizeData, PaymentsResponseData>>
for PaymentMethodTokenizationData
{
fn from(data: &RouterData<F, PaymentsAuthorizeData, PaymentsResponseData>) -> 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<String>);
}
impl Tokenizable for VerifyRequestData {
fn get_pm_data(&self) -> payments::PaymentMethodData {
self.payment_method_data.clone()
}
fn set_session_token(&mut self, _token: Option<String>) {}
}
impl Tokenizable for PaymentsAuthorizeData {
fn get_pm_data(&self) -> payments::PaymentMethodData {
self.payment_method_data.clone()
}
fn set_session_token(&mut self, token: Option<String>) {
self.session_token = token;
}
}
impl From<&VerifyRouterData> for PaymentsAuthorizeData {
fn from(data: &VerifyRouterData) -> Self {
Self {

View File

@ -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)),

View File

@ -171,6 +171,7 @@ key1 = "transaction key"
[square]
api_key="API Key"
key1 = "transaction key"
[helcim]
api_key="API Key"

View File

@ -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<utils::PaymentInfo> {
None
fn get_default_payment_info(payment_method_token: Option<String>) -> Option<utils::PaymentInfo> {
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<types::PaymentsAuthorizeData> {
None
}
fn token_details() -> Option<types::PaymentMethodTokenizationData> {
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<String> {
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.",
);
}

View File

@ -69,6 +69,8 @@ fn token_details() -> Option<types::PaymentMethodTokenizationData> {
..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),
)

View File

@ -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)
}

View File

@ -49,7 +49,7 @@ pub struct ConnectorAuthentication {
pub powertranz: Option<BodyKey>,
pub rapyd: Option<BodyKey>,
pub shift4: Option<HeaderKey>,
pub square: Option<HeaderKey>,
pub square: Option<BodyKey>,
pub stax: Option<HeaderKey>,
pub stripe: Option<HeaderKey>,
pub stripe_au: Option<HeaderKey>,

View File

@ -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/"

View File

@ -3858,6 +3858,7 @@
"powertranz",
"rapyd",
"shift4",
"square",
"stax",
"stripe",
"trustpay",